Merge remote-tracking branch 'origin' into secret-versioning

This commit is contained in:
Tuan Dang
2022-12-26 17:53:10 -05:00
42 changed files with 896 additions and 463 deletions
+1 -1
View File
@@ -321,4 +321,4 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
@@ -10,6 +10,7 @@ import {
} from '../helpers/signup'; } from '../helpers/signup';
import { issueTokens, createToken } from '../helpers/auth'; import { issueTokens, createToken } from '../helpers/auth';
import { INVITED, ACCEPTED } from '../variables'; import { INVITED, ACCEPTED } from '../variables';
import axios from 'axios';
/** /**
* Signup step 1: Initialize account for user under email [email] and send a verification code * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
token = tokens.token; token = tokens.token;
refreshToken = tokens.refreshToken; refreshToken = tokens.refreshToken;
// sending a welcome email to new users
if (process.env.LOOPS_API_KEY) {
await axios.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
},
});
}
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
+1 -1
View File
@@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
Short: "Infisical CLI is used to inject environment variables into any process", Short: "Infisical CLI is used to inject environment variables into any process",
Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
Version: "0.1.14", Version: "0.1.15",
} }
// Execute adds all child commands to the root command and sets flags appropriately. // Execute adds all child commands to the root command and sets flags appropriately.
+12
View File
@@ -60,6 +60,13 @@ var runCmd = &cobra.Command{
return return
} }
secretOverriding, err := cmd.Flags().GetBool("secret-overriding")
if err != nil {
log.Errorln("Unable to parse the secret-overriding flag")
log.Debugln(err)
return
}
shouldExpandSecrets, err := cmd.Flags().GetBool("expand") shouldExpandSecrets, err := cmd.Flags().GetBool("expand")
if err != nil { if err != nil {
log.Errorln("Unable to parse the substitute flag") log.Errorln("Unable to parse the substitute flag")
@@ -84,6 +91,10 @@ var runCmd = &cobra.Command{
secrets = util.SubstituteSecrets(secrets) secrets = util.SubstituteSecrets(secrets)
} }
if secretOverriding {
secrets = util.OverrideWithPersonalSecrets(secrets)
}
if cmd.Flags().Changed("command") { if cmd.Flags().Changed("command") {
command := cmd.Flag("command").Value.String() command := cmd.Flag("command").Value.String()
err = executeMultipleCommandWithEnvs(command, secrets) err = executeMultipleCommandWithEnvs(command, secrets)
@@ -108,6 +119,7 @@ func init() {
runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from")
runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from") runCmd.Flags().String("projectId", "", "The project ID from which your secrets should be pulled from")
runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets")
runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets with the same name over shared secrets")
runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")")
} }
+1
View File
@@ -17,6 +17,7 @@ type ConfigFile struct {
type SingleEnvironmentVariable struct { type SingleEnvironmentVariable struct {
Key string `json:"key"` Key string `json:"key"`
Value string `json:"value"` Value string `json:"value"`
Type string `json:"type"`
} }
type WorkspaceConfigFile struct { type WorkspaceConfigFile struct {
+44
View File
@@ -14,6 +14,9 @@ import (
"golang.org/x/crypto/nacl/box" "golang.org/x/crypto/nacl/box"
) )
const PERSONAL_SECRET_TYPE_NAME = "personal"
const SHARED_SECRET_TYPE_NAME = "shared"
func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) { func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string, workspace models.WorkspaceConfigFile, userCreds models.UserCredentials) (listOfSecrets []models.SingleEnvironmentVariable, err error) {
var pullSecretsRequestResponse models.PullSecretsResponse var pullSecretsRequestResponse models.PullSecretsResponse
response, err := httpClient. response, err := httpClient.
@@ -78,6 +81,7 @@ func getSecretsByWorkspaceIdAndEnvName(httpClient resty.Client, envName string,
env := models.SingleEnvironmentVariable{ env := models.SingleEnvironmentVariable{
Key: string(plainTextKey), Key: string(plainTextKey),
Value: string(plainTextValue), Value: string(plainTextValue),
Type: string(secret.Type),
} }
listOfEnv = append(listOfEnv, env) listOfEnv = append(listOfEnv, env)
@@ -187,6 +191,7 @@ func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string,
env := models.SingleEnvironmentVariable{ env := models.SingleEnvironmentVariable{
Key: string(plainTextKey), Key: string(plainTextKey),
Value: string(plainTextValue), Value: string(plainTextValue),
Type: string(secret.Type),
} }
listOfEnv = append(listOfEnv, env) listOfEnv = append(listOfEnv, env)
@@ -335,9 +340,48 @@ func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.Sing
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{ expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
Key: secret.Key, Key: secret.Key,
Value: expandedVariable, Value: expandedVariable,
Type: secret.Type,
}) })
} }
return expandedSecrets return expandedSecrets
} }
// if two secrets with the same name are found, the one that has type `personal` will be in the returned list
func OverrideWithPersonalSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
personalSecret := make(map[string]models.SingleEnvironmentVariable)
sharedSecret := make(map[string]models.SingleEnvironmentVariable)
secretsToReturn := []models.SingleEnvironmentVariable{}
for _, secret := range secrets {
if secret.Type == PERSONAL_SECRET_TYPE_NAME {
personalSecret[secret.Key] = models.SingleEnvironmentVariable{
Key: secret.Key,
Value: secret.Value,
Type: secret.Type,
}
}
if secret.Type == SHARED_SECRET_TYPE_NAME {
sharedSecret[secret.Key] = models.SingleEnvironmentVariable{
Key: secret.Key,
Value: secret.Value,
Type: secret.Type,
}
}
}
for _, secret := range secrets {
personalValue, personalExists := personalSecret[secret.Key]
sharedValue, sharedExists := sharedSecret[secret.Key]
if personalExists && sharedExists || personalExists && !sharedExists {
secretsToReturn = append(secretsToReturn, personalValue)
} else {
secretsToReturn = append(secretsToReturn, sharedValue)
}
}
return secretsToReturn
}
+1
View File
@@ -34,3 +34,4 @@ Inject environment variables from the platform into an application process.
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None | | `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | None |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` | | `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
| `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None | | `--command` | Pass secrets into chained commands (e.g., `"first-command && second-command; more-commands..."`) | None |
| `--secret-overriding`| Prioritizes personal secrets with the same name over shared secrets | `true` |
+1 -7
View File
@@ -38,13 +38,7 @@ infisical init
infisical run -- [your application start command] infisical run -- [your application start command]
``` ```
Options you can specify: View all available options for `run` command [here](./commands/run)
| Option | Description | Default value |
| ------------- | ----------------------------------------------------------------------------------------------------------- | ------------- |
| `--env` | Used to set the environment that secrets are pulled from. Accepted values: `dev`, `staging`, `test`, `prod` | `dev` |
| `--projectId` | Used to link a local project to the platform (required only if injecting via the service token method) | `None` |
| `--expand` | Parse shell parameter expansions in your secrets (e.g., `${DOMAIN}`) | `true` |
## Examples: ## Examples:
+2 -2
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { memo,useState } from 'react';
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -140,4 +140,4 @@ const InputField = (
} }
}; };
export default React.memo(InputField); export default memo(InputField);
+81
View File
@@ -0,0 +1,81 @@
import React from "react";
import { Switch } from "@headlessui/react";
interface OverrideProps {
id: string;
keyName: string;
value: string;
pos: number;
}
interface ToggleProps {
enabled: boolean;
setEnabled: (value: boolean) => void;
addOverride: (value: OverrideProps) => void;
keyName: string;
value: string;
pos: number;
id: string;
deleteOverride: (id: string) => void;
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
}
/**
* This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values)
* @param obj
* @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
* @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,
deleteOverride,
sharedToHide,
setSharedToHide
}: ToggleProps): JSX.Element {
return (
<Switch
checked={enabled}
onChange={() => {
if (enabled == false) {
addOverride({ id, keyName, value, pos });
setSharedToHide([
...sharedToHide!,
id
])
} else {
setSharedToHide(sharedToHide!.filter(tempId => tempId != id))
deleteOverride(id);
}
setEnabled(!enabled);
}}
className={`${
enabled ? 'bg-primary' : 'bg-bunker-400'
} relative inline-flex h-5 w-9 items-center rounded-full`}
>
<span className="sr-only">Enable notifications</span>
<span
className={`${
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
} inline-block h-3.5 w-3.5 transform rounded-full bg-bunker-800 transition`}
/>
</Switch>
)
}
+5 -4
View File
@@ -16,7 +16,7 @@ type ButtonProps = {
size: string; size: string;
icon?: IconProp; icon?: IconProp;
active?: boolean; active?: boolean;
iconDisabled?: string; iconDisabled?: IconProp;
textDisabled?: string; textDisabled?: string;
type?: ButtonHTMLAttributes<any>['type']; type?: ButtonHTMLAttributes<any>['type'];
}; };
@@ -73,15 +73,16 @@ export default function Button(props: ButtonProps): JSX.Element {
// Setting the text color for the text and icon // Setting the text color for the text and icon
props.color == "mineshaft" && "text-gray-400", props.color == "mineshaft" && "text-gray-400",
props.color != "mineshaft" && props.color != "red" && "text-black", props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black",
props.color == "red" && "text-gray-200", props.color == "red" && "text-gray-200",
activityStatus && props.color != "red" ? "group-hover:text-black" : "", props.color == "none" && "text-gray-200 text-xl",
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "",
props.size == "icon" && "flex items-center justify-center" props.size == "icon" && "flex items-center justify-center"
); );
const textStyle = classNames( const textStyle = classNames(
"relative duration-200", "relative duration-200 text-center w-full",
// Show the loading sign if the loading indicator is on // Show the loading sign if the loading indicator is on
props.loading ? "opacity-0" : "opacity-100", props.loading ? "opacity-0" : "opacity-100",
@@ -3,6 +3,16 @@ import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
interface PopupProps {
buttonText: string;
buttonLink: string;
titleText: string;
emoji: string;
textLine1: string;
textLine2: string;
setCheckDocsPopUpVisible: (value: boolean) => void;
}
/** /**
* This is the notification that pops up at the bottom right when a user performs a certain action * This is the notification that pops up at the bottom right when a user performs a certain action
* @param {object} org * @param {object} org
@@ -23,16 +33,16 @@ export default function BottonRightPopup({
textLine1, textLine1,
textLine2, textLine2,
setCheckDocsPopUpVisible, setCheckDocsPopUpVisible,
}) { }: PopupProps): JSX.Element {
return ( return (
<div <div
class="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6" className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-xl absolute bottom-0 right-0 mr-6 mb-6"
role="alert" role="alert"
> >
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6"> <div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row"> <div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
<div>{titleText}</div> <div>{titleText}</div>
<div class="ml-2.5">{emoji}</div> <div className="ml-2.5">{emoji}</div>
</div> </div>
<button <button
className="mt-1" className="mt-1"
@@ -44,14 +54,14 @@ export default function BottonRightPopup({
/> />
</button> </button>
</div> </div>
<div class="block sm:inline px-6 mt-4 mb-0.5 text-gray-300"> <div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">
{textLine1} {textLine1}
</div> </div>
<div class="block sm:inline mb-4 px-6">{textLine2}</div> <div className="block sm:inline mb-4 px-6">{textLine2}</div>
<div className="flex flex-row px-6 w-full"> <div className="flex flex-row px-6 w-full">
{/*eslint-disable-next-line react/jsx-no-target-blank */} {/*eslint-disable-next-line react/jsx-no-target-blank */}
<a <a
class="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center" className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
href={buttonLink} href={buttonLink}
target="_blank" target="_blank"
rel="noopener" rel="noopener"
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
@@ -1,4 +1,4 @@
import React, { SyntheticEvent, useRef } from 'react'; import { memo, SyntheticEvent, useRef } from 'react';
import { faCircle } from '@fortawesome/free-solid-svg-icons'; import { faCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -11,19 +11,21 @@ interface DashboardInputFieldProps {
onChangeHandler: (value: string, position: number) => void; onChangeHandler: (value: string, position: number) => void;
value: string; value: string;
type: 'varName' | 'value'; type: 'varName' | 'value';
blurred: boolean; blurred?: boolean;
duplicates: string[]; isDuplicate?: boolean;
override?: boolean;
} }
/** /**
* This component renders the input fields on the dashboard * This component renders the input fields on the dashboard
* @param {object} obj - the order number of a keyPair * @param {object} obj - the order number of a keyPair
* @param {number} obj.pos - the order number of a keyPair * @param {number} obj.position - the order number of a keyPair
* @param {function} obj.onChangeHandler - what happens when the input is modified * @param {function} obj.onChangeHandler - what happens when the input is modified
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value * @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
* @param {string} obj.value - value of the InputField * @param {string} obj.value - value of the InputField
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard * @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
* @param {string[]} obj.duplicates - list of all the duplicated key names on the dashboard * @param {boolean} obj.isDuplicate - if the key name is duplicated
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
* @returns * @returns
*/ */
@@ -33,7 +35,8 @@ const DashboardInputField = ({
type, type,
value, value,
blurred, blurred,
duplicates isDuplicate,
override
}: DashboardInputFieldProps) => { }: DashboardInputFieldProps) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => { const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
@@ -45,8 +48,7 @@ const DashboardInputField = ({
if (type === 'varName') { if (type === 'varName') {
const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != ''; const startsWithNumber = !isNaN(Number(value.charAt(0))) && value != '';
const hasDuplicates = duplicates?.includes(value); const error = startsWithNumber || isDuplicate;
const error = startsWithNumber || hasDuplicates;
return ( return (
<div className="flex-col w-full"> <div className="flex-col w-full">
@@ -72,7 +74,7 @@ const DashboardInputField = ({
Should not start with a number Should not start with a number
</p> </p>
)} )}
{hasDuplicates && !startsWithNumber && ( {isDuplicate && !startsWithNumber && (
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs"> <p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
Secret names should be unique Secret names should be unique
</p> </p>
@@ -85,6 +87,7 @@ const DashboardInputField = ({
<div <div
className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`} className={`group relative whitespace-pre flex flex-col justify-center w-full max-w-2xl border border-mineshaft-500 rounded-md`}
> >
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
<input <input
value={value} value={value}
onChange={(e) => onChangeHandler(e.target.value, position)} onChange={(e) => onChangeHandler(e.target.value, position)}
@@ -99,10 +102,13 @@ const DashboardInputField = ({
<div <div
ref={ref} ref={ref}
className={`${ className={`${
blurred blurred && !override
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400' ? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
: '' : ''
} absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`} } ${
override ? 'text-primary-300' : 'text-gray-400'
}
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture max-w-2xl overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
> >
{value.split(REGEX).map((word, id) => { {value.split(REGEX).map((word, id) => {
if (word.match(REGEX) !== null) { if (word.match(REGEX) !== null) {
@@ -153,4 +159,8 @@ const DashboardInputField = ({
return <>Something Wrong</>; return <>Something Wrong</>;
}; };
export default React.memo(DashboardInputField); function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
}
export default memo(DashboardInputField, inputPropsAreEqual);
@@ -0,0 +1,93 @@
import { Fragment,useState } from 'react';
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
/**
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
* @returns the popup-menu for randomly generating secrets
*/
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
const [randomStringLength, setRandomStringLength] = useState(32);
return <Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
</div>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
<div
onClick={() => {
if (randomStringLength > 32) {
setRandomStringLength(32);
} else if (randomStringLength < 2) {
setRandomStringLength(2);
} else {
modifyValue(
[...Array(randomStringLength)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join(''),
position
);
}
}}
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
>
<FontAwesomeIcon
className="text-lg pl-1.5 pr-3"
icon={faShuffle}
/>
<div className="text-sm justify-between flex flex-row w-full">
<p>Generate Random Hex</p>
<p>digits</p>
</div>
</div>
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
<div
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
onClick={() => {
if (randomStringLength > 1) {
setRandomStringLength(randomStringLength - 1);
}
}}
>
-
</div>
<input
onChange={(e) =>
setRandomStringLength(parseInt(e.target.value))
}
value={randomStringLength}
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
spellCheck="false"
/>
<div
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
onClick={() => {
if (randomStringLength < 32) {
setRandomStringLength(randomStringLength + 1);
}
}}
>
+
</div>
</div>
</Menu.Items>
</Transition>
</Menu>
}
export default GenerateSecretMenu;
+102
View File
@@ -0,0 +1,102 @@
import React from 'react';
import { faEllipsis, faShuffle, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
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;
deleteRow: (id: string) => void;
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
sidebarSecretId: string;
}
/**
* This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @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 {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
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @returns
*/
const KeyPair = ({
keyPair,
deleteRow,
modifyKey,
modifyValue,
isBlurred,
isDuplicate,
toggleSidebar,
sidebarSecretId
}: KeyPairProps) => {
return (
<div className={`mx-1 flex flex-col items-center ml-1 ${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">
<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
</span>
</div>}
<div className="min-w-xl w-96">
<div className="flex pr-1 items-center rounded-lg mt-4 md:mt-0 max-h-16">
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={keyPair.pos}
value={keyPair.key}
isDuplicate={isDuplicate}
/>
</div>
</div>
<div className="w-full min-w-5xl">
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 ">
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={keyPair.pos}
value={keyPair.value}
blurred={isBlurred}
override={keyPair.type == "personal"}
/>
</div>
</div>
<div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-9 h-9 bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
<FontAwesomeIcon
className="text-gray-300 px-2.5 text-lg mt-0.5"
icon={faEllipsis}
/>
</div>
<div className="w-2"></div>
<div className="bg-[#9B3535] hover:bg-red rounded-md duration-200">
<Button
onButtonPressed={() => deleteRow(keyPair.id)}
color="none"
size="icon-sm"
icon={faX}
/>
</div>
</div>
</div>
);
};
export default React.memo(KeyPair);
+171
View File
@@ -0,0 +1,171 @@
import { useState } from 'react';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SecretVersionList from 'ee/components/SecretVersionList';
import Button from '../basic/buttons/Button';
import Toggle from '../basic/Toggle';
import DashboardInputField from './DashboardInputField';
import GenerateSecretMenu from './GenerateSecretMenu';
interface SecretProps {
key: string;
value: string;
pos: number;
type: string;
id: string;
}
interface OverrideProps {
id: string;
keyName: string;
value: string;
pos: number;
}
interface SideBarProps {
toggleSidebar: (value: string) => void;
data: SecretProps[];
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
addOverride: (value: OverrideProps) => void;
deleteOverride: (id: string) => void;
buttonReady: boolean;
savePush: () => void;
sharedToHide: string[];
setSharedToHide: (values: string[]) => void;
}
/**
* @param {object} obj
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @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 {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
* @returns the sidebar with 'secret's settings'
*/
const SideBar = ({
toggleSidebar,
data,
modifyKey,
modifyValue,
addOverride,
deleteOverride,
buttonReady,
savePush,
sharedToHide,
setSharedToHide
}: SideBarProps) => {
const [overrideEnabled, setOverrideEnabled] = useState(data.map(secret => secret.type).includes("personal"));
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-50 shadow-xl flex flex-col justify-between'>
<div className='h-min overflow-y-auto'>
<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">Secret</p>
<div className='p-1' onClick={() => toggleSidebar("None")}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='mt-4 px-4 pointer-events-none'>
<p className='text-sm text-bunker-300'>Key</p>
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={data[0].pos}
value={data[0].key}
isDuplicate={false}
blurred={false}
/>
</div>
{data.filter(secret => secret.type == "shared")[0]?.value
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<p className='text-sm text-bunker-300'>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}
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} />
</div>
</div>
: <div className='px-4 text-sm text-bunker-300 pt-4'>
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>Note:</span>
This secret is personal. It is not shared with any of your teammates.
</div>}
<div className='mt-4 px-4'>
{data.filter(secret => secret.type == "shared")[0]?.value &&
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
<p className='text-sm text-bunker-300'>Override value with a personal value</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={addOverride}
keyName={data[0].key}
value={data[0].value}
pos={data[0].pos}
id={data[0].id}
deleteOverride={deleteOverride}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
/>
</div>}
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<DashboardInputField
onChangeHandler={modifyValue}
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}
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} />
</div>
</div>
</div>
{/* <div className={`relative mt-4 px-4 opacity-80 duration-200`}>
<p className='text-sm text-bunker-200'>Group</p>
<ListBox
selected={"Database Secrets"}
onChange={() => {}}
data={["Group1"]}
isFull={true}
/>
</div> */}
<div className={`relative mt-4 px-4 pt-4`}>
<div className='flex flex-row justify-between'>
<p className='text-sm text-bunker-300'>Comments & notes</p>
<div className="bg-yellow rounded-md h-min">
<p className="relative text-black text-xs px-1.5 h-min">Coming soon!</p>
</div>
</div>
<div className='h-32 opacity-50 w-full bg-bunker-800 p-2 rounded-md border border-mineshaft-500 rounded-md text-sm text-bunker-300'>
Leave your comment here...
</div>
</div>
</div>
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
<Button
text="Save Changes"
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
textDisabled="Saved"
/>
</div>
</div>
};
export default SideBar;
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
faArrowRight, faArrowRight,
@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable react/jsx-key */ /* eslint-disable react/jsx-key */
import React, { Fragment, useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons'; import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { import {
faAngleRight, faAngleRight,
@@ -39,7 +39,7 @@ const getSecretsForProject = async ({
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
const tempFileState: { key: string; value: string; type: string }[] = []; const tempFileState: { key: string; value: string; type: 'personal' | 'shared'; }[] = [];
if (file.key) { if (file.key) {
// assymmetrically decrypt symmetric key with local private key // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -97,7 +97,7 @@ const getSecretsForProject = async ({
} catch (error) { } catch (error) {
console.log('Something went wrong during accessing or decripting secrets.'); console.log('Something went wrong during accessing or decripting secrets.');
} }
return true; return [];
}; };
export default getSecretsForProject; export default getSecretsForProject;
@@ -51,7 +51,7 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
iv: ivKey, iv: ivKey,
tag: tagKey, tag: tagKey,
} = encryptSymmetric({ } = encryptSymmetric({
plaintext: key, plaintext: key.slice(1),
key: randomBytes, key: randomBytes,
}); });
@@ -65,13 +65,13 @@ const pushKeys = async({ obj, workspaceId, env }: { obj: object; workspaceId: st
key: randomBytes, key: randomBytes,
}); });
const visibility = obj[key as keyof typeof obj][1] != null ? obj[key as keyof typeof obj][1] : "personal"; const visibility = key.charAt(0) == "p" ? "personal" : "shared";
return { return {
ciphertextKey, ciphertextKey,
ivKey, ivKey,
tagKey, tagKey,
hashKey: crypto.createHash("sha256").update(key).digest("hex"), hashKey: crypto.createHash("sha256").update(key.slice(1)).digest("hex"),
ciphertextValue, ciphertextValue,
ivValue, ivValue,
tagValue, tagValue,
@@ -0,0 +1,44 @@
import { useState } from 'react';
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface SecretVersionListProps {}
const versionData = [{
value: "Value1",
date: "Date1",
user: "[email protected]"
}, {
value: "Value2",
date: "Date2",
user: "[email protected]"
}]
/**
* @returns a list of the versions for a specific secret
*/
const SecretVersionList = () => {
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
<p className=''>Version History</p>
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none'>
<div className='h-48 overflow-y-scroll overflow-x-none'>
{versionData.map((version, index) =>
<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'>{version.date}</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 className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Updated by:</span>{version.user}</p></div>
</div>
</div>
)}
</div>
</div>
</div>
};
export default SecretVersionList;
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -1,4 +1,4 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react'; import { Fragment, useCallback, useEffect, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@@ -6,208 +6,68 @@ import {
faArrowDownAZ, faArrowDownAZ,
faArrowDownZA, faArrowDownZA,
faCheck, faCheck,
faCircleInfo,
faCopy, faCopy,
faDownload, faDownload,
faEllipsis,
faEye, faEye,
faEyeSlash, faEyeSlash,
faFolderOpen, faFolderOpen,
faMagnifyingGlass, faMagnifyingGlass,
faPeopleGroup,
faPerson,
faPlus, faPlus,
faShuffle,
faX
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
import Button from '~/components/basic/buttons/Button'; import Button from '~/components/basic/buttons/Button';
import ListBox from '~/components/basic/Listbox'; import ListBox from '~/components/basic/Listbox';
import BottonRightPopup from '~/components/basic/popups/BottomRightPopup'; import BottonRightPopup from '~/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import DashboardInputField from '~/components/dashboard/DashboardInputField';
import DropZone from '~/components/dashboard/DropZone'; import DropZone from '~/components/dashboard/DropZone';
import KeyPair from '~/components/dashboard/KeyPair';
import SideBar from '~/components/dashboard/SideBar';
import NavHeader from '~/components/navigation/NavHeader'; import NavHeader from '~/components/navigation/NavHeader';
import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject'; import getSecretsForProject from '~/components/utilities/secrets/getSecretsForProject';
import pushKeys from '~/components/utilities/secrets/pushKeys'; import pushKeys from '~/components/utilities/secrets/pushKeys';
import pushKeysIntegration from '~/components/utilities/secrets/pushKeysIntegration';
import guidGenerator from '~/utilities/randomId'; import guidGenerator from '~/utilities/randomId';
import { envMapping } from '../../public/data/frequentConstants'; import { envMapping } from '../../public/data/frequentConstants';
import getWorkspaceIntegrations from '../api/integrations/getWorkspaceIntegrations';
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 getWorkspaces from '../api/workspace/getWorkspaces'; import getWorkspaces from '../api/workspace/getWorkspaces';
/**
* This component represent a single row for an environemnt variable on the dashboard
* @param {object} obj
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @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.modifyVisibility - switch between public/private visibility
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
* @param {string[]} obj.duplicates - list of all the duplicates secret names on the dashboard
* @returns
*/
const KeyPair = ({
keyPair,
deleteRow,
modifyKey,
modifyValue,
modifyVisibility,
isBlurred,
duplicates
}) => {
const [randomStringLength, setRandomStringLength] = useState(32);
return ( interface SecretDataProps {
<div className="px-1 flex flex-col items-center ml-1"> type: 'personal' | 'shared';
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-2"> pos: number;
<div className="min-w-xl w-96"> key: string;
<div className="flex items-center md:px-1 rounded-lg mt-4 md:mt-0 max-h-16"> value: string;
<DashboardInputField id: string;
onChangeHandler={modifyKey} }
type="varName"
position={keyPair.pos} /**
value={keyPair.key} * this function finds the teh duplicates in an array
duplicates={duplicates} * @param arr - array of anything (e.g., with secret keys and types (personal/shared))
/> * @returns - a list with duplicates
</div> */
</div> function findDuplicates(arr: any[]) {
<div className="w-full min-w-5xl"> const map = new Map();
<div className="flex min-w-7xl items-center pl-1 pr-1.5 rounded-lg mt-4 md:mt-0 max-h-10 "> return arr.filter((item) => {
<DashboardInputField if (map.has(item)) {
onChangeHandler={modifyValue} map.set(item, false);
type="value" return true;
position={keyPair.pos} } else {
value={keyPair.value} map.set(item, true);
blurred={isBlurred} return false;
/> }
</div> });
</div> }
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
<div className="cursor-pointer w-9 h-9 bg-white/10 rounded-md flex flex-row justify-center items-center opacity-50 hover:opacity-100 duration-200">
<FontAwesomeIcon
className="text-gray-300 px-2.5 text-lg mt-0.5"
icon={faEllipsis}
/>
</div>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
<div
onClick={() =>
modifyVisibility(
keyPair.type == 'personal' ? 'shared' : 'personal',
keyPair.pos
)
}
className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
>
<FontAwesomeIcon
className="text-lg pl-1.5 pr-3"
icon={keyPair.type == 'personal' ? faPeopleGroup : faPerson}
/>
<div className="text-sm">
{keyPair.type == 'personal' ? 'Make Shared' : 'Make Personal'}
</div>
</div>
<div
onClick={() => {
if (randomStringLength > 32) {
setRandomStringLength(32);
} else if (randomStringLength < 2) {
setRandomStringLength(2);
} else {
modifyValue(
[...Array(randomStringLength)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join(''),
keyPair.pos
);
}
}}
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
>
<FontAwesomeIcon
className="text-lg pl-1.5 pr-3"
icon={keyPair.value == '' ? faPlus : faShuffle}
/>
<div className="text-sm justify-between flex flex-row w-full">
<p>Generate Random Hex</p>
<p>digits</p>
</div>
</div>
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
<div
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
onClick={() => {
if (randomStringLength > 1) {
setRandomStringLength(randomStringLength - 1);
}
}}
>
-
</div>
<input
onChange={(e) =>
setRandomStringLength(parseInt(e.target.value))
}
value={randomStringLength}
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
spellCheck="false"
/>
<div
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
onClick={() => {
if (randomStringLength < 32) {
setRandomStringLength(randomStringLength + 1);
}
}}
>
+
</div>
</div>
</Menu.Items>
</Transition>
</Menu>
<div className="w-2"></div>
<div className="opacity-50 hover:opacity-100 duration-200">
<Button
onButtonPressed={() => deleteRow(keyPair.id)}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</div>
</div>
);
};
/** /**
* This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets) * This is the main component for the dashboard (aka the screen with all the encironemnt variable & secrets)
* @returns * @returns
*/ */
export default function Dashboard() { export default function Dashboard() {
const [data, setData] = useState(); const [data, setData] = useState<SecretDataProps[] | null>();
const [fileState, setFileState] = useState([]); const [fileState, setFileState] = useState<SecretDataProps[]>([]);
const [buttonReady, setButtonReady] = useState(false); const [buttonReady, setButtonReady] = useState(false);
const router = useRouter(); const router = useRouter();
const [workspaceId, setWorkspaceId] = useState(''); const [workspaceId, setWorkspaceId] = useState('');
@@ -227,6 +87,8 @@ export default function Dashboard() {
const [sortMethod, setSortMethod] = useState('alphabetical'); const [sortMethod, setSortMethod] = useState('alphabetical');
const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false); const [checkDocsPopUpVisible, setCheckDocsPopUpVisible] = useState(false);
const [hasUserEverPushed, setHasUserEverPushed] = useState(false); const [hasUserEverPushed, setHasUserEverPushed] = useState(false);
const [sidebarSecretId, toggleSidebar] = useState("None");
const [sharedToHide, setSharedToHide] = useState<string[]>([]);
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
@@ -249,7 +111,7 @@ export default function Dashboard() {
useEffect(() => { useEffect(() => {
const warningText = const warningText =
'Do you want to save your results before leaving this page?'; 'Do you want to save your results before leaving this page?';
const handleWindowClose = (e) => { const handleWindowClose = (e: any) => {
if (!buttonReady) return; if (!buttonReady) return;
e.preventDefault(); e.preventDefault();
return (e.returnValue = warningText); return (e.returnValue = warningText);
@@ -265,18 +127,18 @@ export default function Dashboard() {
/** /**
* Reorder rows alphabetically or in the opprosite order * Reorder rows alphabetically or in the opprosite order
*/ */
const reorderRows = (dataToReorder) => { const reorderRows = (dataToReorder: SecretDataProps[] | 1) => {
setSortMethod((prevSort) => setSortMethod((prevSort) =>
prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical' prevSort == 'alphabetical' ? '-alphabetical' : 'alphabetical'
); );
sortValuesHandler(dataToReorder); sortValuesHandler(dataToReorder, undefined);
}; };
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
let userWorkspaces = await getWorkspaces(); const userWorkspaces = await getWorkspaces();
const listWorkspaces = userWorkspaces.map((workspace) => workspace._id); const listWorkspaces = userWorkspaces.map((workspace) => workspace._id);
if ( if (
!listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0]) !listWorkspaces.includes(router.asPath.split('/')[2].split('?')[0])
@@ -288,31 +150,41 @@ export default function Dashboard() {
router.push(router.asPath.split('?')[0] + '?' + env); router.push(router.asPath.split('?')[0] + '?' + env);
} }
setBlurred(true); setBlurred(true);
setWorkspaceId(router.query.id); setWorkspaceId(String(router.query.id));
const dataToSort = await getSecretsForProject({ const dataToSort = await getSecretsForProject({
env, env,
setFileState, setFileState,
setIsKeyAvailable, setIsKeyAvailable,
setData, setData,
workspaceId: router.query.id workspaceId: String(router.query.id)
}); });
reorderRows(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)
)
const user = await getUser(); const user = await getUser();
setIsNew( setIsNew(
(Date.parse(new Date()) - Date.parse(user.createdAt)) / 60000 < 3 (Date.parse(String(new Date())) - Date.parse(user.createdAt)) / 60000 < 3
? true ? true
: false : false
); );
let userAction = await checkUserAction({ const userAction = await checkUserAction({
action: 'first_time_secrets_pushed' action: 'first_time_secrets_pushed'
}); });
setHasUserEverPushed(userAction ? true : false); setHasUserEverPushed(userAction ? true : false);
} catch (error) { } catch (error) {
console.log('Error', error); console.log('Error', error);
setData([]); setData(undefined);
} }
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -321,10 +193,10 @@ export default function Dashboard() {
const addRow = () => { const addRow = () => {
setIsNew(false); setIsNew(false);
setData([ setData([
...data, ...data!,
{ {
id: guidGenerator(), id: guidGenerator(),
pos: data.length, pos: data!.length,
key: '', key: '',
value: '', value: '',
type: 'shared' type: 'shared'
@@ -332,45 +204,85 @@ export default function Dashboard() {
]); ]);
}; };
const deleteRow = (id) => { interface overrideProps {
setButtonReady(true); id: string;
setData(data.filter((row) => row.id !== id)); keyName: string;
value: string;
pos: number;
}
/**
* 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 }: overrideProps) => {
setIsNew(false);
const tempdata: SecretDataProps[] | 1 = [
...data!,
{
id: id,
pos: pos,
key: keyName,
value: value,
type: 'personal'
}
];
sortValuesHandler(tempdata, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical");
}; };
const modifyValue = (value, pos) => { const deleteRow = (id: string) => {
setButtonReady(true);
setData(data!.filter((row: SecretDataProps) => row.id !== id));
};
/**
* This function deleted the override of a certain secrer
* @param {string} id - id of a secret to be deleted
*/
const deleteOverride = (id: string) => {
setButtonReady(true);
const tempData = data!.filter((row: SecretDataProps) => !(row.id == id && row.type == 'personal'))
sortValuesHandler(tempData, sortMethod == "alhpabetical" ? "-alphabetical" : "alphabetical")
};
const modifyValue = (value: string, pos: number) => {
setData((oldData) => { setData((oldData) => {
oldData[pos].value = value; oldData![pos].value = value;
return [...oldData]; return [...oldData!];
}); });
setButtonReady(true); setButtonReady(true);
}; };
const modifyKey = (value, pos) => { const modifyKey = (value: string, pos: number) => {
setData((oldData) => { setData((oldData) => {
oldData[pos].key = value; oldData![pos].key = value;
return [...oldData]; return [...oldData!];
}); });
setButtonReady(true); setButtonReady(true);
}; };
const modifyVisibility = (value, pos) => { const modifyVisibility = (value: "shared" | "personal", pos: number) => {
setData((oldData) => { setData((oldData) => {
oldData[pos].type = value; oldData![pos].type = value;
return [...oldData]; return [...oldData!];
}); });
setButtonReady(true); setButtonReady(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, pos) => { const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos); modifyValue(value, pos);
}, []); }, []);
const listenChangeKey = useCallback((value, pos) => { const listenChangeKey = useCallback((value: string, pos: number) => {
modifyKey(value, pos); modifyKey(value, pos);
}, []); }, []);
const listenChangeVisibility = useCallback((value, pos) => { const listenChangeVisibility = useCallback((value: "shared" | "personal", pos: number) => {
modifyVisibility(value, pos); modifyVisibility(value, pos);
}, []); }, []);
@@ -379,21 +291,16 @@ export default function Dashboard() {
*/ */
const savePush = async () => { const savePush = async () => {
// Format the new object with environment variables // Format the new object with environment variables
let obj = Object.assign( const obj = Object.assign(
{}, {},
...data.map((row) => ({ [row.key]: [row.value, row.type] })) ...data!.map((row: SecretDataProps) => ({ [row.type.charAt(0) + row.key]: [row.value, row.type] }))
); );
// 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 = !Object.keys(obj) const nameErrors = !Object.keys(obj)
.map((key) => !isNaN(key.charAt(0))) .map((key) => !isNaN(Number(key[0].charAt(0))))
.every((v) => v === false); .every((v) => v === false);
const duplicatesExist = const duplicatesExist = findDuplicates(data!.map((item: SecretDataProps) => item.key + item.type)).length > 0;
data
?.map((item) => item.key)
.filter(
(item, index) => index !== data?.map((item) => item.key).indexOf(item)
).length > 0;
if (nameErrors) { if (nameErrors) {
return createNotification({ return createNotification({
@@ -409,9 +316,11 @@ export default function Dashboard() {
}); });
} }
console.log('pushing', obj)
// Once "Save changed is clicked", disable that button // Once "Save changed is clicked", disable that button
setButtonReady(false); setButtonReady(false);
pushKeys({ obj, workspaceId: router.query.id, env }); pushKeys({ obj, workspaceId: String(router.query.id), env });
// If this user has never saved environment variables before, show them a prompt to read docs // If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) { if (!hasUserEverPushed) {
@@ -420,8 +329,8 @@ export default function Dashboard() {
} }
}; };
const addData = (newData) => { const addData = (newData: SecretDataProps[]) => {
setData(data.concat(newData)); setData(data!.concat(newData));
setButtonReady(true); setButtonReady(true);
}; };
@@ -429,37 +338,39 @@ export default function Dashboard() {
setBlurred(!blurred); setBlurred(!blurred);
}; };
const sortValuesHandler = (dataToSort) => { const sortValuesHandler = (dataToSort: SecretDataProps[] | 1, specificSortMethod?: 'alphabetical' | '-alphabetical') => {
const sortedData = (dataToSort != 1 ? dataToSort : data) const howToSort = specificSortMethod == undefined ? sortMethod : specificSortMethod;
.sort((a, b) => const sortedData = (dataToSort != 1 ? dataToSort : data)!
sortMethod == 'alphabetical' .sort((a, b) =>
? a.key.localeCompare(b.key) howToSort == 'alphabetical'
: b.key.localeCompare(a.key) ? a.key.localeCompare(b.key)
) : b.key.localeCompare(a.key)
.map((item, index) => { )
return { .map((item: SecretDataProps, index: number) => {
...item, return {
pos: index ...item,
}; pos: index
}); };
});
console.log('override', sortedData)
setData(sortedData); setData(sortedData);
}; };
// This function downloads the secrets as a .env file // This function downloads the secrets as a .env file
const download = () => { const download = () => {
const file = data const file = data!
.map((item) => [item.key, item.value].join('=')) .map((item: SecretDataProps) => [item.key, item.value].join('='))
.join('\n'); .join('\n');
const blob = new Blob([file]); const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob); const fileDownloadUrl = URL.createObjectURL(blob);
let alink = document.createElement('a'); const alink = document.createElement('a');
alink.href = fileDownloadUrl; alink.href = fileDownloadUrl;
alink.download = envMapping[env] + '.env'; alink.download = envMapping[env] + '.env';
alink.click(); alink.click();
}; };
const deleteCertainRow = (id) => { const deleteCertainRow = (id: string) => {
deleteRow(id); deleteRow(id);
}; };
@@ -467,15 +378,17 @@ export default function Dashboard() {
* This function copies the project id to the clipboard * This function copies the project id to the clipboard
*/ */
function copyToClipboard() { function copyToClipboard() {
var copyText = document.getElementById('myInput'); const copyText = document.getElementById('myInput') as HTMLInputElement;
copyText.select(); if (copyText) {
copyText.setSelectionRange(0, 99999); // For mobile devices copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
navigator.clipboard.writeText(copyText.value); navigator.clipboard.writeText(copyText.value);
setProjectIdCopied(true); setProjectIdCopied(true);
setTimeout(() => setProjectIdCopied(false), 2000); setTimeout(() => setProjectIdCopied(false), 2000);
}
} }
return data ? ( return data ? (
@@ -491,6 +404,18 @@ export default function Dashboard() {
/> />
</Head> </Head>
<div className="flex flex-row"> <div className="flex flex-row">
{sidebarSecretId != "None" && <SideBar
toggleSidebar={toggleSidebar}
data={data.filter((row: SecretDataProps) => row.id == sidebarSecretId)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
addOverride={addOverride}
deleteOverride={deleteOverride}
buttonReady={buttonReady}
savePush={savePush}
sharedToHide={sharedToHide}
setSharedToHide={setSharedToHide}
/>}
<div className="w-full max-h-96 pb-2"> <div className="w-full max-h-96 pb-2">
<NavHeader pageName="Secrets" isProjectRelated={true} /> <NavHeader pageName="Secrets" isProjectRelated={true} />
{checkDocsPopUpVisible && ( {checkDocsPopUpVisible && (
@@ -513,7 +438,6 @@ export default function Dashboard() {
data={['Development', 'Staging', 'Production', 'Testing']} data={['Development', 'Staging', 'Production', 'Testing']}
// ref={useRef(123)} // ref={useRef(123)}
onChange={setEnv} onChange={setEnv}
className="z-40"
/> />
)} )}
</div> </div>
@@ -568,7 +492,6 @@ export default function Dashboard() {
data={['Development', 'Staging', 'Production', 'Testing']} data={['Development', 'Staging', 'Production', 'Testing']}
// ref={useRef(123)} // ref={useRef(123)}
onChange={setEnv} onChange={setEnv}
className="z-40"
/> />
<div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center"> <div className="h-10 w-full bg-white/5 hover:bg-white/10 ml-2 flex items-center rounded-md flex flex-row items-center">
<FontAwesomeIcon <FontAwesomeIcon
@@ -630,144 +553,55 @@ export default function Dashboard() {
</div> </div>
</div> </div>
{data?.length !== 0 ? ( {data?.length !== 0 ? (
<div <div className="flex flex-col w-full mt-1 mb-2">
id="dataall" <div className='bg-mineshaft-800 rounded-md px-2 py-2 max-w-5xl'>
className="flex flex-col max-h-40 grow max-h-[calc(100vh-240px)] w-full overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar" <div
> className={`mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar`}
<div >
className={`bg-white/5 mt-1 mb-1 rounded-md pb-2 max-w-5xl overflow-visible`} <div className="px-1 pt-2">
> {data?.filter(row => !(sharedToHide.includes(row.id) && row.type == 'shared')).map((keyPair) => (
<div className="rounded-t-md sticky top-0 z-20 bg-bunker flex flex-row pl-4 pr-6 pt-4 pb-2 items-center justify-between text-gray-300 font-bold">
{/* <FontAwesomeIcon icon={faAngleDown} /> */}
<div className="flex flex-row items-center">
<p className="pl-2 text-lg">Personal</p>
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
<FontAwesomeIcon
className="ml-3 mt-1 text-lg"
icon={faCircleInfo}
/>
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
Personal keys are only visible to you
</span>
</div>
</div>
</div>
<div id="data1" className="">
{data
.filter(
(keyPair) =>
keyPair.key
.toLowerCase()
.includes(searchKeys.toLowerCase()) &&
keyPair.type == 'personal'
)
?.map((keyPair) => (
<KeyPair <KeyPair
key={keyPair.id} key={keyPair.id}
keyPair={keyPair} keyPair={keyPair}
deleteRow={deleteCertainRow} deleteRow={deleteCertainRow}
modifyValue={listenChangeValue} modifyValue={listenChangeValue}
modifyKey={listenChangeKey} modifyKey={listenChangeKey}
modifyVisibility={listenChangeVisibility}
isBlurred={blurred} isBlurred={blurred}
duplicates={data isDuplicate={findDuplicates(data?.map((item) => item.key + item.type))?.includes(keyPair.key + keyPair.type)}
?.map((item) => item.key) toggleSidebar={toggleSidebar}
.filter( sidebarSecretId={sidebarSecretId}
(item, index) =>
index !==
data?.map((item) => item.key).indexOf(item)
)}
/> />
))} ))}
</div> </div>
</div> <div className="w-full max-w-5xl px-2 pt-2">
<div <DropZone
className={`bg-white/5 mt-1 mb-2 rounded-md p-1 pb-2 max-w-5xl ${ setData={addData}
data?.length > 8 ? 'h-3/4' : 'h-min' setErrorDragAndDrop={setErrorDragAndDrop}
}`} createNewFile={addRow}
> errorDragAndDrop={errorDragAndDrop}
<div className="sticky top-0 z-40 bg-bunker flex flex-row pl-4 pr-5 pt-4 pb-2 items-center justify-between text-gray-300 font-bold"> setButtonReady={setButtonReady}
{/* <FontAwesomeIcon icon={faAngleDown} /> */} keysExist={true}
<div className="flex flex-row items-center"> numCurrentRows={data.length}
<p className="pl-2 text-lg">Shared</p> />
<div className="group font-normal group relative inline-block text-gray-300 underline hover:text-primary duration-200">
<FontAwesomeIcon
className="ml-3 text-lg mt-1"
icon={faCircleInfo}
/>
<span className="absolute hidden group-hover:flex group-hover:animate-popdown duration-300 w-44 -left-16 -top-7 translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-1/2 after:bottom-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-gray-700">
Shared keys are visible to your whole team
</span>
</div>
</div> </div>
</div> </div>
<div id="data2" className="data2">
{data
.filter(
(keyPair) =>
keyPair.key
.toLowerCase()
.includes(searchKeys.toLowerCase()) &&
keyPair.type == 'shared'
)
?.map((keyPair) => (
<KeyPair
key={keyPair.id}
keyPair={keyPair}
deleteRow={deleteCertainRow}
modifyValue={listenChangeValue}
modifyKey={listenChangeKey}
modifyVisibility={listenChangeVisibility}
isBlurred={blurred}
duplicates={data
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
data?.map((item) => item.key).indexOf(item)
)}
/>
))}
</div>
</div>
<div className="w-full max-w-5xl">
<DropZone
setData={addData}
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
keysExist={true}
numCurrentRows={data.length}
/>
</div> </div>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28"> <div className="flex flex-col items-center justify-center h-full text-xl text-gray-400 max-w-5xl mt-28">
{fileState.message != "There's nothing to pull" && {isKeyAvailable && (
fileState.message != undefined && ( <DropZone
<FontAwesomeIcon setData={setData}
className="text-7xl mb-8" setErrorDragAndDrop={setErrorDragAndDrop}
icon={faFolderOpen} createNewFile={addRow}
/> errorDragAndDrop={errorDragAndDrop}
)} setButtonReady={setButtonReady}
{(fileState.message == "There's nothing to pull" || numCurrentRows={data.length}
fileState.message == undefined) && keysExist={false}
isKeyAvailable && ( />
<DropZone
setData={setData}
setErrorDragAndDrop={setErrorDragAndDrop}
createNewFile={addRow}
errorDragAndDrop={errorDragAndDrop}
setButtonReady={setButtonReady}
numCurrentRows={data.length}
/>
)}
{fileState.message ==
'Failed membership validation for workspace' && (
<p>You are not authorized to view this project.</p>
)} )}
{fileState.message == 'Access needed to pull the latest file' || {
// fileState.message == 'Access needed to pull the latest file' ||
(!isKeyAvailable && ( (!isKeyAvailable && (
<> <>
<FontAwesomeIcon <FontAwesomeIcon
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
const queryString = require("query-string"); const queryString = require("query-string");
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { IconProp } from '@fortawesome/fontawesome-svg-core';
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
export default function Home() { export default function Home() {
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
const queryString = require("query-string"); const queryString = require("query-string");
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import Plan from "~/components/billing/Plan"; import Plan from "~/components/billing/Plan";
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { import {
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faPlus } from "@fortawesome/free-solid-svg-icons";
+65 -46
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import ReactCodeInput from 'react-code-input'; import ReactCodeInput from 'react-code-input';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
@@ -260,46 +260,49 @@ export default function SignUp() {
// Step 1 of the sign up process (enter the email or choose google authentication) // Step 1 of the sign up process (enter the email or choose google authentication)
const step1 = ( const step1 = (
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 mb-48 md:mb-16 rounded-xl drop-shadow-xl"> <div>
<p className="text-4xl font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary"> <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">
{'Let\''}s get started <p className="text-4xl font-semibold flex justify-center text-primary">
</p> {'Let\''}s get started
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-h-24 max-w-md mx-auto pt-2"> </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"
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
error={emailError}
errorText={emailErrorMessage}
autoComplete="username"
/>
</div>
{/* <div className='flex flex-row justify-left mt-4 max-w-md mx-auto'>
<Checkbox className="mr-4"/>
<p className='text-sm'>I do not want to receive emails about Infisical and its products.</p>
</div> */}
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
<p className="text-gray-400 mt-2 md:mx-0.5">
By creating an account, you agree to our Terms and have read and
acknowledged the Privacy Policy.
</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" />
</div>
</div>
</div>
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2">
<Link href="/login"> <Link href="/login">
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200"> <button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
<u className="font-normal text-md text-sky-500"> <u className="font-normal text-sm text-primary-500">
Have an account? Log in Have an account? Log in
</u> </u>
</button> </button>
</Link> </Link>
</div> </div>
<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"
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
error={emailError}
errorText={emailErrorMessage}
autoComplete="username"
/>
</div>
{/* <div className='flex flex-row justify-left mt-4 max-w-md mx-auto'>
<Checkbox className="mr-4"/>
<p className='text-sm'>I do not want to receive emails about Infisical and its products.</p>
</div> */}
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
<p className="text-gray-400 mt-2 md:mx-0.5">
By creating an account, you agree to our Terms and have read and
acknowledged the Privacy Policy.
</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" />
</div>
</div>
</div> </div>
); );
// Step 2 of the signup process (enter the email verification code) // Step 2 of the signup process (enter the email verification code)
@@ -340,11 +343,11 @@ export default function SignUp() {
<Button text="Verify" onButtonPressed={incrementStep} size="lg" /> <Button text="Verify" onButtonPressed={incrementStep} size="lg" />
</div> </div>
<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-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"> <div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-gray-400"> <span className="text-gray-400">
Not seeing an email? Not seeing an email?
</span> </span>
<u className={`font-normal text-sm ${isResendingVerificationEmail ? 'text-gray-400' : 'text-sky-500 hover:opacity-90 duration-200'}`}> <u className={`font-normal ${isResendingVerificationEmail ? 'text-gray-400' : 'text-primary-500 hover:opacity-90 duration-200'}`}>
<button disabled={isLoading} onClick={resendVerificationEmail}> <button disabled={isLoading} onClick={resendVerificationEmail}>
{isResendingVerificationEmail ? 'Resending...' : 'Resend'} {isResendingVerificationEmail ? 'Resending...' : 'Resend'}
</button> </button>
@@ -512,17 +515,33 @@ export default function SignUp() {
It contains your Secret Key which we cannot access or recover for you if It contains your Secret Key which we cannot access or recover for you if
you lose it. you lose it.
</div> </div>
<div <div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer" <Button
onClick={() => { text="Download PDF"
if (localStorage.getItem("projectData.id")) { onButtonPressed={async () => {
router.push("/dashboard/" + localStorage.getItem("projectData.id")); await issueBackupKey({
} else { email,
router.push("/noprojects") password,
} personalName: firstName + ' ' + lastName,
}} setBackupKeyError,
> setBackupKeyIssued
Later });
router.push('/dashboard/');
}}
size="lg"
/>
{/* <div
className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer"
onClick={() => {
if (localStorage.getItem("projectData.id")) {
router.push("/dashboard/" + localStorage.getItem("projectData.id"));
} else {
router.push("/noprojects")
}
}}
>
Later
</div> */}
</div> </div>
</div> </div>
); );
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useEffect } from "react"; import { useEffect } from "react";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
const queryString = require("query-string"); const queryString = require("query-string");
+1 -1
View File
@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';