chore(frontend): fixed all eslint errors

This commit is contained in:
akhilmhdh
2023-01-17 20:53:35 +05:30
parent 9f82e2d836
commit cf7834bfc3
184 changed files with 6255 additions and 6117 deletions

View File

@@ -1,49 +0,0 @@
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"next",
"next/core-web-vitals"
],
"parser": "@typescript-eslint/parser",
"plugins": ["simple-import-sort", "@typescript-eslint"],
"rules": {
"react-hooks/exhaustive-deps": "off",
"no-unused-vars": "warn",
"@typescript-eslint/ban-ts-comment": "warn",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"simple-import-sort/exports": "warn",
"simple-import-sort/imports": [
"warn",
{
"groups": [
// Node.js builtins. You could also generate this regex if you use a `.js` config.
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
// Note that if you use the `node:` prefix for Node.js builtins,
// you can avoid this complexity: You can simply use "^node:".
[
"^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)"
],
// Packages `react` related packages
["^react", "^next", "^@?\\w"],
// Internal packages.
["^~(/.*|$)"],
// Relative imports
[
"^\\.\\.(?!/?$)",
"^\\.\\./?$",
"^\\./(?=.*/)(?!/?$)",
"^\\.(?!/?$)",
"^\\./?$"
],
// Style imports.
["^.+\\.?(css|scss)$"]
]
}
]
}
}

72
frontend/.eslintrc.js Normal file
View File

@@ -0,0 +1,72 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true
},
extends: ['airbnb', 'airbnb-typescript', 'airbnb/hooks', 'plugin:react/recommended', 'prettier'],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json',
ecmaFeatures: {
jsx: true
},
tsconfigRootDir: __dirname
},
plugins: ['react', 'prettier', 'simple-import-sort', 'import'],
rules: {
'react/react-in-jsx-scope': 'off',
'import/prefer-default-export': 'off',
'react-hooks/exhaustive-deps': 'off',
'@typescript-eslint/ban-ts-comment': 'warn',
// TODO: This rule will be switched ON after complete revamp of frontend
'@typescript-eslint/no-explicit-any': 'off',
'no-console': 'off',
'arrow-body-style': 'off',
'no-underscore-dangle': ['error', { allow: ['_id'] }],
'jsx-a11y/anchor-is-valid': 'off', // all those <a> tags must be converted to label or a p component
//
'react/require-default-props': 'off',
'react/jsx-filename-extension': [1, { extensions: ['.tsx', '.ts'] }],
// TODO: turn this rule ON after migration. everything should use arrow functions
'react/function-component-definition': [
0,
{
namedComponents: 'arrow-function'
}
],
'react/no-unknown-property': ['error', { ignore: ['jsx'] }],
'@typescript-eslint/no-non-null-assertion': 'off',
'simple-import-sort/exports': 'warn',
'simple-import-sort/imports': [
'warn',
{
groups: [
// Node.js builtins. You could also generate this regex if you use a `.js` config.
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
// Note that if you use the `node:` prefix for Node.js builtins,
// you can avoid this complexity: You can simply use "^node:".
[
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
],
// Packages `react` related packages
['^react', '^next', '^@?\\w'],
// Internal packages.
['^~(/.*|$)'],
// Relative imports
['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// Style imports.
['^.+\\.?(css|scss)$']
]
}
]
},
settings: {
'import/resolver': {
typescript: {
project: ['./tsconfig.json']
}
}
}
};

7
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 2,
"semi": true
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"start:docker": "next build && next start", "start:docker": "next build && next start",
"lint": "next lint", "lint": "eslint --fix --ext js,ts,tsx ./src",
"type-check": "tsc --project tsconfig.json" "type-check": "tsc --project tsconfig.json"
}, },
"dependencies": { "dependencies": {
@@ -62,14 +62,23 @@
"@types/jsrp": "^0.2.4", "@types/jsrp": "^0.2.4",
"@types/node": "18.11.9", "@types/node": "18.11.9",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.45.0", "@typescript-eslint/parser": "^5.45.0",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"eslint": "^8.29.0", "eslint": "^8.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "^13.0.5", "eslint-config-next": "^13.0.5",
"eslint-config-prettier": "^8.6.0",
"eslint-import-resolver-typescript": "^3.5.2", "eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.27.4",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-simple-import-sort": "^8.0.0",
"postcss": "^8.4.14", "postcss": "^8.4.14",
"prettier": "^2.8.3",
"tailwindcss": "^3.1.4", "tailwindcss": "^3.1.4",
"typescript": "^4.9.3" "typescript": "^4.9.3"
} }

View File

@@ -1,8 +1,7 @@
import { ReactNode, useEffect, useState } from 'react'; import { ReactNode, useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { publicPaths } from '@app/const';
import { publicPaths } from '~/const'; import checkAuth from '@app/pages/api/auth/CheckAuth';
import checkAuth from '~/pages/api/auth/CheckAuth';
// #TODO: finish spinner only when the data loads fully // #TODO: finish spinner only when the data loads fully
// #TODO: Redirect somewhere if the page does not exist // #TODO: Redirect somewhere if the page does not exist
@@ -13,8 +12,36 @@ type Prop = {
export default function RouteGuard({ children }: Prop): JSX.Element { export default function RouteGuard({ children }: Prop): JSX.Element {
const router = useRouter(); const router = useRouter();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [authorized, setAuthorized] = useState(false); const [authorized, setAuthorized] = useState(false);
/**
* redirect to login page if accessing a private page and not logged in
*/
async function authCheck(url: string) {
// Make sure that we don't redirect when the user is on the following pages.
const path = `/${url.split('?')[0].split('/')[1]}`;
// Check if the user is authenticated
const response = await checkAuth();
// #TODO: figure our why sometimes it doesn't output a response
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
if (!publicPaths.includes(path)) {
try {
if (response.status !== 200) {
router.push('/login');
console.log('Unauthorized to access.');
setAuthorized(false);
} else {
setAuthorized(true);
console.log('Authorized to access.');
}
} catch (error) {
console.log('Error (probably the authCheck route is stuck again...):', error);
}
}
}
useEffect(() => { useEffect(() => {
// on initial load - run auth check // on initial load - run auth check
(async () => { (async () => {
@@ -40,35 +67,5 @@ export default function RouteGuard({ children }: Prop): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
/**
* redirect to login page if accessing a private page and not logged in
*/
async function authCheck(url: string) {
// Make sure that we don't redirect when the user is on the following pages.
const path = '/' + url.split('?')[0].split('/')[1];
// Check if the user is authenticated
const response = await checkAuth();
// #TODO: figure our why sometimes it doesn't output a response
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
if (!publicPaths.includes(path)) {
try {
if (response.status !== 200) {
router.push('/login');
console.log('Unauthorized to access.');
setAuthorized(false);
} else {
setAuthorized(true);
console.log('Authorized to access.');
}
} catch (error) {
console.log(
'Error (probably the authCheck route is stuck again...):',
error
);
}
}
}
return children as JSX.Element; return children as JSX.Element;
} }

View File

@@ -8,7 +8,7 @@ export const initPostHog = () => {
try { try {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
// @ts-ignore // @ts-ignore
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
posthog.init(POSTHOG_API_KEY, { posthog.init(POSTHOG_API_KEY, {
api_host: POSTHOG_HOST api_host: POSTHOG_HOST
}); });
@@ -19,4 +19,6 @@ export const initPostHog = () => {
} catch (e) { } catch (e) {
console.log("posthog err", e) console.log("posthog err", e)
} }
return undefined;
}; };

View File

@@ -1,16 +1,13 @@
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
export default function Error({ text }: { text: string }): JSX.Element { const Error = ({ text }: { text: string }): JSX.Element => {
return ( return (
<div className='relative flex flex-row justify-center m-auto items-center w-fit rounded-full'> <div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
<FontAwesomeIcon <FontAwesomeIcon icon={faExclamationTriangle} className="text-red mt-1.5 mb-2 mx-2" />
icon={faExclamationTriangle} {text && <p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>}
className='text-red mt-1.5 mb-2 mx-2'
/>
{text && (
<p className='relative top-0 text-red mr-2 text-sm py-1'>{text}</p>
)}
</div> </div>
); );
} };
export default Error;

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React, { Fragment } from 'react';
import { Fragment } from 'react'; import { useTranslation } from 'next-i18next';
import { useTranslation } from "next-i18next";
import { import {
faAngleDown, faAngleDown,
faEye, faEye,
@@ -42,27 +41,20 @@ const eventOptions = [
* @param {string} obj.selected - the event that is currently selected * @param {string} obj.selected - the event that is currently selected
* @param {function} obj.select - an action that happens when an item is selected * @param {function} obj.select - an action that happens when an item is selected
*/ */
export default function EventFilter({ const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
selected,
select
}: ListBoxProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Listbox value={t("activity:event." + selected)} onChange={select}> <Listbox value={t(`activity:event.${selected}`)} onChange={select}>
<div className="relative"> <div className="relative">
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm"> <Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
{selected != '' ? ( {selected !== '' ? (
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p> <p className="select-none text-bunker-100">{t(`activity:event.${selected}`)}</p>
) : ( ) : (
<p className="select-none">{String(t("common:select-event"))}</p> <p className="select-none">{String(t('common:select-event'))}</p>
)} )}
{selected != '' ? ( {selected !== '' ? (
<FontAwesomeIcon <FontAwesomeIcon icon={faX} className="pl-2 w-2 p-2" onClick={() => select('')} />
icon={faX}
className="pl-2 w-2 p-2"
onClick={() => select('')}
/>
) : ( ) : (
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" /> <FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
)} )}
@@ -74,33 +66,29 @@ export default function EventFilter({
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> <Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{eventOptions.map((event, id) => { {eventOptions.map((event, id) => (
return ( <Listbox.Option
<Listbox.Option key={`${event.name}.${id + 1}`}
key={id} className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${ selected === t(`activity:event.${event.name}`) && 'bg-mineshaft-700'
selected == t("activity:event." + event.name) && 'bg-mineshaft-700' }`}
}`} value={event.name}
value={event.name} >
> {({ selected: isSelected }) => (
{({ selected }) => ( <span
<> className={`block truncate ${isSelected ? 'font-semibold' : 'font-normal'}`}
<span >
className={`block truncate ${ <FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
selected ? 'font-semibold' : 'font-normal' {t(`activity:event.${event.name}`)}
}`} </span>
> )}
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '} </Listbox.Option>
{t("activity:event." + event.name)} ))}
</span>
</>
)}
</Listbox.Option>
);
})}
</Listbox.Options> </Listbox.Options>
</Transition> </Transition>
</div> </div>
</Listbox> </Listbox>
); );
} };
export default EventFilter;

View File

@@ -5,13 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId'; import guidGenerator from '../utilities/randomId';
interface InputFieldProps { interface InputFieldProps {
static?: boolean; isStatic?: boolean;
label: string; label: string;
type: string; type: string;
value: string; value: string;
placeholder?: string; placeholder?: string;
isRequired: boolean; isRequired: boolean;
disabled?: boolean;
error?: boolean; error?: boolean;
text?: string; text?: string;
name?: string; name?: string;
@@ -20,46 +19,54 @@ interface InputFieldProps {
onChangeHandler: (value: string) => void; onChangeHandler: (value: string) => void;
} }
const InputField = ( const InputField = ({
props: InputFieldProps & isRequired,
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'> label,
) => { onChangeHandler,
type,
value,
autoComplete,
blurred,
error,
errorText,
id,
name,
placeholder,
isStatic,
text
}: InputFieldProps & Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>) => {
const [passwordVisible, setPasswordVisible] = useState(false); const [passwordVisible, setPasswordVisible] = useState(false);
if (props.static === true) { if (isStatic === true) {
return ( return (
<div className='flex flex-col my-2 md:my-4 justify-center w-full max-w-md'> <div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md">
<p className='text-sm font-semibold text-gray-400 mb-0.5'> <p className="text-sm font-semibold text-gray-400 mb-0.5">{label}</p>
{props.label} {text && <p className="text-xs text-gray-400 mb-2">{text}</p>}
</p>
{props.text && (
<p className='text-xs text-gray-400 mb-2'>{props.text}</p>
)}
<input <input
onChange={(e) => props.onChangeHandler(e.target.value)} onChange={(e) => onChangeHandler(e.target.value)}
type={props.type} type={type}
placeholder={props.placeholder} placeholder={placeholder}
value={props.value} value={value}
required={props.isRequired} required={isRequired}
className='bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none' className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none"
name={props.name} name={name}
readOnly readOnly
autoComplete={props.autoComplete} autoComplete={autoComplete}
id={props.id} id={id}
/> />
</div> </div>
); );
} else { }
return ( return (
<div className='flex-col w-full'> <div className="flex-col w-full">
<div className='flex flex-row text-mineshaft-300 items-center mb-0.5'> <div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
<p className='text-sm font-semibold mr-1'>{props.label}</p> <p className="text-sm font-semibold mr-1">{label}</p>
{/* {props.label == "Password" && router.asPath != "/login" && ( {/* {label === "Password" && router.asPath !== "/login" && (
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200"> <div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
<FontAwesomeIcon <FontAwesomeIcon
icon={faCircleExclamation} icon={faCircleExclamation}
className={`text-sm peer ${ className={`text-sm peer ${
props.error && "text-red" error && "text-red"
}`} }`}
/> />
<span className="absolute hidden peer-hover:block duration-200 w-60 -left-28 -top-2 -translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-200 text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700"> <span className="absolute hidden peer-hover:block duration-200 w-60 -left-28 -top-2 -translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-200 text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
@@ -70,74 +77,69 @@ const InputField = (
</span> </span>
</div> </div>
)} */} )} */}
</div> </div>
<div <div
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${ className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
props.error ? 'border-red' : 'border-mineshaft-500' error ? 'border-red' : 'border-mineshaft-500'
} rounded-md`} } rounded-md`}
> >
<input <input
onChange={(e) => props.onChangeHandler(e.target.value)} onChange={(e) => onChangeHandler(e.target.value)}
type={passwordVisible === false ? props.type : 'text'} type={passwordVisible === false ? type : 'text'}
placeholder={props.placeholder} placeholder={placeholder}
value={props.value} value={value}
required={props.isRequired} required={isRequired}
className={`${ className={`${
props.blurred blurred
? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400' ? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400'
: '' : ''
} ${ } ${
props.error ? 'focus:ring-red/50' : 'focus:ring-primary/50' error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`} } relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
name={props.name} name={name}
spellCheck='false' spellCheck="false"
autoComplete={props.autoComplete} autoComplete={autoComplete}
id={props.id} id={id}
/> />
{props.label?.includes('Password') && ( {label?.includes('Password') && (
<button <button
type='button' type="button"
onClick={() => { onClick={() => {
setPasswordVisible(!passwordVisible); setPasswordVisible(!passwordVisible);
}} }}
className='absolute self-end mr-3 text-gray-400 cursor-pointer' className="absolute self-end mr-3 text-gray-400 cursor-pointer"
> >
{passwordVisible ? ( {passwordVisible ? (
<FontAwesomeIcon icon={faEyeSlash} /> <FontAwesomeIcon icon={faEyeSlash} />
) : ( ) : (
<FontAwesomeIcon icon={faEye} /> <FontAwesomeIcon icon={faEye} />
)} )}
</button> </button>
)} )}
{props.blurred && ( {blurred && (
<div className='peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden'> <div className="peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden">
<p className='ml-2'></p> <p className="ml-2" />
{props.value {value
.split('') .split('')
.slice(0, 54) .slice(0, 54)
.map(() => ( .map(() => (
<FontAwesomeIcon <FontAwesomeIcon
key={guidGenerator()} key={guidGenerator()}
className='text-xxs mx-0.5' className="text-xxs mx-0.5"
icon={faCircle} icon={faCircle}
/> />
))} ))}
</div> </div>
)} )}
{/* {props.error && ( {/* {error && (
<div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end"> <div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end">
<Error /> <Error />
</div> </div>
)} */} )} */}
</div>
{props.error && (
<p className='text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs'>
{props.errorText}
</p>
)}
</div> </div>
); {error && <p className="text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs">{errorText}</p>}
} </div>
);
}; };
export default memo(InputField); export default memo(InputField);

View File

@@ -1,9 +1,20 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unexpected-multiline */ /* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useMemo, useState } from "react"; import crypto from 'crypto';
import Link from "next/link";
import { useRouter } from "next/router"; import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from "next-i18next"; import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import getOrganizationUsers from '@app/pages/api/organization/GetOrgUsers';
import getUser from '@app/pages/api/user/getUser';
import addUserToWorkspace from '@app/pages/api/workspace/addUserToWorkspace';
import createWorkspace from '@app/pages/api/workspace/createWorkspace';
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
import { import {
faBookOpen, faBookOpen,
faFileLines, faFileLines,
@@ -11,56 +22,42 @@ import {
faKey, faKey,
faMobile, faMobile,
faPlug, faPlug,
faUser, faPlus,
} from "@fortawesome/free-solid-svg-icons"; faUser
import { faPlus } 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 getOrganizations from "~/pages/api/organization/getOrgs"; import NavBarDashboard from '../navigation/NavBarDashboard';
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects"; import onboardingCheck from '../utilities/checks/OnboardingCheck';
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers"; import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
import getUser from "~/pages/api/user/getUser"; import { decryptAssymmetric, encryptAssymmetric } from '../utilities/cryptography/crypto';
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace"; import Button from './buttons/Button';
import createWorkspace from "~/pages/api/workspace/createWorkspace"; import AddWorkspaceDialog from './dialog/AddWorkspaceDialog';
import getWorkspaces from "~/pages/api/workspace/getWorkspaces"; import Listbox from './Listbox';
import uploadKeys from "~/pages/api/workspace/uploadKeys";
import NavBarDashboard from "../navigation/NavBarDashboard";
import onboardingCheck from "../utilities/checks/OnboardingCheck";
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
import {
decryptAssymmetric,
encryptAssymmetric,
} from "../utilities/cryptography/crypto";
import Button from "./buttons/Button";
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
import Listbox from "./Listbox";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
} }
const crypto = require("crypto");
export default function Layout({ children }: LayoutProps) { const Layout = ({ children }: LayoutProps) => {
const router = useRouter(); const router = useRouter();
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]); const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
const [workspaceSelected, setWorkspaceSelected] = useState("∞"); const [workspaceSelected, setWorkspaceSelected] = useState('∞');
const [newWorkspaceName, setNewWorkspaceName] = useState(""); const [newWorkspaceName, setNewWorkspaceName] = useState('');
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
useState(0);
const { t } = useTranslation(); const { t } = useTranslation();
function closeModal() { const closeModal = () => {
setIsOpen(false); setIsOpen(false);
} };
function openModal() { const openModal = () => {
setIsOpen(true); setIsOpen(true);
} };
// TODO: what to do about the fact that 2ids can have the same name // TODO: what to do about the fact that 2ids can have the same name
@@ -68,7 +65,7 @@ export default function Layout({ children }: LayoutProps) {
* When a user creates a new workspace, redirect them to the page of the new workspace. * When a user creates a new workspace, redirect them to the page of the new workspace.
* @param {*} workspaceName * @param {*} workspaceName
*/ */
async function submitModal(workspaceName: string, addAllUsers: boolean) { const submitModal = async (workspaceName: string, addAllUsers: boolean) => {
setLoading(true); setLoading(true);
// timeout code. // timeout code.
setTimeout(() => setLoading(false), 1500); setTimeout(() => setLoading(false), 1500);
@@ -79,62 +76,49 @@ export default function Layout({ children }: LayoutProps) {
if (!currentWorkspaces.includes(workspaceName)) { if (!currentWorkspaces.includes(workspaceName)) {
const newWorkspace = await createWorkspace({ const newWorkspace = await createWorkspace({
workspaceName, workspaceName,
organizationId: tempLocalStorage("orgData.id"), organizationId: tempLocalStorage('orgData.id')
}); });
const newWorkspaceId = newWorkspace._id; const newWorkspaceId = newWorkspace._id;
const randomBytes = crypto.randomBytes(16).toString("hex"); const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY")); const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
const myUser = await getUser(); const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({ const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes, plaintext: randomBytes,
publicKey: myUser.publicKey, publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY
}) as { ciphertext: string; nonce: string }; });
await uploadKeys( await uploadKeys(newWorkspaceId, myUser._id, ciphertext, nonce);
newWorkspaceId,
myUser._id,
ciphertext,
nonce
);
if (addAllUsers) { if (addAllUsers) {
console.log('adding other users') console.log('adding other users');
const orgUsers = await getOrganizationUsers({ const orgUsers = await getOrganizationUsers({
orgId: tempLocalStorage("orgData.id"), orgId: tempLocalStorage('orgData.id')
}); });
orgUsers.map(async (user: any) => { orgUsers.map(async (user: any) => {
if (user.status == "accepted" && user.email != myUser.email) { if (user.status === 'accepted' && user.email !== myUser.email) {
const result = await addUserToWorkspace( const result = await addUserToWorkspace(user.user.email, newWorkspaceId);
user.user.email,
newWorkspaceId
);
if (result?.invitee && result?.latestKey) { if (result?.invitee && result?.latestKey) {
const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY"); const TEMP_PRIVATE_KEY = tempLocalStorage('PRIVATE_KEY');
// assymmetrically decrypt symmetric key with local private key // assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({ const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey, ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce, nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey, publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY, privateKey: TEMP_PRIVATE_KEY
}); });
const { ciphertext, nonce } = encryptAssymmetric({ const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey: result.invitee.publicKey, publicKey: result.invitee.publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY
}) as { ciphertext: string; nonce: string }; });
uploadKeys( uploadKeys(newWorkspaceId, result.invitee._id, inviteeCipherText, inviteeNonce);
newWorkspaceId,
result.invitee._id,
ciphertext,
nonce
);
} }
} }
}); });
@@ -142,12 +126,12 @@ export default function Layout({ children }: LayoutProps) {
setWorkspaceMapping((prevState) => ({ setWorkspaceMapping((prevState) => ({
...prevState, ...prevState,
[workspaceName]: newWorkspaceId [workspaceName]: newWorkspaceId
})) }));
setWorkspaceSelected(workspaceName); setWorkspaceSelected(workspaceName);
setIsOpen(false); setIsOpen(false);
setNewWorkspaceName(""); setNewWorkspaceName('');
} else { } else {
console.error("A project with this name already exists."); console.error('A project with this name already exists.');
setError(true); setError(true);
setLoading(false); setLoading(false);
} }
@@ -156,37 +140,35 @@ export default function Layout({ children }: LayoutProps) {
setError(true); setError(true);
setLoading(false); setLoading(false);
} }
} };
const menuItems = useMemo( const menuItems = useMemo(
() => [ () => [
{ {
href: href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
"/dashboard/" + title: t('nav:menu.secrets'),
workspaceMapping[workspaceSelected as any], emoji: <FontAwesomeIcon icon={faKey} />
title: t("nav:menu.secrets"),
emoji: <FontAwesomeIcon icon={faKey} />,
}, },
{ {
href: "/users/" + workspaceMapping[workspaceSelected as any], href: `/users/${workspaceMapping[workspaceSelected as any]}`,
title: t("nav:menu.members"), title: t('nav:menu.members'),
emoji: <FontAwesomeIcon icon={faUser} />, emoji: <FontAwesomeIcon icon={faUser} />
}, },
{ {
href: "/integrations/" + workspaceMapping[workspaceSelected as any], href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
title: t("nav:menu.integrations"), title: t('nav:menu.integrations'),
emoji: <FontAwesomeIcon icon={faPlug} />, emoji: <FontAwesomeIcon icon={faPlug} />
}, },
{ {
href: '/activity/' + workspaceMapping[workspaceSelected as any], href: `/activity/${workspaceMapping[workspaceSelected as any]}`,
title: 'Activity Logs', title: 'Activity Logs',
emoji: <FontAwesomeIcon icon={faFileLines} /> emoji: <FontAwesomeIcon icon={faFileLines} />
}, },
{ {
href: "/settings/project/" + workspaceMapping[workspaceSelected as any], href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
title: t("nav:menu.project-settings"), title: t('nav:menu.project-settings'),
emoji: <FontAwesomeIcon icon={faGear} />, emoji: <FontAwesomeIcon icon={faGear} />
}, }
], ],
[t, workspaceMapping, workspaceSelected] [t, workspaceMapping, workspaceSelected]
); );
@@ -194,61 +176,49 @@ export default function Layout({ children }: LayoutProps) {
useEffect(() => { useEffect(() => {
// Put a user in a workspace if they're not in one yet // Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => { const putUserInWorkSpace = async () => {
if (tempLocalStorage("orgData.id") === "") { if (tempLocalStorage('orgData.id') === '') {
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
localStorage.setItem("orgData.id", userOrgs[0]._id); localStorage.setItem('orgData.id', userOrgs[0]._id);
} }
const orgUserProjects = await getOrganizationUserProjects({ const orgUserProjects = await getOrganizationUserProjects({
orgId: tempLocalStorage("orgData.id"), orgId: tempLocalStorage('orgData.id')
}); });
const userWorkspaces = orgUserProjects; const userWorkspaces = orgUserProjects;
if ( if (
userWorkspaces.length == 0 && userWorkspaces.length === 0 &&
router.asPath != "/noprojects" && router.asPath !== '/noprojects' &&
!router.asPath.includes("home")&& !router.asPath.includes('home') &&
!router.asPath.includes("settings") !router.asPath.includes('settings')
) { ) {
router.push("/noprojects"); router.push('/noprojects');
} else if (router.asPath != "/noprojects") { } else if (router.asPath !== '/noprojects') {
const intendedWorkspaceId = router.asPath const intendedWorkspaceId = router.asPath
.split("/") .split('/')
[router.asPath.split("/").length - 1].split("?")[0]; [router.asPath.split('/').length - 1].split('?')[0];
if ( if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId) localStorage.setItem('projectData.id', intendedWorkspaceId);
) {
localStorage.setItem("projectData.id", intendedWorkspaceId);
} }
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs // If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if ( if (
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId) && !['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
!userWorkspaces !userWorkspaces
.map((workspace: { _id: string }) => workspace._id) .map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId) .includes(intendedWorkspaceId)
) { ) {
router.push("/dashboard/" + userWorkspaces[0]._id); router.push(`/dashboard/${userWorkspaces[0]._id}`);
} else { } else {
setWorkspaceMapping( setWorkspaceMapping(
Object.fromEntries( Object.fromEntries(
userWorkspaces.map((workspace: any) => [ userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
workspace.name,
workspace._id,
])
) as any ) as any
); );
setWorkspaceSelected( setWorkspaceSelected(
Object.fromEntries( Object.fromEntries(
userWorkspaces.map((workspace: any) => [ userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
workspace._id, )[router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]]
workspace.name,
])
)[
router.asPath
.split("/")
[router.asPath.split("/").length - 1].split("?")[0]
]
); );
} }
} }
@@ -262,31 +232,20 @@ export default function Layout({ children }: LayoutProps) {
if ( if (
workspaceMapping[workspaceSelected as any] && workspaceMapping[workspaceSelected as any] &&
`${workspaceMapping[workspaceSelected as any]}` !== `${workspaceMapping[workspaceSelected as any]}` !==
router.asPath router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]
.split("/")
[router.asPath.split("/").length - 1].split("?")[0]
) { ) {
localStorage.setItem( localStorage.setItem('projectData.id', `${workspaceMapping[workspaceSelected as any]}`);
"projectData.id", router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
`${workspaceMapping[workspaceSelected as any]}`
);
router.push(
"/dashboard/" +
workspaceMapping[workspaceSelected as any]
);
} }
} catch (error) { } catch (err) {
console.log(error); console.log(err);
} }
}, [workspaceSelected]); }, [workspaceSelected]);
return ( return (
<> <>
<div className="fixed w-full hidden md:block flex flex-col h-screen"> <div className="fixed w-full md:block flex flex-col h-screen">
<script <script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js" defer />
src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js"
defer
></script>
<NavBarDashboard /> <NavBarDashboard />
<div className="flex flex-col md:flex-row flex-1"> <div className="flex flex-col md:flex-row flex-1">
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen"> <aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
@@ -295,7 +254,7 @@ export default function Layout({ children }: LayoutProps) {
<div> <div>
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4"> <div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide"> <div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
{t("nav:menu.project")} {t('nav:menu.project')}
</div> </div>
{Object.keys(workspaceMapping).length > 0 ? ( {Object.keys(workspaceMapping).length > 0 ? (
<Listbox <Listbox
@@ -319,35 +278,27 @@ export default function Layout({ children }: LayoutProps) {
{Object.keys(workspaceMapping).length > 0 && {Object.keys(workspaceMapping).length > 0 &&
menuItems.map(({ href, title, emoji }) => ( menuItems.map(({ href, title, emoji }) => (
<li className="mt-0.5 mx-2" key={title}> <li className="mt-0.5 mx-2" key={title}>
{router.asPath.split("/")[1] === href.split("/")[1] && {router.asPath.split('/')[1] === href.split('/')[1] &&
(["project", "billing", "org", "personal"].includes( (['project', 'billing', 'org', 'personal'].includes(
router.asPath.split("/")[2] router.asPath.split('/')[2]
) )
? router.asPath.split("/")[2] === href.split("/")[2] ? router.asPath.split('/')[2] === href.split('/')[2]
: true) ? ( : true) ? (
<div <div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`} <div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
>
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg"> <p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
{emoji} {emoji}
</p> </p>
{title} {title}
</div> </div>
) : router.asPath == "/noprojects" ? ( ) : router.asPath === '/noprojects' ? (
<div <div className="flex p-2.5 text-white text-sm rounded">
className={`flex p-2.5 text-white text-sm rounded`} <p className="w-10 flex items-center justify-center text-lg">{emoji}</p>
>
<p className="w-10 flex items-center justify-center text-lg">
{emoji}
</p>
{title} {title}
</div> </div>
) : ( ) : (
<Link href={href}> <Link href={href}>
<div <div className="flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5">
className={`flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5`}
>
<p className="w-10 flex items-center justify-center text-lg"> <p className="w-10 flex items-center justify-center text-lg">
{emoji} {emoji}
</p> </p>
@@ -360,53 +311,47 @@ export default function Layout({ children }: LayoutProps) {
</ul> </ul>
</div> </div>
<div className="w-full mt-40 mb-4 px-2"> <div className="w-full mt-40 mb-4 px-2">
{router.asPath.split("/")[1] === "home" ? ( {router.asPath.split('/')[1] === 'home' ? (
<div <div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`} <div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
>
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg"> <p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} /> <FontAwesomeIcon icon={faBookOpen} />
</p> </p>
Infisical Guide Infisical Guide
<img <img
src={`/images/progress-${ src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
totalOnboardingActionsDone == 0 ? "0" : "" totalOnboardingActionsDone === 1 ? '14' : ''
}${totalOnboardingActionsDone == 1 ? "14" : ""}${ }${totalOnboardingActionsDone === 2 ? '28' : ''}${
totalOnboardingActionsDone == 2 ? "28" : "" totalOnboardingActionsDone === 3 ? '43' : ''
}${totalOnboardingActionsDone == 3 ? "43" : ""}${ }${totalOnboardingActionsDone === 4 ? '57' : ''}${
totalOnboardingActionsDone == 4 ? "57" : "" totalOnboardingActionsDone === 5 ? '71' : ''
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`} }.svg`}
height={58} height={58}
width={58} width={58}
alt="progress bar" alt="progress bar"
className="absolute right-2 -top-2" className="absolute right-2 -top-2"
></img> />
</div> </div>
) : ( ) : (
<Link <Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
href={`/home/` + workspaceMapping[workspaceSelected as any]} <div className="relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max">
>
<div
className={`relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max`}
>
<p className="w-10 flex items-center justify-center text-lg"> <p className="w-10 flex items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} /> <FontAwesomeIcon icon={faBookOpen} />
</p> </p>
Infisical Guide Infisical Guide
<img <img
src={`/images/progress-${ src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
totalOnboardingActionsDone == 0 ? "0" : "" totalOnboardingActionsDone === 1 ? '14' : ''
}${totalOnboardingActionsDone == 1 ? "14" : ""}${ }${totalOnboardingActionsDone === 2 ? '28' : ''}${
totalOnboardingActionsDone == 2 ? "28" : "" totalOnboardingActionsDone === 3 ? '43' : ''
}${totalOnboardingActionsDone == 3 ? "43" : ""}${ }${totalOnboardingActionsDone === 4 ? '57' : ''}${
totalOnboardingActionsDone == 4 ? "57" : "" totalOnboardingActionsDone === 5 ? '71' : ''
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`} }.svg`}
height={58} height={58}
width={58} width={58}
alt="progress bar" alt="progress bar"
className="absolute right-2 -top-2" className="absolute right-2 -top-2"
></img> />
</div> </div>
</Link> </Link>
)} )}
@@ -426,14 +371,13 @@ export default function Layout({ children }: LayoutProps) {
</div> </div>
</div> </div>
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center"> <div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
<FontAwesomeIcon <FontAwesomeIcon icon={faMobile} className="text-gray-300 text-7xl mb-8" />
icon={faMobile}
className="text-gray-300 text-7xl mb-8"
/>
<p className="text-gray-200 px-6 text-center text-lg max-w-sm"> <p className="text-gray-200 px-6 text-center text-lg max-w-sm">
{` ${t("common:no-mobile")} `} {` ${t('common:no-mobile')} `}
</p> </p>
</div> </div>
</> </>
); );
} };
export default Layout;

View File

@@ -1,10 +1,5 @@
import React from 'react'; import React, { Fragment } from 'react';
import { Fragment } from 'react'; import { faAngleDown, faCheck, faPlus } from '@fortawesome/free-solid-svg-icons';
import {
faAngleDown,
faCheck,
faPlus,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Listbox, Transition } from '@headlessui/react'; import { Listbox, Transition } from '@headlessui/react';
@@ -27,72 +22,69 @@ interface ListBoxProps {
* @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button * @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button
* @returns * @returns
*/ */
export default function ListBox({ const ListBox = ({
selected, selected,
onChange, onChange,
data, data,
text, text,
buttonAction, buttonAction,
isFull, isFull
}: ListBoxProps): JSX.Element { }: ListBoxProps): JSX.Element => {
return ( return (
<Listbox value={selected} onChange={onChange}> <Listbox value={selected} onChange={onChange}>
<div className='relative'> <div className="relative">
<Listbox.Button <Listbox.Button
className={`text-gray-400 relative ${ className={`text-gray-400 relative ${
isFull ? 'w-full' : 'w-52' isFull ? 'w-full' : 'w-52'
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`} } cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
> >
<div className='flex flex-row'> <div className="flex flex-row">
{text} {text}
<span className='ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize'> <span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize">
{' '} {' '}
{selected} {selected}
</span> </span>
</div> </div>
{data && ( {data && (
<div className='cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2'> <div className="cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<FontAwesomeIcon icon={faAngleDown} className='text-md mr-1.5' /> <FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
</div> </div>
)} )}
</Listbox.Button> </Listbox.Button>
{data && ( {data && (
<Transition <Transition
as={Fragment} as={Fragment}
leave='transition ease-in duration-100' leave="transition ease-in duration-100"
leaveFrom='opacity-100' leaveFrom="opacity-100"
leaveTo='opacity-0' leaveTo="opacity-0"
> >
<Listbox.Options className='border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm'> <Listbox.Options className="border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
{data.map((person, personIdx) => ( {data.map((person, personIdx) => (
<Listbox.Option <Listbox.Option
key={personIdx} key={`${person}.${personIdx + 1}`}
className={({ active, selected }) => className={({ active, selected: isSelected }) =>
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${ `my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
selected ? 'bg-white/10 text-gray-400 font-bold' : '' isSelected ? 'bg-white/10 text-gray-400 font-bold' : ''
} ${ } ${
active && !selected active && !isSelected
? 'bg-white/5 text-mineshaft-200 cursor-pointer' ? 'bg-white/5 text-mineshaft-200 cursor-pointer'
: 'text-gray-400' : 'text-gray-400'
} ` } `
} }
value={person} value={person}
> >
{({ selected }) => ( {({ selected: isSelected }) => (
<> <>
<span <span
className={`block truncate text-primary${ className={`block truncate text-primary${
selected ? 'font-medium' : 'font-normal' isSelected ? 'font-medium' : 'font-normal'
}`} }`}
> >
{person} {person}
</span> </span>
{selected ? ( {selected ? (
<span className='text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3'> <span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3">
<FontAwesomeIcon <FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
icon={faCheck}
className='text-md ml-1'
/>
</span> </span>
) : null} ) : null}
</> </>
@@ -100,13 +92,10 @@ export default function ListBox({
</Listbox.Option> </Listbox.Option>
))} ))}
{buttonAction && ( {buttonAction && (
<button <button type="button" onClick={buttonAction} className="cursor-pointer w-full">
onClick={buttonAction} <div className="my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2">
className='cursor-pointer w-full' <span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
> <FontAwesomeIcon icon={faPlus} className="text-lg" />
<div className='my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2'>
<span className='rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4'>
<FontAwesomeIcon icon={faPlus} className='text-lg' />
</span> </span>
Add Project Add Project
</div> </div>
@@ -118,4 +107,6 @@ export default function ListBox({
</div> </div>
</Listbox> </Listbox>
); );
} };
export default ListBox;

View File

@@ -16,17 +16,12 @@ interface ToggleProps {
* @param {number} obj.pos - position of a certain secret * @param {number} obj.pos - position of a certain secret
* @returns * @returns
*/ */
export default function Toggle({ const Toggle = ({ enabled, setEnabled, addOverride, pos }: ToggleProps): JSX.Element => {
enabled,
setEnabled,
addOverride,
pos,
}: ToggleProps): JSX.Element {
return ( return (
<Switch <Switch
checked={enabled} checked={enabled}
onChange={() => { onChange={() => {
if (enabled == false) { if (enabled === false) {
addOverride('', pos); addOverride('', pos);
} else { } else {
addOverride(undefined, pos); addOverride(undefined, pos);
@@ -37,7 +32,7 @@ export default function Toggle({
enabled ? 'bg-primary' : 'bg-bunker-400' enabled ? 'bg-primary' : 'bg-bunker-400'
} relative inline-flex h-5 w-9 items-center rounded-full`} } relative inline-flex h-5 w-9 items-center rounded-full`}
> >
<span className='sr-only'>Enable notifications</span> <span className="sr-only">Enable notifications</span>
<span <span
className={`${ className={`${
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5' enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
@@ -45,4 +40,6 @@ export default function Toggle({
/> />
</Switch> </Switch>
); );
} };
export default Toggle;

View File

@@ -1,11 +1,10 @@
import React, { ButtonHTMLAttributes } from "react"; /* eslint-disable react/button-has-type */
import Image from "next/image"; import React, { ButtonHTMLAttributes } from 'react';
import { IconProp } from "@fortawesome/fontawesome-svg-core"; import Image from 'next/image';
import { import { IconProp } from '@fortawesome/fontawesome-svg-core';
FontAwesomeIcon, import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
} from "@fortawesome/react-fontawesome";
const classNames = require("classnames"); const classNames = require('classnames');
type ButtonProps = { type ButtonProps = {
text?: string; text?: string;
@@ -17,7 +16,7 @@ type ButtonProps = {
active?: boolean; active?: boolean;
iconDisabled?: IconProp; iconDisabled?: IconProp;
textDisabled?: string; textDisabled?: string;
type?: ButtonHTMLAttributes<any>['type']; type?: ButtonHTMLAttributes<any>["type"];
}; };
/** /**
@@ -34,72 +33,76 @@ type ButtonProps = {
* @param {string} props.textDisable - text inside the button when it is disabled * @param {string} props.textDisable - text inside the button when it is disabled
* @returns * @returns
*/ */
export default function Button(props: ButtonProps): JSX.Element { const Button = ({
active,
text,
textDisabled,
color,
size,
onButtonPressed,
loading,
icon,
iconDisabled,
type = "button",
}: ButtonProps): JSX.Element => {
// Check if the button show always be 'active' - then true; // Check if the button show always be 'active' - then true;
// or if it should switch between 'active' and 'disabled' - then give the status // or if it should switch between 'active' and 'disabled' - then give the status
const activityStatus = const activityStatus = active || (text !== '' && textDisabled === undefined);
props.active || (props.text != "" && props.textDisabled == undefined);
const styleButton = classNames( const styleButton = classNames(
"group m-auto md:m-0 inline-block rounded-md duration-200", 'group m-auto md:m-0 inline-block rounded-md duration-200',
// Setting background colors and hover modes // Setting background colors and hover modes
props.color == "mineshaft" && color === 'mineshaft' && activityStatus && 'bg-mineshaft-700 hover:bg-primary',
activityStatus && color === 'mineshaft' && !activityStatus && 'bg-mineshaft',
"bg-mineshaft-700 hover:bg-primary", (color === 'primary' || !color) && activityStatus && 'bg-primary hover:opacity-80',
props.color == "mineshaft" && !activityStatus && "bg-mineshaft", (color === 'primary' || !color) && !activityStatus && 'bg-primary',
(props.color == "primary" || !props.color) && color === 'red' && 'bg-red',
activityStatus &&
"bg-primary hover:opacity-80",
(props.color == "primary" || !props.color) &&
!activityStatus &&
"bg-primary",
props.color == "red" && "bg-red",
// Changing the opacity when active vs when not // Changing the opacity when active vs when not
activityStatus ? "opacity-100 cursor-pointer" : "opacity-40", activityStatus ? 'opacity-100 cursor-pointer' : 'opacity-40',
// Setting the button sizes // Setting the button sizes
props.size == "md" && "h-10 w-full px-2 md:px-4", size === 'md' && 'h-10 w-full px-2 md:px-4',
props.size == "lg" && "h-12 w-full px-2 md:px-8", size === 'lg' && 'h-12 w-full px-2 md:px-8',
!props.size && "md:py-1 px-3 md:px-8", !size && 'md:py-1 px-3 md:px-8',
props.size == "icon-md" && "h-10 w-10 flex items-center justify-center", size === 'icon-md' && 'h-10 w-10 flex items-center justify-center',
props.size == "icon-sm" && "h-9 w-9 flex items-center justify-center" size === 'icon-sm' && 'h-9 w-9 flex items-center justify-center'
); );
const styleMainDiv = classNames( const styleMainDiv = classNames(
"relative font-medium flex items-center", 'relative font-medium flex items-center',
// Setting the text color for the text and icon // Setting the text color for the text and icon
props.color == "mineshaft" && "text-gray-400", color === 'mineshaft' && 'text-gray-400',
props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black", color !== 'mineshaft' && color !== 'red' && color !== 'none' && 'text-black',
props.color == "red" && "text-gray-200", color === 'red' && 'text-gray-200',
props.color == "none" && "text-gray-200 text-xl", color === 'none' && 'text-gray-200 text-xl',
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "", activityStatus && color !== 'red' && color !== 'none' ? 'group-hover:text-black' : '',
props.size == "icon" && "flex items-center justify-center" size === 'icon' && 'flex items-center justify-center'
); );
const textStyle = classNames( const textStyle = classNames(
"relative duration-200 text-center w-full", '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", loading ? 'opacity-0' : 'opacity-100',
props.size == "md" && "text-sm", size === 'md' && 'text-sm',
props.size == "lg" && "text-lg" size === 'lg' && 'text-lg'
); );
const button = ( const button = (
<button <button
disabled={!activityStatus} disabled={!activityStatus}
type={props.type} type={type}
onClick={props.onButtonPressed} onClick={onButtonPressed}
className={styleButton} className={styleButton}
> >
<div className={styleMainDiv}> <div className={styleMainDiv}>
<div <div
className={`${ className={`${
props.loading == true ? "opacity-100" : "opacity-0" loading === true ? 'opacity-100' : 'opacity-0'
} absolute flex items-center px-3 bg-primary duration-200 w-full`} } absolute flex items-center px-3 bg-primary duration-200 w-full`}
> >
<Image <Image
@@ -107,33 +110,33 @@ export default function Button(props: ButtonProps): JSX.Element {
height={25} height={25}
width={42} width={42}
alt="loading animation" alt="loading animation"
className={`rounded-xl`} className="rounded-xl"
></Image> />
</div> </div>
{props.icon && ( {icon && (
<FontAwesomeIcon <FontAwesomeIcon
icon={props.icon} icon={icon}
className={`flex my-auto font-extrabold ${ className={`flex my-auto font-extrabold ${size === 'icon-sm' ? 'text-sm' : 'text-sm'} ${
props.size == "icon-sm" ? "text-sm" : "text-sm" (text || textDisabled) && 'mr-2'
} ${(props.text || props.textDisabled) && "mr-2"}`} }`}
/> />
)} )}
{props.iconDisabled && ( {iconDisabled && (
<FontAwesomeIcon <FontAwesomeIcon
icon={props.iconDisabled as IconProp} icon={iconDisabled as IconProp}
className={`flex my-auto font-extrabold ${ className={`flex my-auto font-extrabold ${size === 'icon-sm' ? 'text-sm' : 'text-md'} ${
props.size == "icon-sm" ? "text-sm" : "text-md" (text || textDisabled) && 'mr-2'
} ${(props.text || props.textDisabled) && "mr-2"}`} }`}
/> />
)} )}
{(props.text || props.textDisabled) && ( {(text || textDisabled) && (
<p className={textStyle}> <p className={textStyle}>{activityStatus ? text : textDisabled}</p>
{activityStatus ? props.text : props.textDisabled}
</p>
)} )}
</div> </div>
</button> </button>
); );
return button; return button;
} };
export default Button;

View File

@@ -17,7 +17,7 @@ const ActivateBotDialog = ({
closeModal, closeModal,
selectedIntegrationOption, selectedIntegrationOption,
handleBotActivate, handleBotActivate,
handleIntegrationOption, handleIntegrationOption
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -30,7 +30,7 @@ const ActivateBotDialog = ({
if (!selectedIntegrationOption) return; if (!selectedIntegrationOption) return;
// 2. start integration // 2. start integration
await handleIntegrationOption({ await handleIntegrationOption({
integrationOption: selectedIntegrationOption, integrationOption: selectedIntegrationOption
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@@ -42,47 +42,44 @@ const ActivateBotDialog = ({
return ( return (
<div> <div>
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative z-10' onClose={closeModal}> <Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter='ease-out duration-300' enter="ease-out duration-300"
enterFrom='opacity-0' enterFrom="opacity-0"
enterTo='opacity-100' enterTo="opacity-100"
leave='ease-in duration-200' leave="ease-in duration-200"
leaveFrom='opacity-100' leaveFrom="opacity-100"
leaveTo='opacity-0' leaveTo="opacity-0"
> >
<div className='fixed inset-0 bg-black bg-opacity-70' /> <div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child> </Transition.Child>
<div className='fixed inset-0 overflow-y-auto'> <div className="fixed inset-0 overflow-y-auto">
<div className='flex min-h-full items-center justify-center p-4 text-center'> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter='ease-out duration-300' enter="ease-out duration-300"
enterFrom='opacity-0 scale-95' enterFrom="opacity-0 scale-95"
enterTo='opacity-100 scale-100' enterTo="opacity-100 scale-100"
leave='ease-in duration-200' leave="ease-in duration-200"
leaveFrom='opacity-100 scale-100' leaveFrom="opacity-100 scale-100"
leaveTo='opacity-0 scale-95' leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
as='h3'
className='text-lg font-medium leading-6 text-gray-400'
>
{t('integrations:grant-access-to-secrets')} {t('integrations:grant-access-to-secrets')}
</Dialog.Title> </Dialog.Title>
<div className='mt-2 mb-2'> <div className="mt-2 mb-2">
<p className='text-sm text-gray-500'> <p className="text-sm text-gray-500">
{t('integrations:why-infisical-needs-access')} {t('integrations:why-infisical-needs-access')}
</p> </p>
</div> </div>
<div className='mt-6 max-w-max'> <div className="mt-6 max-w-max">
<Button <Button
onButtonPressed={submit} onButtonPressed={submit}
color='mineshaft' color="mineshaft"
text={t('integrations:grant-access-button') as string} text={t('integrations:grant-access-button') as string}
size='md' size="md"
/> />
</div> </div>
</Dialog.Panel> </Dialog.Panel>

View File

@@ -1,11 +1,10 @@
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import addAPIKey from '@app/pages/api/apiKey/addAPIKey';
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import addAPIKey from '~/pages/api/apiKey/addAPIKey';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
import InputField from '../InputField'; import InputField from '../InputField';
import ListBox from '../Listbox'; import ListBox from '../Listbox';
@@ -99,7 +98,7 @@ const AddApiKeyDialog = ({
leaveFrom='opacity-100 scale-100' leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95' leaveTo='opacity-0 scale-95'
> >
{apiKey == '' ? ( {apiKey === '' ? (
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'> <Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title <Dialog.Title
as='h3' as='h3'
@@ -135,7 +134,7 @@ const AddApiKeyDialog = ({
'6 months', '6 months',
'12 months', '12 months',
]} ]}
isFull={true} isFull
text={`${t('common:expired-in')}: `} text={`${t('common:expired-in')}: `}
/> />
</div> </div>
@@ -149,7 +148,7 @@ const AddApiKeyDialog = ({
t('section-api-key:add-dialog.add') as string t('section-api-key:add-dialog.add') as string
} }
size='md' size='md'
active={apiKeyName == '' ? false : true} active={apiKeyName !== ''}
/> />
</div> </div>
</div> </div>
@@ -176,15 +175,16 @@ const AddApiKeyDialog = ({
<input <input
type='text' type='text'
value={apiKey} value={apiKey}
disabled={true} disabled
id='apiKey' id='apiKey'
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none' className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
></input> />
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'> <div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
{apiKey} {apiKey}
</div> </div>
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'> <div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button <button
type="button"
onClick={copyToClipboard} onClick={copyToClipboard}
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200' className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
> >

View File

@@ -1,16 +1,14 @@
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import addIncidentContact from '@app/pages/api/organization/addIncidentContact';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import addIncidentContact from '~/pages/api/organization/addIncidentContact';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
import InputField from '../InputField'; import InputField from '../InputField';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
closeModal: () => void; closeModal: () => void;
workspaceId: string;
incidentContacts: string[]; incidentContacts: string[];
setIncidentContacts: (arg: string[]) => void; setIncidentContacts: (arg: string[]) => void;
}; };

View File

@@ -27,122 +27,105 @@ const AddProjectMemberDialog = ({
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className='z-50'> <div className="z-50">
<Transition appear show={isOpen} as={Fragment}> <Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative' onClose={closeModal}> <Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter='ease-out duration-300' enter="ease-out duration-300"
enterFrom='opacity-0' enterFrom="opacity-0"
enterTo='opacity-100' enterTo="opacity-100"
leave='ease-in duration-200' leave="ease-in duration-200"
leaveFrom='opacity-100' leaveFrom="opacity-100"
leaveTo='opacity-0' leaveTo="opacity-0"
> >
<div className='fixed inset-0 bg-black bg-opacity-70' /> <div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child> </Transition.Child>
<div className='fixed inset-0 overflow-y-auto'> <div className="fixed inset-0 overflow-y-auto">
<div className='flex min-h-full items-center justify-center p-4 text-center'> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter='ease-out duration-300' enter="ease-out duration-300"
enterFrom='opacity-0 scale-95' enterFrom="opacity-0 scale-95"
enterTo='opacity-100 scale-100' enterTo="opacity-100 scale-100"
leave='ease-in duration-200' leave="ease-in duration-200"
leaveFrom='opacity-100 scale-100' leaveFrom="opacity-100 scale-100"
leaveTo='opacity-0 scale-95' leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'> <Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
{data?.length > 0 ? ( {data?.length > 0 ? (
<Dialog.Title <Dialog.Title
as='h3' as="h3"
className='text-lg font-medium leading-6 text-gray-400 z-50' className="text-lg font-medium leading-6 text-gray-400 z-50"
> >
{t('section-members:add-dialog.add-member-to-project')} {t('section-members:add-dialog.add-member-to-project')}
</Dialog.Title> </Dialog.Title>
) : ( ) : (
<Dialog.Title <Dialog.Title
as='h3' as="h3"
className='text-lg font-medium leading-6 text-gray-400 z-50' className="text-lg font-medium leading-6 text-gray-400 z-50"
> >
{t('section-members:add-dialog.already-all-invited')} {t('section-members:add-dialog.already-all-invited')}
</Dialog.Title> </Dialog.Title>
)} )}
<div className='mt-2 mb-4'> <div className="mt-2 mb-4">
{data?.length > 0 ? ( {data?.length > 0 ? (
<div className='flex flex-col'> <div className="flex flex-col">
<p className='text-sm text-gray-500'> <p className="text-sm text-gray-500">
{t('section-members:add-dialog.user-will-email')} {t('section-members:add-dialog.user-will-email')}
</p> </p>
<div className=''> <div className="">
<Trans <Trans
i18nKey='section-members:add-dialog.looking-add' i18nKey="section-members:add-dialog.looking-add"
components={[ components={[
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
<button <button
type='button' type="button"
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2' className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() => onClick={() => router.push(`/settings/org/${router.query.id}`)}
router.push( aria-label="add member"
'/settings/org/' + router.query.id
)
}
/>, />,
// eslint-disable-next-line react/jsx-key // eslint-disable-next-line react/jsx-key
<button <button
type='button' type="button"
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2' className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() => onClick={() =>
router.push( router.push(`/settings/org/${router.query.id}?invite`)
'/settings/org/' +
router.query.id +
'?invite'
)
} }
/>, aria-label="add member"
/>
]} ]}
/> />
</div> </div>
</div> </div>
) : ( ) : (
<p className='text-sm text-gray-500'> <p className="text-sm text-gray-500">
{t('section-members:add-dialog.add-user-org-first')} {t('section-members:add-dialog.add-user-org-first')}
</p> </p>
)} )}
</div> </div>
<div className='max-h-28'> <div className="max-h-28">
{data?.length > 0 && ( {data?.length > 0 && (
<ListBox <ListBox selected={email || data[0]} onChange={setEmail} data={data} isFull />
selected={email ? email : data[0]}
onChange={setEmail}
data={data}
isFull={true}
/>
)} )}
</div> </div>
<div className='max-w-max'> <div className="max-w-max">
{data?.length > 0 ? ( {data?.length > 0 ? (
<div className='mt-6 flex flex-col justify-start w-max'> <div className="mt-6 flex flex-col justify-start w-max">
<Button <Button
onButtonPressed={submitModal} onButtonPressed={submitModal}
color='mineshaft' color="mineshaft"
text={t('section-members:add-member') as string} text={t('section-members:add-member') as string}
size='md' size="md"
/> />
</div> </div>
) : ( ) : (
<Button <Button
onButtonPressed={() => onButtonPressed={() => router.push(`/settings/org/${router.query.id}`)}
router.push('/settings/org/' + router.query.id) color="mineshaft"
} text={t('section-members:add-dialog.add-user-to-org') as string}
color='mineshaft' size="md"
text={
t(
'section-members:add-dialog.add-user-to-org'
) as string
}
size='md'
/> />
)} )}
</div> </div>

View File

@@ -2,13 +2,12 @@ import crypto from 'crypto';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import addServiceToken from '@app/pages/api/serviceToken/addServiceToken';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Dialog, Transition } from '@headlessui/react'; import { Dialog, Transition } from '@headlessui/react';
import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptSymmetric, encryptSymmetric,
@@ -86,7 +85,7 @@ const AddServiceTokenDialog = ({
}); });
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData])); setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
setServiceToken(newServiceToken.serviceToken + '.' + randomBytes); setServiceToken(`${newServiceToken.serviceToken }.${ randomBytes}`);
}; };
function copyToClipboard() { function copyToClipboard() {
@@ -141,7 +140,7 @@ const AddServiceTokenDialog = ({
leaveFrom='opacity-100 scale-100' leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95' leaveTo='opacity-0 scale-95'
> >
{serviceToken == '' ? ( {serviceToken === '' ? (
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'> <Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
<Dialog.Title <Dialog.Title
as='h3' as='h3'
@@ -186,7 +185,7 @@ const AddServiceTokenDialog = ({
} }
) )
} }
isFull={true} isFull
text={`${t('common:environment')}: `} text={`${t('common:environment')}: `}
/> />
</div> </div>
@@ -201,7 +200,7 @@ const AddServiceTokenDialog = ({
'6 months', '6 months',
'12 months', '12 months',
]} ]}
isFull={true} isFull
text={`${t('common:expired-in')}: `} text={`${t('common:expired-in')}: `}
/> />
</div> </div>
@@ -215,7 +214,7 @@ const AddServiceTokenDialog = ({
t('section-token:add-dialog.add') as string t('section-token:add-dialog.add') as string
} }
size='md' size='md'
active={serviceTokenName == '' ? false : true} active={serviceTokenName !== ''}
/> />
</div> </div>
</div> </div>
@@ -244,13 +243,14 @@ const AddServiceTokenDialog = ({
value={serviceToken} value={serviceToken}
id='serviceToken' id='serviceToken'
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none' className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
></input> />
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'> <div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
{serviceToken} {serviceToken}
</div> </div>
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'> <div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button <button
onClick={copyToClipboard} onClick={copyToClipboard}
type="button"
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200' className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
> >
{serviceTokenCopied ? ( {serviceTokenCopied ? (

View File

@@ -10,7 +10,6 @@ type Props = {
isOpen: boolean; isOpen: boolean;
closeModal: () => void; closeModal: () => void;
submitModal: (email: string) => void; submitModal: (email: string) => void;
workspaceId: string;
email: string; email: string;
setEmail: (email: string) => void; setEmail: (email: string) => void;
currentPlan: string; currentPlan: string;
@@ -82,13 +81,13 @@ const AddUserDialog = ({
isRequired isRequired
/> />
</div> </div>
{currentPlan == STRIPE_PRODUCT_STARTER && ( {currentPlan === STRIPE_PRODUCT_STARTER && (
<div className='flex flex-row'> <div className='flex flex-row'>
<button <button
type='button' type='button'
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2' className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={() => onClick={() =>
router.push('/settings/billing/' + router.query.id) router.push(`/settings/billing/${ router.query.id}`)
} }
> >
You can add up to 5 members on a Free tier. You can add up to 5 members on a Free tier.
@@ -97,7 +96,7 @@ const AddUserDialog = ({
type='button' type='button'
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2' className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
onClick={() => onClick={() =>
router.push('/settings/billing/' + router.query.id) router.push(`/settings/billing/${ router.query.id}`)
} }
> >
Upgrade now. Upgrade now.

View File

@@ -26,7 +26,14 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} /> <div
className="fixed inset-0 bg-black bg-opacity-70"
onClick={onClose}
onKeyDown={onClose}
role="button"
tabIndex={0}
aria-label="Close"
/>
</Transition.Child> </Transition.Child>
<div className="flex min-h-full items-center justify-center p-4 text-center"> <div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child <Transition.Child
@@ -39,10 +46,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
leaveTo="opacity-0 scale-95" leaveTo="opacity-0 scale-95"
> >
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title <Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
{t('dashboard:sidebar.delete-key-dialog.title')} {t('dashboard:sidebar.delete-key-dialog.title')}
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">

View File

@@ -6,7 +6,6 @@ import InputField from "../InputField";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
closeModal: () => void; closeModal: () => void;
submitModal: (userIdToBeDeleted: string) => void;
selectedIntegrationOption: string; selectedIntegrationOption: string;
handleBotActivate: () => void; handleBotActivate: () => void;
handleIntegrationOption: (arg:{integrationOption:string})=>void; handleIntegrationOption: (arg:{integrationOption:string})=>void;
@@ -20,6 +19,7 @@ const IntegrationAccessTokenDialog = ({
handleIntegrationOption handleIntegrationOption
}:Props) => { }:Props) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const submit = async () => { const submit = async () => {
try { try {
// 1. activate bot // 1. activate bot
@@ -85,7 +85,7 @@ const IntegrationAccessTokenDialog = ({
label="Access token" label="Access token"
onChangeHandler={() => {}} onChangeHandler={() => {}}
type="varName" type="varName"
value={"Hello"} value="Hello"
placeholder="" placeholder=""
isRequired isRequired
/> />

View File

@@ -23,50 +23,47 @@ interface PopupProps {
* @param {string} org.setCheckDocsPopUpVisible - the functions that closes the popup * @param {string} org.setCheckDocsPopUpVisible - the functions that closes the popup
* @returns * @returns
*/ */
export default function BottonRightPopup({ const BottonRightPopup = ({
buttonText, buttonText,
buttonLink, buttonLink,
titleText, titleText,
emoji, emoji,
textLine1, textLine1,
textLine2, textLine2,
setCheckDocsPopUpVisible, setCheckDocsPopUpVisible
}: PopupProps): JSX.Element { }: PopupProps): JSX.Element => {
return ( return (
<div <div
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-md 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-md 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 className='ml-2.5'>{emoji}</div> <div className="ml-2.5">{emoji}</div>
</div> </div>
<button <button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
className='mt-1'
onClick={() => setCheckDocsPopUpVisible(false)}
>
<FontAwesomeIcon <FontAwesomeIcon
icon={faXmark} icon={faXmark}
className='text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer' className="text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer"
/> />
</button> </button>
</div> </div>
<div className='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}</div>
{textLine1} <div className="block sm:inline mb-4 px-6">{textLine2}</div>
</div> <div className="flex flex-row px-6 w-full">
<div className='block sm:inline mb-4 px-6'>{textLine2}</div> {/* eslint-disable-next-line react/jsx-no-target-blank */}
<div className='flex flex-row px-6 w-full'>
{/*eslint-disable-next-line react/jsx-no-target-blank */}
<a <a
className='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"
> >
{buttonText} {buttonText}
</a> </a>
</div> </div>
</div> </div>
); );
} };
export default BottonRightPopup;

View File

@@ -1,8 +1,7 @@
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import deleteAPIKey from '../../../pages/api/apiKey/deleteAPIKey';
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
import guidGenerator from '../../utilities/randomId'; import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
@@ -28,49 +27,47 @@ const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
return ( return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div> <div className="absolute rounded-t-md w-full h-12 bg-white/5" />
<table className="w-full my-1"> <table className="w-full my-1">
<thead className="text-bunker-300 text-sm font-light"> <thead className="text-bunker-300 text-sm font-light">
<tr> <tr>
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th> <th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
<th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th> <th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
<th></th> <th aria-label="button" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.length > 0 ? ( {data?.length > 0 ? (
data?.map((row) => { data?.map((row) => (
return ( <tr
<tr key={guidGenerator()}
key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100" >
> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {row.name}
{row.name} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {new Date(row.expiresAt).toUTCString()}
{new Date(row.expiresAt).toUTCString()} </td>
</td> <td className="py-2 border-mineshaft-700 border-t">
<td className="py-2 border-mineshaft-700 border-t"> <div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center"> <Button
<Button onButtonPressed={() => {
onButtonPressed={() => { deleteAPIKey({ apiKeyId: row._id });
deleteAPIKey({ apiKeyId: row._id} ); setApiKeys(data.filter((token) => token._id !== row._id));
setApiKeys(data.filter(token => token._id != row._id)); createNotification({
createNotification({ text: `'${row.name}' API key has been revoked.`,
text: `'${row.name}' API key has been revoked.`, type: 'error'
type: 'error' });
}); }}
}} color="red"
color="red" size="icon-sm"
size="icon-sm" icon={faX}
icon={faX} />
/> </div>
</div> </td>
</td> </tr>
</tr> ))
);
})
) : ( ) : (
<tr> <tr>
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm"> <td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">

View File

@@ -1,31 +1,32 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
type Props = { type Props = {
addAllUsers: boolean; addAllUsers: boolean;
setAddAllUsers: (arg: boolean) => void; setAddAllUsers: (arg: boolean) => void;
}; };
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => { export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
return ( <div className="flex flex-row items-center">
<> {addAllUsers === true ? (
<div className='flex flex-row items-center'> <input
{addAllUsers == true ? ( type="checkbox"
<input className="accent-primary h-4 w-4"
type='checkbox' checked
className='accent-primary h-4 w-4' readOnly
checked onClick={() => setAddAllUsers(!addAllUsers)}
readOnly />
onClick={() => setAddAllUsers(!addAllUsers)} ) : (
/> <div
) : ( onKeyDown={() => setAddAllUsers(!addAllUsers)}
<div role="button"
className='h-4 w-4 bg-bunker border border-gray-600 rounded-sm' tabIndex={0}
onClick={() => setAddAllUsers(!addAllUsers)} aria-label="add all users"
></div> className="h-4 w-4 bg-bunker border border-gray-600 rounded-sm"
)} onClick={() => setAddAllUsers(!addAllUsers)}
/>
)}
<label className='ml-2 text-gray-500 text-sm'> <label className="ml-2 text-gray-500 text-sm">
Add all members of my organization to this project. Add all members of my organization to this project.
</label> </label>
</div> </div>
</> );
);
};

View File

@@ -14,15 +14,10 @@ type Props = {
onDeleteEnv: (slug: string) => Promise<void>; onDeleteEnv: (slug: string) => Promise<void>;
}; };
const EnvironmentTable = ({ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: Props) => {
data = [],
onCreateEnv,
onDeleteEnv,
onUpdateEnv,
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'createUpdateEnv', 'createUpdateEnv',
'deleteEnv', 'deleteEnv'
] as const); ] as const);
const onEnvCreateCB = async (env: Env) => { const onEnvCreateCB = async (env: Env) => {
@@ -36,10 +31,7 @@ const EnvironmentTable = ({
const onEnvUpdateCB = async (env: Env) => { const onEnvUpdateCB = async (env: Env) => {
try { try {
await onUpdateEnv( await onUpdateEnv((popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug, env);
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
env
);
handlePopUpClose('createUpdateEnv'); handlePopUpClose('createUpdateEnv');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -48,9 +40,7 @@ const EnvironmentTable = ({
const onEnvDeleteCB = async () => { const onEnvDeleteCB = async () => {
try { try {
await onDeleteEnv( await onDeleteEnv((popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug);
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
);
handlePopUpClose('deleteEnv'); handlePopUpClose('deleteEnv');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -59,83 +49,68 @@ const EnvironmentTable = ({
return ( return (
<> <>
<div className='flex flex-row justify-between w-full'> <div className="flex flex-row justify-between w-full">
<div className='flex flex-col w-full'> <div className="flex flex-col w-full">
<p className='text-xl font-semibold mb-3'>Project Environments</p> <p className="text-xl font-semibold mb-3">Project Environments</p>
<p className='text-base text-gray-400 mb-4'> <p className="text-base text-gray-400 mb-4">
Choose which environments will show up in your dashboard like Choose which environments will show up in your dashboard like development, staging,
development, staging, production production
</p> </p>
<p className='text-sm mr-1 text-gray-500 self-start'> <p className="text-sm mr-1 text-gray-500 self-start">
Note: the text in slugs shows how these environmant should be Note: the text in slugs shows how these environmant should be accessed in CLI.
accessed in CLI.
</p> </p>
</div> </div>
<div className='w-48'> <div className="w-48">
<Button <Button
text='Add New Env' text="Add New Env"
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')} onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
color='mineshaft' color="mineshaft"
icon={faPlus} icon={faPlus}
size='md' size="md"
/> />
</div> </div>
</div> </div>
<div className='table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1'> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className='absolute rounded-t-md w-full h-12 bg-white/5'></div> <div className="absolute rounded-t-md w-full h-12 bg-white/5" />
<table className='w-full my-1'> <table className="w-full my-1">
<thead className='text-bunker-300'> <thead className="text-bunker-300">
<tr> <tr>
<th className='text-left pl-6 pt-2.5 pb-2'>Name</th> <th className="text-left pl-6 pt-2.5 pb-2">Name</th>
<th className='text-left pl-6 pt-2.5 pb-2'>Slug</th> <th className="text-left pl-6 pt-2.5 pb-2">Slug</th>
<th></th> <th aria-label="buttons" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.length > 0 ? ( {data?.length > 0 ? (
data.map(({ name, slug }) => { data.map(({ name, slug }) => (
return ( <tr key={name} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100">
<tr <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize">
key={name} {name}
className='bg-bunker-800 hover:bg-bunker-800/5 duration-100' </td>
> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">{slug}</td>
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize'> <td className="py-2 border-mineshaft-700 border-t flex">
{name} <div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-8">
</td> <Button
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300'> onButtonPressed={() => handlePopUpOpen('createUpdateEnv', { name, slug })}
{slug} color="red"
</td> size="icon-sm"
<td className='py-2 border-mineshaft-700 border-t flex'> icon={faPencil}
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'> />
<Button </div>
onButtonPressed={() => <div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
handlePopUpOpen('createUpdateEnv', { name, slug }) <Button
} onButtonPressed={() => handlePopUpOpen('deleteEnv', { name, slug })}
color='red' color="red"
size='icon-sm' size="icon-sm"
icon={faPencil} icon={faX}
/> />
</div> </div>
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center'> </td>
<Button </tr>
onButtonPressed={() => ))
handlePopUpOpen('deleteEnv', { name, slug })
}
color='red'
size='icon-sm'
icon={faX}
/>
</div>
</td>
</tr>
);
})
) : ( ) : (
<tr> <tr>
<td <td colSpan={4} className="text-center pt-7 pb-4 text-bunker-400">
colSpan={4}
className='text-center pt-7 pb-4 text-bunker-400'
>
No environmants found No environmants found
</td> </td>
</tr> </tr>
@@ -143,7 +118,7 @@ const EnvironmentTable = ({
</tbody> </tbody>
</table> </table>
<DeleteActionModal <DeleteActionModal
isOpen={popUp['deleteEnv'].isOpen} isOpen={popUp.deleteEnv.isOpen}
title={`Are you sure want to delete ${ title={`Are you sure want to delete ${
(popUp?.deleteEnv?.data as { name: string })?.name || ' ' (popUp?.deleteEnv?.data as { name: string })?.name || ' '
}?`} }?`}

View File

@@ -1,8 +1,7 @@
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; import deleteServiceToken from '../../../pages/api/serviceToken/deleteServiceToken';
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
import guidGenerator from '../../utilities/randomId'; import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
@@ -33,7 +32,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
return ( return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div> <div className="absolute rounded-t-md w-full h-12 bg-white/5" />
<table className="w-full my-1"> <table className="w-full my-1">
<thead className="text-bunker-300 text-sm font-light"> <thead className="text-bunker-300 text-sm font-light">
<tr> <tr>
@@ -41,49 +40,47 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
<th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th> <th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th> <th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th> <th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
<th></th> <th aria-label="button" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.length > 0 ? ( {data?.length > 0 ? (
data?.map((row) => { data?.map((row) => (
return ( <tr
<tr key={guidGenerator()}
key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100" >
> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {row.name}
{row.name} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {workspaceName}
{workspaceName} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {row.environment}
{row.environment} </td>
</td> <td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> {new Date(row.expiresAt).toUTCString()}
{new Date(row.expiresAt).toUTCString()} </td>
</td> <td className="py-2 border-mineshaft-700 border-t">
<td className="py-2 border-mineshaft-700 border-t"> <div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center"> <Button
<Button onButtonPressed={() => {
onButtonPressed={() => { deleteServiceToken({ serviceTokenId: row._id });
deleteServiceToken({ serviceTokenId: row._id} ); setServiceTokens(data.filter((token) => token._id !== row._id));
setServiceTokens(data.filter(token => token._id != row._id)); createNotification({
createNotification({ text: `'${row.name}' token has been revoked.`,
text: `'${row.name}' token has been revoked.`, type: 'error'
type: 'error' });
}); }}
}} color="red"
color="red" size="icon-sm"
size="icon-sm" icon={faX}
icon={faX} />
/> </div>
</div> </td>
</td> </tr>
</tr> ))
);
})
) : ( ) : (
<tr> <tr>
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm"> <td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">

View File

@@ -1,241 +0,0 @@
import { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/router';
import { faX } from '@fortawesome/free-solid-svg-icons';
import deleteUserFromOrganization from '~/pages/api/organization/deleteUserFromOrganization';
import changeUserRoleInWorkspace from '~/pages/api/workspace/changeUserRoleInWorkspace';
import deleteUserFromWorkspace from '~/pages/api/workspace/deleteUserFromWorkspace';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import uploadKeys from '~/pages/api/workspace/uploadKeys';
import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button';
import Listbox from '../Listbox';
const {
decryptAssymmetric,
encryptAssymmetric
} = require('../../utilities/cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const roles = ['admin', 'user'];
/**
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too.
* #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {*} props
* @returns
*/
const UserTable = ({
userData,
changeData,
myUser,
filter,
resendInvite,
isOrg,
onClick,
deleteUser,
setUserIdToBeDeleted
}) => {
const [roleSelected, setRoleSelected] = useState(
Array(userData?.length).fill(userData.map((user) => user.role))
);
const router = useRouter();
const [myRole, setMyRole] = useState('member');
// Delete the row in the table (e.g. a user)
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
const handleDelete = (membershipId, index, e) => {
// setUserIdToBeDeleted(userId);
// onClick();
if (isOrg) {
deleteUserFromOrganization(membershipId);
} else {
deleteUserFromWorkspace(membershipId);
}
changeData(userData.filter((v, i) => i !== index));
setRoleSelected([
...roleSelected.slice(0, index),
...roleSelected.slice(index + 1, userData?.length)
]);
};
// Update the rold of a certain user
const handleRoleUpdate = (index, e) => {
changeUserRoleInWorkspace(userData[index].membershipId, e);
changeData([
...userData.slice(0, index),
...[
{
key: userData[index].key,
firstName: userData[index].firstName,
lastName: userData[index].lastName,
email: userData[index].email,
role: e,
status: userData[index].status,
userId: userData[index].userId,
membershipId: userData[index].membershipId,
publicKey: userData[index].publicKey
}
],
...userData.slice(index + 1, userData?.length)
]);
};
useEffect(() => {
setMyRole(userData.filter((user) => user.email == myUser)[0]?.role);
}, [userData, myUser]);
const grantAccess = async (id, publicKey) => {
let result = await getLatestFileKey({ workspaceId: router.query.id });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey: publicKey,
privateKey: PRIVATE_KEY
});
uploadKeys(router.query.id, id, ciphertext, nonce);
router.reload();
};
const deleteMembershipAndResendInvite = (email, membershipId) => {
// deleteUserFromWorkspace(membershipId);
resendInvite(email);
};
return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5"></div>
<table className="w-full my-0.5">
<thead className="text-gray-400 text-sm font-light">
<tr>
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
<th className="text-left pl-6 py-3.5">LAST NAME</th>
<th className="text-left pl-6 py-3.5">EMAIL</th>
<th></th>
</tr>
</thead>
<tbody>
{userData?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
).length > 0 &&
userData
?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
)
.map((row, index) => {
return (
<tr
key={guidGenerator()}
className="bg-bunker-800 hover:bg-bunker-800/5"
>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.firstName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.lastName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.email}
</td>
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
<div className="flex justify-end mr-6 w-3/4 mx-2 w-full h-full flex flex-row items-center">
{row.status == 'granted' &&
((myRole == 'admin' && row.role != 'owner') ||
myRole == 'owner') &&
myUser !== row.email ? (
<Listbox
selected={row.role}
onChange={(e) => handleRoleUpdate(index, e)}
data={
myRole == 'owner'
? ['owner', 'admin', 'member']
: ['admin', 'member']
}
text="Role: "
membershipId={row.membershipId}
/>
) : (
row.status != 'invited' &&
row.status != 'verified' && (
<Listbox
selected={row.role}
text="Role: "
membershipId={row.membershipId}
/>
)
)}
{(row.status == 'invited' ||
row.status == 'verified') && (
<div className="w-full pl-9">
<Button
onButtonPressed={() =>
deleteMembershipAndResendInvite(
row.email,
row.membershipId
)
}
color="mineshaft"
text="Resend Invite"
size="md"
/>
</div>
)}
{row.status == 'completed' && myUser !== row.email && (
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
<Button
onButtonPressed={() =>
grantAccess(row.userId, row.publicKey)
}
color="mineshaft"
text="Grant Access"
size="md"
/>
</div>
)}
</div>
{myUser !== row.email &&
// row.role != "admin" &&
myRole != 'member' ? (
<div className="opacity-50 hover:opacity-100 flex items-center">
<Button
onButtonPressed={(e) =>
handleDelete(row.membershipId, index, e)
}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
) : (
<div className="w-9 h-9"></div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default UserTable;

View File

@@ -0,0 +1,218 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace';
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { decryptAssymmetric, encryptAssymmetric } from '../../utilities/cryptography/crypto';
import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button';
import Listbox from '../Listbox';
// const roles = ['admin', 'user'];
// TODO: Set type for this
type Props = {
userData: any[];
changeData: (users: any[]) => void;
myUser: string;
filter: string;
resendInvite: (email: string) => void;
isOrg: boolean;
};
/**
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too.
* #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {*} props
* @returns
*/
const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }: Props) => {
const [roleSelected, setRoleSelected] = useState(
Array(userData?.length).fill(userData.map((user) => user.role))
);
const router = useRouter();
const [myRole, setMyRole] = useState('member');
const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user)
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
const handleDelete = (membershipId: string, index: number) => {
// setUserIdToBeDeleted(userId);
// onClick();
if (isOrg) {
deleteUserFromOrganization(membershipId);
} else {
deleteUserFromWorkspace(membershipId);
}
changeData(userData.filter((v, i) => i !== index));
setRoleSelected([
...roleSelected.slice(0, index),
...roleSelected.slice(index + 1, userData?.length)
]);
};
// Update the rold of a certain user
const handleRoleUpdate = (index: number, e: string) => {
changeUserRoleInWorkspace(userData[index].membershipId, e);
changeData([
...userData.slice(0, index),
...[
{
key: userData[index].key,
firstName: userData[index].firstName,
lastName: userData[index].lastName,
email: userData[index].email,
role: e,
status: userData[index].status,
userId: userData[index].userId,
membershipId: userData[index].membershipId,
publicKey: userData[index].publicKey
}
],
...userData.slice(index + 1, userData?.length)
]);
};
useEffect(() => {
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
}, [userData, myUser]);
const grantAccess = async (id: string, publicKey: string) => {
const result = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
uploadKeys(workspaceId, id, ciphertext, nonce);
router.reload();
};
const deleteMembershipAndResendInvite = (email: string) => {
// deleteUserFromWorkspace(membershipId);
resendInvite(email);
};
return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
<table className="w-full my-0.5">
<thead className="text-gray-400 text-sm font-light">
<tr>
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
<th className="text-left pl-6 py-3.5">LAST NAME</th>
<th className="text-left pl-6 py-3.5">EMAIL</th>
<th aria-label="buttons" />
</tr>
</thead>
<tbody>
{userData?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
).length > 0 &&
userData
?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
)
.map((row, index) => (
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5">
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.firstName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.lastName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.email}
</td>
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
<div className="justify-end mr-6 mx-2 w-full h-full flex flex-row items-center">
{row.status === 'granted' &&
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
myUser !== row.email ? (
<Listbox
selected={row.role}
onChange={(e) => handleRoleUpdate(index, e)}
data={
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
}
text="Role: "
/>
) : (
row.status !== 'invited' &&
row.status !== 'verified' && (
<Listbox
selected={row.role}
text="Role: "
onChange={() => {
throw new Error('Function not implemented.');
}}
data={null}
/>
)
)}
{(row.status === 'invited' || row.status === 'verified') && (
<div className="w-full pl-9">
<Button
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
color="mineshaft"
text="Resend Invite"
size="md"
/>
</div>
)}
{row.status === 'completed' && myUser !== row.email && (
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
<Button
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
color="mineshaft"
text="Grant Access"
size="md"
/>
</div>
)}
</div>
{myUser !== row.email &&
// row.role !== "admin" &&
myRole !== 'member' ? (
<div className="opacity-50 hover:opacity-100 flex items-center">
<Button
onButtonPressed={() => handleDelete(row.membershipId, index)}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
) : (
<div className="w-9 h-9" />
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default UserTable;

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import StripeRedirect from '@app/pages/api/organization/StripeRedirect';
import StripeRedirect from '~/pages/api/organization/StripeRedirect';
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage'; import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
@@ -21,54 +20,43 @@ export default function Plan({ plan }: Props) {
return ( return (
<div <div
className={`relative flex flex-col justify-between border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${ className={`relative flex flex-col justify-between border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
plan.name != 'Starter' && plan.current == true plan.name !== 'Starter' && plan.current === true ? 'border-primary' : 'border-chicago-700'
? 'border-primary'
: 'border-chicago-700'
} }
`} `}
> >
<div className='flex flex-col'> <div className="flex flex-col">
<div className='flex flex-row justify-between items-center relative z-10'> <div className="flex flex-row justify-between items-center relative z-10">
<p className={`px-6 py-4 text-3xl font-semibold text-gray-400`}> <p className="px-6 py-4 text-3xl font-semibold text-gray-400">{plan.name}</p>
{plan.name}
</p>
</div> </div>
<div className='flex flwx-row items-end justify-start mb-4'> <div className="flex flwx-row items-end justify-start mb-4">
<p className='pl-6 text-3xl font-semibold text-primary'> <p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
{plan.price} <p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
</p>
<p className='pl-3 mb-1 text-lg text-gray-400'>
{plan.priceExplanation}
</p>
</div> </div>
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'> <p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
{plan.text} <p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
</p>
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'>
{plan.subtext}
</p>
</div> </div>
<div className='flex flex-row items-center'> <div className="flex flex-row items-center">
{plan.current == false ? ( {plan.current === false ? (
<> <>
{plan.buttonTextMain == 'Schedule a Demo' ? ( {plan.buttonTextMain === 'Schedule a Demo' ? (
<a href='/scheduledemo' target='_blank rel="noopener"'> <a href="/scheduledemo" target='_blank rel="noopener"'>
<div className='relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max'> <div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
{plan.buttonTextMain} {plan.buttonTextMain}
</div> </div>
</a> </a>
) : ( ) : (
<div <div
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${ className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${
plan.buttonTextMain == 'Downgrade' plan.buttonTextMain === 'Downgrade'
? 'hover:bg-red hover:text-white hover:border-red' ? 'hover:bg-red hover:text-white hover:border-red'
: 'hover:bg-primary hover:text-black hover:border-primary' : 'hover:bg-primary hover:text-black hover:border-primary'
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`} } bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
> >
<button <button
type="button"
onClick={() => onClick={() =>
StripeRedirect({ StripeRedirect({
orgId: tempLocalStorage('orgData.id'), orgId: tempLocalStorage('orgData.id')
}) })
} }
> >
@@ -76,11 +64,8 @@ export default function Plan({ plan }: Props) {
</button> </button>
</div> </div>
)} )}
<a <a href="https://infisical.com/pricing" target='_blank rel="noopener"'>
href='https://infisical.com/pricing' <div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
target='_blank rel="noopener"'
>
<div className='relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5'>
{plan.buttonTextSecondary} {plan.buttonTextSecondary}
</div> </div>
</a> </a>
@@ -88,12 +73,10 @@ export default function Plan({ plan }: Props) {
) : ( ) : (
<div <div
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${ className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
plan.name != 'Starter' && plan.current == true plan.name !== 'Starter' && plan.current === true ? 'bg-primary' : 'bg-chicago-700'
? 'bg-primary'
: 'bg-chicago-700'
}`} }`}
> >
<p className='text-xs text-black font-semibold'>CURRENT PLAN</p> <p className="text-xs text-black font-semibold">CURRENT PLAN</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -2,26 +2,26 @@ import { useEffect, useRef } from 'react';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Notification as NotificationType } from './NotificationProvider'; type NotificationType = 'success' | 'error' | 'info';
export type TNotification = {
text: string;
type?: NotificationType;
timeoutMs?: number;
};
interface NotificationProps { interface NotificationProps {
notification: Required<NotificationType>; notification: Required<TNotification>;
clearNotification: (text: string) => void; clearNotification: (text: string) => void;
} }
const Notification = ({ const Notification = ({ notification, clearNotification }: NotificationProps) => {
notification,
clearNotification
}: NotificationProps) => {
const timeout = useRef<number>(); const timeout = useRef<number>();
const handleClearNotification = () => clearNotification(notification.text); const handleClearNotification = () => clearNotification(notification.text);
const setNotifTimeout = () => { const setNotifTimeout = () => {
timeout.current = window.setTimeout( timeout.current = window.setTimeout(handleClearNotification, notification.timeoutMs);
handleClearNotification,
notification.timeoutMs
);
}; };
const cancelNotifTimeout = () => { const cancelNotifTimeout = () => {
@@ -40,25 +40,21 @@ const Notification = ({
role="alert" role="alert"
> >
{notification.type === 'error' && ( {notification.type === 'error' && (
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md"></div> <div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md" />
)} )}
{notification.type === 'success' && ( {notification.type === 'success' && (
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md"></div> <div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md" />
)} )}
{notification.type === 'info' && ( {notification.type === 'info' && (
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md"></div> <div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md" />
)} )}
<p className="text-bunker-200 text-sm font-semibold mt-0.5"> <p className="text-bunker-200 text-sm font-semibold mt-0.5">{notification.text}</p>
{notification.text}
</p>
<button <button
type="button"
className="rounded-lg" className="rounded-lg"
onClick={() => clearNotification(notification.text)} onClick={() => clearNotification(notification.text)}
> >
<FontAwesomeIcon <FontAwesomeIcon className="text-white pl-2 w-4 h-3 hover:text-red" icon={faX} />
className="text-white pl-2 w-4 h-3 hover:text-red"
icon={faX}
/>
</button> </button>
</div> </div>
); );

View File

@@ -1,17 +1,10 @@
import { createContext, ReactNode, useContext, useState } from 'react'; import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';
import { TNotification } from './Notification';
import Notifications from './Notifications'; import Notifications from './Notifications';
type NotificationType = 'success' | 'error' | 'info';
export type Notification = {
text: string;
type?: NotificationType;
timeoutMs?: number;
};
type NotificationContextState = { type NotificationContextState = {
createNotification: (newNotification: Notification) => void; createNotification: (newNotification: TNotification) => void;
}; };
const NotificationContext = createContext<NotificationContextState>({ const NotificationContext = createContext<NotificationContextState>({
@@ -24,43 +17,33 @@ interface NotificationProviderProps {
children: ReactNode; children: ReactNode;
} }
// TODO: Migration to radix toast
const NotificationProvider = ({ children }: NotificationProviderProps) => { const NotificationProvider = ({ children }: NotificationProviderProps) => {
const [notifications, setNotifications] = useState<Required<Notification>[]>( const [notifications, setNotifications] = useState<Required<TNotification>[]>([]);
[]
const clearNotification = (text: string) =>
setNotifications((state) => state.filter((notif) => notif.text !== text));
const createNotification = useCallback(
({ text, type = 'success', timeoutMs = 4000 }: TNotification) => {
const doesNotifExist = notifications.some((notif) => notif.text === text);
if (doesNotifExist) {
return;
}
const newNotification: Required<TNotification> = { text, type, timeoutMs };
setNotifications((state) => [...state, newNotification]);
},
[notifications]
); );
const clearNotification = (text: string) => { const value = useMemo(() => ({ createNotification }), [createNotification]);
return setNotifications((state) =>
state.filter((notif) => notif.text !== text)
);
};
const createNotification = ({
text,
type = 'success',
timeoutMs = 4000
}: Notification) => {
const doesNotifExist = notifications.some((notif) => notif.text === text);
if (doesNotifExist) {
return;
}
const newNotification: Required<Notification> = { text, type, timeoutMs };
return setNotifications((state) => [...state, newNotification]);
};
return ( return (
<NotificationContext.Provider <NotificationContext.Provider value={value}>
value={{ <Notifications notifications={notifications} clearNotification={clearNotification} />
createNotification
}}
>
<Notifications
notifications={notifications}
clearNotification={clearNotification}
/>
{children} {children}
</NotificationContext.Provider> </NotificationContext.Provider>
); );

View File

@@ -1,27 +1,19 @@
import Notification from './Notification'; import Notification, { TNotification } from './Notification';
import { Notification as NotificationType } from './NotificationProvider';
interface NoticationsProps { interface NoticationsProps {
notifications: Required<NotificationType>[]; notifications: Required<TNotification>[];
clearNotification: (text: string) => void; clearNotification: (text: string) => void;
} }
const Notifications = ({ const Notifications = ({ notifications, clearNotification }: NoticationsProps) => {
notifications,
clearNotification
}: NoticationsProps) => {
if (!notifications.length) { if (!notifications.length) {
return null; return null;
} }
return ( return (
<div className="hidden fixed z-50 md:flex md:flex-col-reverse bottom-1 gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none"> <div className="hidden fixed z-50 md:flex md:flex-col-reverse gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
{notifications.map((notif) => ( {notifications.map((notif) => (
<Notification <Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
key={notif.text}
notification={notif}
clearNotification={clearNotification}
/>
))} ))}
</div> </div>
); );

View File

@@ -1,20 +1,30 @@
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
/** /**
* This is the text field where people can add comments to particular secrets. * This is the text field where people can add comments to particular secrets.
*/ */
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => { const CommentField = ({
comment,
modifyComment,
position
}: {
comment: string;
modifyComment: (value: string, posistion: number) => void;
position: number;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return <div className={`relative mt-4 px-4 pt-6`}> return (
<p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p> <div className="relative mt-4 px-4 pt-6">
<textarea <p className="text-sm text-bunker-300 pl-0.5">{t('dashboard:sidebar.comments')}</p>
className="bg-bunker-800 placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70" <textarea
value={comment} className="placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
onChange={(e) => modifyComment(e.target.value, position)} value={comment}
placeholder="Leave any comments here..." onChange={(e) => modifyComment(e.target.value, position)}
/> placeholder="Leave any comments here..."
</div> />
} </div>
);
};
export default CommentField; export default CommentField;

View File

@@ -40,14 +40,14 @@ const DashboardInputField = ({
}: DashboardInputFieldProps) => { }: DashboardInputFieldProps) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => { const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
if (ref.current == null) return; if (ref.current === null) return;
ref.current.scrollTop = e.currentTarget.scrollTop; ref.current.scrollTop = e.currentTarget.scrollTop;
ref.current.scrollLeft = e.currentTarget.scrollLeft; ref.current.scrollLeft = e.currentTarget.scrollLeft;
}; };
if (type === 'varName') { if (type === 'varName') {
const startsWithNumber = !isNaN(Number(value?.charAt(0))) && value != ''; const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== '';
const error = startsWithNumber || isDuplicate; const error = startsWithNumber || isDuplicate;
return ( return (
@@ -58,9 +58,7 @@ const DashboardInputField = ({
} rounded-md`} } rounded-md`}
> >
<input <input
onChange={(e) => onChange={(e) => onChangeHandler(e.target.value.toUpperCase(), position)}
onChangeHandler(e.target.value.toUpperCase(), position)
}
type={type} type={type}
value={value} value={value}
className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${ className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${
@@ -81,13 +79,16 @@ const DashboardInputField = ({
)} )}
</div> </div>
); );
} else if (type === 'value') { }
if (type === 'value') {
return ( return (
<div className="flex-col w-full"> <div className="flex-col w-full">
<div <div className="group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md">
className={`group relative whitespace-pre flex flex-col justify-center w-full 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 == 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>} Override enabled
</div>
)}
<input <input
value={value} value={value}
onChange={(e) => onChangeHandler(e.target.value, position)} onChange={(e) => onChangeHandler(e.target.value, position)}
@@ -105,20 +106,18 @@ const DashboardInputField = ({
blurred && !override 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'
: '' : ''
} ${ } ${override ? 'text-primary-300' : 'text-gray-400'}
override ? 'text-primary-300' : 'text-gray-400'
}
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture 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`} absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture 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) {
return ( return (
<span className="ph-no-capture text-yellow" key={id}> <span className="ph-no-capture text-yellow" key={`${word}.${id + 1}`}>
{word.slice(0, 2)} {word.slice(0, 2)}
<span className="ph-no-capture text-yellow-200/80"> <span className="ph-no-capture text-yellow-200/80">
{word.slice(2, word.length - 1)} {word.slice(2, word.length - 1)}
</span> </span>
{word.slice(word.length - 1, word.length) == '}' ? ( {word.slice(word.length - 1, word.length) === '}' ? (
<span className="ph-no-capture text-yellow"> <span className="ph-no-capture text-yellow">
{word.slice(word.length - 1, word.length)} {word.slice(word.length - 1, word.length)}
</span> </span>
@@ -129,13 +128,12 @@ const DashboardInputField = ({
)} )}
</span> </span>
); );
} else {
return (
<span key={id} className="ph-no-capture">
{word}
</span>
);
} }
return (
<span key={`${word}_${id + 1}`} className="ph-no-capture">
{word}
</span>
);
})} })}
</div> </div>
{blurred && ( {blurred && (
@@ -160,7 +158,14 @@ const DashboardInputField = ({
}; };
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) { 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; 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); export default memo(DashboardInputField, inputPropsAreEqual);

View File

@@ -8,7 +8,7 @@ type Props = {
onSubmit: () => void onSubmit: () => void
} }
export function DeleteActionButton({ onSubmit }: Props) { export const DeleteActionButton = ({ onSubmit }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)

View File

@@ -1,5 +1,4 @@
import { Fragment } from 'react'; import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import { faDownload } from '@fortawesome/free-solid-svg-icons'; import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { Menu, Transition } from '@headlessui/react'; import { Menu, Transition } from '@headlessui/react';
import { SecretDataProps } from 'public/data/frequentInterfaces'; import { SecretDataProps } from 'public/data/frequentInterfaces';
@@ -10,57 +9,49 @@ import downloadYaml from '../utilities/secrets/downloadYaml';
/** /**
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options) * 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 * @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod * @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
* @param {string} obj.env - the environment which we're downloading (used for naming the file) * @param {string} obj.env - the environment which we're downloading (used for naming the file)
*/ */
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string; }) => { const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string }) => {
const { t } = useTranslation(); return (
<Menu as="div" className="relative inline-block text-left">
return <Menu <Menu.Button
as="div" as="div"
className="relative inline-block text-left" className="inline-flex w-full justify-center 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"
> >
<Menu.Button <Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
as="div" </Menu.Button>
className="inline-flex w-full justify-center 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" <Transition
> as={Fragment}
<Button enter="transition ease-out duration-100"
color="mineshaft" enterFrom="transform opacity-0 scale-95"
size="icon-md" enterTo="transform opacity-100 scale-100"
icon={faDownload} leave="transition ease-in duration-75"
onButtonPressed={() => {}} leaveFrom="transform opacity-100 scale-100"
/> leaveTo="transform opacity-0 scale-95"
</Menu.Button> >
<Transition <Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
as={Fragment} <Menu.Item>
enter="transition ease-out duration-100" <Button
enterFrom="transform opacity-0 scale-95" color="mineshaft"
enterTo="transform opacity-100 scale-100" onButtonPressed={() => downloadDotEnv({ data, env })}
leave="transition ease-in duration-75" size="md"
leaveFrom="transform opacity-100 scale-100" text="Download as .env"
leaveTo="transform opacity-0 scale-95" />
> </Menu.Item>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2"> <Menu.Item>
<Menu.Item> <Button
<Button color="mineshaft"
color="mineshaft" onButtonPressed={() => downloadYaml({ data, env })}
onButtonPressed={() => downloadDotEnv({ data, env })} size="md"
size="md" text="Download as .yml"
text="Download as .env" />
/> </Menu.Item>
</Menu.Item> </Menu.Items>
<Menu.Item> </Transition>
<Button </Menu>
color="mineshaft" );
onButtonPressed={() => downloadYaml({ data, env })} };
size="md"
text="Download as .yml"
/>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
}
export default DownloadSecretMenu; export default DownloadSecretMenu;

View File

@@ -1,14 +1,15 @@
import { type ChangeEvent, type DragEvent, useState } from "react"; /* eslint-disable no-nested-ternary */
import Image from "next/image"; import { type ChangeEvent, type DragEvent, useState } from 'react';
import { useTranslation } from "next-i18next"; import Image from 'next/image';
import { faUpload } from "@fortawesome/free-solid-svg-icons"; import { useTranslation } from 'next-i18next';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faUpload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { parseDocument, Scalar, YAMLMap } from 'yaml'; import { parseDocument, Scalar, YAMLMap } from 'yaml';
import Button from "../basic/buttons/Button"; import Button from '../basic/buttons/Button';
import Error from "../basic/Error"; import Error from '../basic/Error';
import { parseDotEnv } from '../utilities/parseDotEnv'; import { parseDotEnv } from '../utilities/parseDotEnv';
import guidGenerator from "../utilities/randomId"; import guidGenerator from '../utilities/randomId';
interface DropZoneProps { interface DropZoneProps {
// TODO: change Data type from any // TODO: change Data type from any
@@ -28,7 +29,7 @@ const DropZone = ({
errorDragAndDrop, errorDragAndDrop,
setButtonReady, setButtonReady,
keysExist, keysExist,
numCurrentRows, numCurrentRows
}: DropZoneProps) => { }: DropZoneProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -47,7 +48,7 @@ const DropZone = ({
e.stopPropagation(); e.stopPropagation();
// set dropEffect to copy i.e copy of the source item // set dropEffect to copy i.e copy of the source item
e.dataTransfer.dropEffect = "copy"; e.dataTransfer.dropEffect = 'copy';
}; };
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -57,16 +58,14 @@ const DropZone = ({
switch (fileType) { switch (fileType) {
case 'env': { case 'env': {
const keyPairs = parseDotEnv(file); const keyPairs = parseDotEnv(file);
secrets = Object.keys(keyPairs).map((key, index) => { secrets = Object.keys(keyPairs).map((key, index) => ({
return { id: guidGenerator(),
id: guidGenerator(), pos: numCurrentRows + index,
pos: numCurrentRows + index, key,
key: key, value: keyPairs[key as keyof typeof keyPairs].value,
value: keyPairs[key as keyof typeof keyPairs].value, comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'), type: 'shared'
type: 'shared', }));
};
});
break; break;
} }
case 'yml': { case 'yml': {
@@ -79,15 +78,15 @@ const DropZone = ({
fileContent!.items fileContent!.items
.find((item) => item.key.value === key) .find((item) => item.key.value === key)
?.key?.commentBefore?.split('\n') ?.key?.commentBefore?.split('\n')
.map((comment) => comment.trim()) .map((cmnt) => cmnt.trim())
.join('\n') ?? ''; .join('\n') ?? '';
return { return {
id: guidGenerator(), id: guidGenerator(),
pos: numCurrentRows + index, pos: numCurrentRows + index,
key: key, key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '', value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment, comment,
type: 'shared', type: 'shared'
}; };
}); });
break; break;
@@ -105,7 +104,7 @@ const DropZone = ({
setTimeout(() => setLoading(false), 5000); setTimeout(() => setLoading(false), 5000);
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.dataTransfer.dropEffect = "copy"; e.dataTransfer.dropEffect = 'copy';
const file = e.dataTransfer.files[0]; const file = e.dataTransfer.files[0];
const reader = new FileReader(); const reader = new FileReader();
@@ -149,16 +148,11 @@ const DropZone = ({
return loading ? ( return loading ? (
<div className="flex items-center justify-center pt-16 mb-16"> <div className="flex items-center justify-center pt-16 mb-16">
<Image <Image src="/images/loading/loading.gif" height={70} width={120} alt="google logo" />
src="/images/loading/loading.gif"
height={70}
width={120}
alt="google logo"
></Image>
</div> </div>
) : keysExist ? ( ) : keysExist ? (
<div <div
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2" className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
@@ -171,36 +165,27 @@ const DropZone = ({
accept=".txt,.env,.yml" accept=".txt,.env,.yml"
onChange={handleFileSelect} onChange={handleFileSelect}
/> />
{errorDragAndDrop ? ( {errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
<div className="my-3 max-w-xl opacity-80"></div>
) : (
<div className=""></div>
)}
<div className="flex flex-row"> <div className="flex flex-row">
<FontAwesomeIcon <FontAwesomeIcon icon={faUpload} className="text-bunker-300 text-3xl mr-6" />
icon={faUpload} <p className="text-bunker-300 mt-1">{t('common:drop-zone-keys')}</p>
className="text-bunker-300 text-3xl mr-6"
/>
<p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
</div> </div>
{errorDragAndDrop ? ( {errorDragAndDrop && (
<div className="mt-8 max-w-xl opacity-80"> <div className="mt-8 max-w-xl opacity-80">
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" /> <Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
</div> </div>
) : (
<></>
)} )}
</div> </div>
) : ( ) : (
<div <div
className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker outline max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4" className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4"
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
> >
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" /> <FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
<p className="">{t("common:drop-zone")}</p> <p className="">{t('common:drop-zone')}</p>
<input <input
id="fileSelect" id="fileSelect"
type="file" type="file"
@@ -209,14 +194,14 @@ const DropZone = ({
onChange={handleFileSelect} onChange={handleFileSelect}
/> />
<div className="flex flex-row w-full items-center justify-center mb-6 mt-5"> <div className="flex flex-row w-full items-center justify-center mb-6 mt-5">
<div className="border-t border-gray-700 w-1/5"></div> <div className="border-t border-gray-700 w-1/5" />
<p className="text-gray-400 text-xs mx-4">OR</p> <p className="text-gray-400 text-xs mx-4">OR</p>
<div className="border-t border-gray-700 w-1/5"></div> <div className="border-t border-gray-700 w-1/5" />
</div> </div>
<div className="z-10 mb-6"> <div className="z-10 mb-6">
<Button <Button
color="mineshaft" color="mineshaft"
text={String(t("dashboard:add-secret"))} text={String(t('dashboard:add-secret'))}
onButtonPressed={createNewFile} onButtonPressed={createNewFile}
size="md" size="md"
/> />

View File

@@ -1,95 +1,106 @@
import { Fragment,useState } from 'react'; import { Fragment, useState } from 'react';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import { faShuffle } from '@fortawesome/free-solid-svg-icons'; import { faShuffle } 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 { 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) * 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 * @returns the popup-menu for randomly generating secrets
*/ */
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => { const GenerateSecretMenu = ({
modifyValue,
position
}: {
modifyValue: (value: string, position: number) => void;
position: number;
}) => {
const [randomStringLength, setRandomStringLength] = useState(32); const [randomStringLength, setRandomStringLength] = useState(32);
const { t } = useTranslation(); const { t } = useTranslation();
return <Menu as="div" className="relative inline-block text-left"> return (
<div> <Menu as="div" className="relative inline-block text-left">
<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>
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'> <Menu.Button className="inline-flex w-full justify-center 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">
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/> <div className="py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500">
</div> <FontAwesomeIcon icon={faShuffle} className="text-bunker-300" />
</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>{t("dashboard:sidebar.generate-random-hex")}</p>
<p>{t("dashboard:sidebar.digits")}</p>
</div> </div>
</div> </Menu.Button>
<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>
<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 <div
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700" onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => { onClick={() => {
if (randomStringLength > 1) { if (randomStringLength > 32) {
setRandomStringLength(randomStringLength - 1); 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>{t('dashboard:sidebar.generate-random-hex')}</p>
<p>{t('dashboard:sidebar.digits')}</p>
</div>
</div> </div>
<input <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 ">
onChange={(e) => <div
setRandomStringLength(parseInt(e.target.value)) onKeyDown={() => null}
} role="button"
value={randomStringLength} tabIndex={0}
className="text-center z-20 peer text-sm bg-transparent w-full outline-none" className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
spellCheck="false" onClick={() => {
/> if (randomStringLength > 1) {
<div setRandomStringLength(randomStringLength - 1);
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>
}} <input
> onChange={(e) => setRandomStringLength(parseInt(e.target.value, 10))}
+ value={randomStringLength}
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
spellCheck="false"
/>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
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> </div>
</div> </Menu.Items>
</Menu.Items> </Transition>
</Transition> </Menu>
</Menu> );
} };
export default GenerateSecretMenu; export default GenerateSecretMenu;

View File

@@ -1,4 +1,3 @@
import React from 'react';
import { faEllipsis } from '@fortawesome/free-solid-svg-icons'; import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces'; import { SecretDataProps } from 'public/data/frequentInterfaces';
@@ -7,9 +6,9 @@ import DashboardInputField from './DashboardInputField';
interface KeyPairProps { interface KeyPairProps {
keyPair: SecretDataProps; keyPair: SecretDataProps;
modifyKey: (value: string, position: number) => void; modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string, position: number) => void; modifyValueOverride: (value: string, position: number) => void;
isBlurred: boolean; isBlurred: boolean;
isDuplicate: boolean; isDuplicate: boolean;
toggleSidebar: (id: string) => void; toggleSidebar: (id: string) => void;
@@ -28,7 +27,7 @@ interface KeyPairProps {
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard * @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar * @param {function} obj.toggleSidebar - open/close/switch sidebar
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed * @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar * @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
* @returns * @returns
*/ */
const KeyPair = ({ const KeyPair = ({
@@ -41,48 +40,61 @@ const KeyPair = ({
toggleSidebar, toggleSidebar,
sidebarSecretId, sidebarSecretId,
isSnapshot isSnapshot
}: KeyPairProps) => { }: KeyPairProps) => (
return ( <div
<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`}> className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && 'pointer-events-none'} ${
<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.id === sidebarSecretId && 'bg-mineshaft-500 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"> } rounded-md`}
<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"> <div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
This secret is overriden {keyPair.valueOverride && (
</span> <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>} <div className="w-1 h-1 rounded-full bg-primary z-40" />
<div className="min-w-xl w-96"> <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">
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16"> This secret is overriden
<DashboardInputField </span>
onChangeHandler={modifyKey}
type="varName"
position={keyPair.pos}
value={keyPair.key}
isDuplicate={isDuplicate}
/>
</div>
</div> </div>
<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`}> <div className="min-w-xl w-96">
<DashboardInputField <div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue} <DashboardInputField
type="value" onChangeHandler={modifyKey}
position={keyPair.pos} type="varName"
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value} position={keyPair.pos}
blurred={isBlurred} value={keyPair.key}
override={Boolean(keyPair.valueOverride)} isDuplicate={isDuplicate}
/>
</div>
</div>
{!isSnapshot && <div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] 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>
</div> </div>
<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={keyPair.valueOverride ? modifyValueOverride : modifyValue}
type="value"
position={keyPair.pos}
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred}
override={Boolean(keyPair.valueOverride)}
/>
</div>
</div>
{!isSnapshot && (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => toggleSidebar(keyPair.id)}
className="cursor-pointer w-[2.35rem] h-[2.35rem] 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> </div>
); </div>
}; );
export default KeyPair; export default KeyPair;

View File

@@ -1,6 +1,7 @@
/* eslint-disable react/no-unused-prop-types */
import { useState } from 'react'; import { useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import SecretVersionList from '@app/ee/components/SecretVersionList'; import SecretVersionList from '@app/ee/components/SecretVersionList';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -12,7 +13,6 @@ import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton'; import { DeleteActionButton } from './DeleteActionButton';
import GenerateSecretMenu from './GenerateSecretMenu'; import GenerateSecretMenu from './GenerateSecretMenu';
interface SecretProps { interface SecretProps {
key: string; key: string;
value: string; value: string;
@@ -22,22 +22,18 @@ interface SecretProps {
comment: string; comment: string;
} }
interface OverrideProps { export interface DeleteRowFunctionProps {
id: string; ids: string[];
valueOverride: string; secretName: string;
}
export interface DeleteRowFunctionProps {
ids: string[];
secretName: string;
} }
interface SideBarProps { interface SideBarProps {
toggleSidebar: (value: string) => void; toggleSidebar: (value: string) => void;
data: SecretProps[]; data: SecretProps[];
modifyKey: (value: string, position: number) => void; modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void; modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void; modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void; modifyComment: (value: string, position: number) => void;
buttonReady: boolean; buttonReady: boolean;
savePush: () => void; savePush: () => void;
sharedToHide: string[]; sharedToHide: string[];
@@ -57,112 +53,140 @@ interface SideBarProps {
* @param {function} obj.deleteRow - a function to delete a certain keyPair * @param {function} obj.deleteRow - a function to delete a certain keyPair
* @returns the sidebar with 'secret's settings' * @returns the sidebar with 'secret's settings'
*/ */
const SideBar = ({ const SideBar = ({
toggleSidebar, toggleSidebar,
data, data,
modifyKey, modifyKey,
modifyValue, modifyValue,
modifyValueOverride, modifyValueOverride,
modifyComment, modifyComment,
buttonReady, buttonReady,
savePush, savePush,
deleteRow deleteRow
}: SideBarProps) => { }: SideBarProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride != undefined); const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
const { t } = useTranslation(); 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'> return (
{isLoading ? ( <div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between">
<div className="flex items-center justify-center h-full"> {isLoading ? (
<Image <div className="flex items-center justify-center h-full">
src="/images/loading/loading.gif" <Image
height={60} src="/images/loading/loading.gif"
width={100} height={60}
alt="infisical loading indicator" width={100}
></Image> alt="infisical loading indicator"
</div> />
</div>
) : ( ) : (
<div className='h-min overflow-y-auto'> <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"> <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("dashboard:sidebar.secret")}</p> <p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
<div className='p-1' onClick={() => toggleSidebar("None")}> <div
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/> onKeyDown={() => null}
</div> role="button"
</div> tabIndex={0}
<div className='mt-4 px-4 pointer-events-none'> className="p-1"
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p> onClick={() => toggleSidebar('None')}
<DashboardInputField >
onChangeHandler={modifyKey} <FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
type="varName"
position={data[0]?.pos}
value={data[0]?.key}
isDuplicate={false}
blurred={false}
/>
</div>
{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[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[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'>{t("common:note")}:</span>
{t("dashboard:sidebar.personal-explanation")}
</div>}
<div className='mt-4 px-4'>
{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={modifyValueOverride}
pos={data[0]?.pos}
/>
</div>}
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
<DashboardInputField
onChangeHandler={modifyValueOverride}
type="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={modifyValueOverride} position={data[0]?.pos} />
</div> </div>
</div> </div>
<div className="mt-4 px-4 pointer-events-none">
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.key')}</p>
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={data[0]?.pos}
value={data[0]?.key}
isDuplicate={false}
blurred={false}
/>
</div>
{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[0].pos}
value={data[0]?.value}
isDuplicate={false}
blurred
/>
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
<GenerateSecretMenu modifyValue={modifyValue} position={data[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">
{t('common:note')}:
</span>
{t('dashboard:sidebar.personal-explanation')}
</div>
)}
<div className="mt-4 px-4">
{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={modifyValueOverride}
pos={data[0]?.pos}
/>
</div>
)}
<div
className={`relative ${
!overrideEnabled && 'opacity-40 pointer-events-none'
} duration-200`}
>
<DashboardInputField
onChangeHandler={modifyValueOverride}
type="value"
position={data[0]?.pos}
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
isDuplicate={false}
blurred
/>
<div className="absolute right-[0.57rem] top-[0.3rem] z-50">
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
</div>
</div>
</div>
<SecretVersionList secretId={data[0]?.id} />
<CommentField
comment={data[0]?.comment}
modifyComment={modifyComment}
position={data[0]?.pos}
/>
</div> </div>
<SecretVersionList secretId={data[0]?.id} /> )}
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} /> <div className="flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]">
<Button
text={String(t('common:save-changes'))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() =>
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
}
/>
</div> </div>
)}
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
<Button
text={String(t("common:save-changes"))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() => deleteRow({ ids: data.map(secret => secret.id), secretName: data[0]?.key })}
/>
</div> </div>
</div> );
}; };
export default SideBar; export default SideBar;

View File

@@ -21,12 +21,8 @@ interface IntegrationAuth {
interface Props { interface Props {
cloudIntegrationOption: CloudIntegrationOption; cloudIntegrationOption: CloudIntegrationOption;
setSelectedIntegrationOption: ( setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
cloudIntegration: CloudIntegrationOption integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
) => void;
integrationOptionPress: (
cloudIntegrationOption: CloudIntegrationOption
) => void;
integrationAuths: IntegrationAuth[]; integrationAuths: IntegrationAuth[];
} }
@@ -34,11 +30,14 @@ const CloudIntegration = ({
cloudIntegrationOption, cloudIntegrationOption,
setSelectedIntegrationOption, setSelectedIntegrationOption,
integrationOptionPress, integrationOptionPress,
integrationAuths, integrationAuths
}: Props) => { }: Props) => {
const router = useRouter(); const router = useRouter();
return integrationAuths ? ( return integrationAuths ? (
<div <div
onKeyDown={() => null}
role="button"
tabIndex={0}
className={`relative ${ className={`relative ${
cloudIntegrationOption.isAvailable cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer' ? 'hover:bg-white/10 duration-200 cursor-pointer'
@@ -55,18 +54,17 @@ const CloudIntegration = ({
src={`/images/integrations/${cloudIntegrationOption.name}.png`} src={`/images/integrations/${cloudIntegrationOption.name}.png`}
height={70} height={70}
width={70} width={70}
alt='integration logo' alt="integration logo"
/> />
{cloudIntegrationOption.name.split(' ').length > 2 ? ( {cloudIntegrationOption.name.split(' ').length > 2 ? (
<div className='font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs'> <div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
<div>{cloudIntegrationOption.name.split(' ')[0]}</div> <div>{cloudIntegrationOption.name.split(' ')[0]}</div>
<div className='text-base'> <div className="text-base">
{cloudIntegrationOption.name.split(' ')[1]}{' '} {cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
{cloudIntegrationOption.name.split(' ')[2]}
</div> </div>
</div> </div>
) : ( ) : (
<div className='font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs'> <div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
{cloudIntegrationOption.name} {cloudIntegrationOption.name}
</div> </div>
)} )}
@@ -74,43 +72,45 @@ const CloudIntegration = ({
integrationAuths integrationAuths
.map((authorization) => authorization.integration) .map((authorization) => authorization.integration)
.includes(cloudIntegrationOption.name.toLowerCase()) && ( .includes(cloudIntegrationOption.name.toLowerCase()) && (
<div className='absolute group z-40 top-0 right-0 flex flex-row'> <div className="absolute group z-40 top-0 right-0 flex flex-row">
<div <div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
deleteIntegrationAuth({ deleteIntegrationAuth({
integrationAuthId: integrationAuths integrationAuthId: integrationAuths
.filter( .filter(
(authorization) => (authorization) =>
authorization.integration == authorization.integration === cloudIntegrationOption.name.toLowerCase()
cloudIntegrationOption.name.toLowerCase()
) )
.map((authorization) => authorization._id)[0], .map((authorization) => authorization._id)[0]
}); });
router.reload(); router.reload();
}} }}
className='cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200' className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
> >
<FontAwesomeIcon icon={faX} className='text-xs mr-2 py-px' /> <FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
Revoke Revoke
</div> </div>
<div className='w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200'> <div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
<FontAwesomeIcon icon={faCheck} className='text-xs mr-2' /> <FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
Authorized Authorized
</div> </div>
</div> </div>
)} )}
{!cloudIntegrationOption.isAvailable && ( {!cloudIntegrationOption.isAvailable && (
<div className='absolute group z-50 top-0 right-0 flex flex-row'> <div className="absolute group z-50 top-0 right-0 flex flex-row">
<div className='w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90'> <div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
Coming Soon Coming Soon
</div> </div>
</div> </div>
)} )}
</div> </div>
) : ( ) : (
<div></div> <div />
); );
}; };

View File

@@ -29,7 +29,7 @@ const CloudIntegrationSection = ({
return ( return (
<> <>
<div <div
className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`} className="flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2"
> >
<h1 className='font-semibold text-3xl'> <h1 className='font-semibold text-3xl'>
{t('integrations:cloud-integrations')} {t('integrations:cloud-integrations')}

View File

@@ -7,33 +7,29 @@ interface Framework {
docsLink: string; docsLink: string;
} }
const FrameworkIntegration = ({ framework }: { framework: Framework }) => { const FrameworkIntegration = ({ framework }: { framework: Framework }) => (
return ( <a
<a href={framework.docsLink}
href={framework.docsLink} rel="noopener"
rel='noopener' className="relative flex flex-row justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer"
className={`relative flex flex-row items-center justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer`} >
<div
className={`hover:bg-white/10 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
framework?.name?.split(' ').length > 1 ? 'text-sm px-1' : 'text-xl px-2'
} text-center w-full max-w-xs`}
> >
<div {framework?.image && (
className={`hover:bg-white/10 duration-200 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${ <Image
framework?.name?.split(' ').length > 1 src={`/images/integrations/${framework.image}.png`}
? 'text-sm px-1' height={framework?.name ? 60 : 90}
: 'text-xl px-2' width={framework?.name ? 60 : 90}
} text-center w-full max-w-xs`} alt="integration logo"
> />
{framework?.image && ( )}
<Image {framework?.name && framework?.image && <div className="h-2" />}
src={`/images/integrations/${framework.image}.png`} {framework?.name && framework.name}
height={framework?.name ? 60 : 90} </div>
width={framework?.name ? 60 : 90} </a>
alt='integration logo' );
></Image>
)}
{framework?.name && framework?.image && <div className='h-2'></div>}
{framework?.name && framework.name}
</div>
</a>
);
};
export default FrameworkIntegration; export default FrameworkIntegration;

View File

@@ -1,33 +1,26 @@
import { useEffect, useState } from "react"; /* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router"; import { useEffect, useState } from 'react';
import { import { useRouter } from 'next/router';
faArrowRight, import Button from '@app/components/basic/buttons/Button';
faRotate, import ListBox from '@app/components/basic/Listbox';
faX, import { faArrowRight, faRotate, 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"; // TODO: This needs to be moved from public folder
// TODO: This needs to be moved from public folder import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
import {
contextNetlifyMapping,
reverseContextNetlifyMapping,
} from "public/data/frequentConstants";
import Button from "~/components/basic/buttons/Button"; import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
import ListBox from "~/components/basic/Listbox"; import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
import updateIntegration from '../../pages/api/integrations/updateIntegration';
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration" interface TIntegration {
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps"; _id: string;
import updateIntegration from "../../pages/api/integrations/updateIntegration" app?: string;
target?: string;
interface Integration { environment: string;
_id: string; integration: string;
app?: string; integrationAuth: string;
target?: string; isActive: boolean;
environment: string; context: string;
integration: string;
integrationAuth: string;
isActive: boolean;
context: string;
} }
interface IntegrationApp { interface IntegrationApp {
@@ -36,219 +29,190 @@ interface IntegrationApp {
} }
type Props = { type Props = {
integration: Integration; integration: TIntegration;
environments: Array<{ name: string; slug: string }>; environments: Array<{ name: string; slug: string }>;
}; };
const Integration = ({ const Integration = ({ integration, environments = [] }: Props) => {
integration, // set initial environment. This find will only execute when component is mounting
environments = [] const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
}:Props ) => { environments.find(({ slug }) => slug === integration.environment) || {
// set initial environment. This find will only execute when component is mounting name: '',
const [integrationEnvironment, setIntegrationEnvironment] = useState< slug: ''
Props['environments'][0]
>(
environments.find(({ slug }) => slug === integration.environment) || {
name: '',
slug: '',
}
);
const [fileState, setFileState] = useState([]);
const router = useRouter();
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
const [integrationApp, setIntegrationApp] = useState(""); // integration app name
const [integrationTarget, setIntegrationTarget] = useState(""); // vercel-specific integration param
const [integrationContext, setIntegrationContext] = useState(""); // netlify-specific integration param
useEffect(() => {
const loadIntegration = async () => {
interface App {
name: string;
siteId?: string;
}
const tempApps: [IntegrationApp] = await getIntegrationApps({
integrationAuthId: integration.integrationAuth,
});
setApps(tempApps);
setIntegrationApp(
integration.app ? integration.app : tempApps[0].name
);
switch (integration.integration) {
case "vercel":
setIntegrationTarget(
integration?.target
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
: "Development"
);
break;
case "netlify":
setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development");
break;
default:
break;
}
}
loadIntegration();
}, []);
const renderIntegrationSpecificParams = (integration: Integration) => {
try {
switch (integration.integration) {
case "vercel":
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">
ENVIRONMENT
</div>
<ListBox
data={!integration.isActive ? [
"Development",
"Preview",
"Production"
] : null}
selected={integrationTarget}
onChange={setIntegrationTarget}
isFull={true}
/>
</div>
);
case "netlify":
return (
<div>
<div className="text-gray-400 text-xs font-semibold mb-2">
CONTEXT
</div>
<ListBox
data={!integration.isActive ? [
"Production",
"Deploy previews",
"Branch deploys",
"Local development"
] : null}
selected={integrationContext}
onChange={setIntegrationContext}
/>
</div>
);
default:
return <div></div>;
}
} catch (err) {
console.error(err);
}
} }
);
if (!integrationApp || apps.length === 0) return <div></div> const [fileState, setFileState] = useState([]);
const router = useRouter();
return ( const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
<div className='max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between'> const [integrationApp, setIntegrationApp] = useState(''); // integration app name
<div className='flex'> const [integrationTarget, setIntegrationTarget] = useState(''); // vercel-specific integration param
<div> const [integrationContext, setIntegrationContext] = useState(''); // netlify-specific integration param
<p className='text-gray-400 text-xs font-semibold mb-2'>
ENVIRONMENT useEffect(() => {
</p> const loadIntegration = async () => {
<ListBox interface App {
data={ name: string;
!integration.isActive siteId?: string;
? environments.map(({ name }) => name) }
: null
} const tempApps: [IntegrationApp] = await getIntegrationApps({
selected={integrationEnvironment.name} integrationAuthId: integration.integrationAuth
onChange={(envName) => });
setIntegrationEnvironment(
environments.find(({ name }) => envName === name) || { setApps(tempApps);
name: 'unknown', setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
slug: 'unknown',
} switch (integration.integration) {
) case 'vercel':
} setIntegrationTarget(
isFull={true} integration?.target
/> ? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
</div> : 'Development'
<div className='pt-2'> );
<FontAwesomeIcon break;
icon={faArrowRight} case 'netlify':
className='mx-4 text-gray-400 mt-8' setIntegrationContext(
/> integration?.context ? contextNetlifyMapping[integration.context] : 'Local development'
</div> );
<div className='mr-2'> break;
<p className='text-gray-400 text-xs font-semibold mb-2'> default:
INTEGRATION break;
</p> }
<div className='py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300'> };
{integration.integration.charAt(0).toUpperCase() +
integration.integration.slice(1)} loadIntegration();
</div> }, []);
</div>
<div className='mr-2'> // eslint-disable-next-line @typescript-eslint/no-shadow
<div className='text-gray-400 text-xs font-semibold mb-2'>APP</div> const renderIntegrationSpecificParams = (integration: TIntegration) => {
<ListBox try {
data={!integration.isActive ? apps.map((app) => app.name) : null} switch (integration.integration) {
selected={integrationApp} case 'vercel':
onChange={(app) => { return (
setIntegrationApp(app); <div>
}} <div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
/> <ListBox
</div> data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
{renderIntegrationSpecificParams(integration)} selected={integrationTarget}
</div> onChange={setIntegrationTarget}
<div className='flex items-end'> isFull
{integration.isActive ? (
<div className='max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4'>
<FontAwesomeIcon
icon={faRotate}
className='text-lg mr-2.5 text-primary animate-spin'
/> />
<div className='text-gray-300 font-semibold'>In Sync</div>
</div> </div>
) : ( );
<Button case 'netlify':
text='Start Integration' return (
onButtonPressed={async () => { <div>
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined <div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
const siteId = siteApp?.siteId ? siteApp.siteId : null; <ListBox
data={
!integration.isActive
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
: null
}
selected={integrationContext}
onChange={setIntegrationContext}
/>
</div>
);
default:
return <div />;
}
} catch (err) {
console.error(err);
}
await updateIntegration({ return <div />;
integrationId: integration._id,
environment: integrationEnvironment.slug,
app: integrationApp,
isActive: true,
target: integrationTarget
? integrationTarget.toLowerCase()
: null,
context: integrationContext
? reverseContextNetlifyMapping[integrationContext]
: null,
siteId,
});
router.reload();
}}
color='mineshaft'
size='md'
/>
)}
<div className='opacity-50 hover:opacity-100 duration-200 ml-2'>
<Button
onButtonPressed={async () => {
await deleteIntegration({
integrationId: integration._id,
});
router.reload();
}}
color='red'
size='icon-md'
icon={faX}
/>
</div>
</div>
</div>
);
}; };
export default Integration; if (!integrationApp || apps.length === 0) return <div />;
return (
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
<div className="flex">
<div>
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
<ListBox
data={!integration.isActive ? environments.map(({ name }) => name) : null}
selected={integrationEnvironment.name}
onChange={(envName) =>
setIntegrationEnvironment(
environments.find(({ name }) => envName === name) || {
name: 'unknown',
slug: 'unknown'
}
)
}
isFull
/>
</div>
<div className="pt-2">
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
</div>
<div className="mr-2">
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
</div>
</div>
<div className="mr-2">
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
<ListBox
data={!integration.isActive ? apps.map((app) => app.name) : null}
selected={integrationApp}
onChange={(app) => {
setIntegrationApp(app);
}}
/>
</div>
{renderIntegrationSpecificParams(integration)}
</div>
<div className="flex items-end">
{integration.isActive ? (
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
<div className="text-gray-300 font-semibold">In Sync</div>
</div>
) : (
<Button
text="Start Integration"
onButtonPressed={async () => {
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
const siteId = siteApp?.siteId ? siteApp.siteId : null;
await updateIntegration({
integrationId: integration._id,
environment: integrationEnvironment.slug,
app: integrationApp,
isActive: true,
target: integrationTarget ? integrationTarget.toLowerCase() : null,
context: integrationContext
? reverseContextNetlifyMapping[integrationContext]
: null,
siteId
});
router.reload();
}}
color="mineshaft"
size="md"
/>
)}
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
<Button
onButtonPressed={async () => {
await deleteIntegration({
integrationId: integration._id
});
router.reload();
}}
color="red"
size="icon-md"
icon={faX}
/>
</div>
</div>
</div>
);
};
export default Integration;

View File

@@ -1,4 +1,4 @@
import guidGenerator from '~/utilities/randomId'; import guidGenerator from '@app/components/utilities/randomId';
import Integration from './Integration'; import Integration from './Integration';
@@ -17,29 +17,21 @@ interface IntegrationType {
context: string; context: string;
} }
const ProjectIntegrationSection = ({ const ProjectIntegrationSection = ({ integrations, environments = [] }: Props) =>
integrations, integrations.length > 0 ? (
environments = [], <div className="mb-12">
}: Props) => { <div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
return integrations.length > 0 ? ( <h1 className="font-semibold text-3xl">Current Integrations</h1>
<div className='mb-12'> <p className="text-base text-gray-400">
<div className='flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2'>
<h1 className='font-semibold text-3xl'>Current Integrations</h1>
<p className='text-base text-gray-400'>
Manage your integrations of Infisical with third-party services. Manage your integrations of Infisical with third-party services.
</p> </p>
</div> </div>
{integrations.map((integration: IntegrationType) => ( {integrations.map((integration: IntegrationType) => (
<Integration <Integration key={guidGenerator()} integration={integration} environments={environments} />
key={guidGenerator()}
integration={integration}
environments={environments}
/>
))} ))}
</div> </div>
) : ( ) : (
<div></div> <div />
); );
};
export default ProjectIntegrationSection; export default ProjectIntegrationSection;

View File

@@ -1,10 +1,12 @@
/* eslint-disable jsx-a11y/anchor-is-valid */
/* eslint-disable react/jsx-key */ /* eslint-disable react/jsx-key */
import { Fragment, useEffect, useMemo, useState } from "react"; import { Fragment, useEffect, useMemo, useState } from 'react';
import Image from "next/image"; import Image from 'next/image';
import { useRouter } from "next/router"; import { useRouter } from 'next/router';
import { TFunction, useTranslation } from "next-i18next"; import { TFunction, useTranslation } from 'next-i18next';
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import logout from '@app/pages/api/auth/Logout';
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
import { import {
faAngleDown, faAngleDown,
faBook, faBook,
@@ -12,40 +14,37 @@ import {
faEnvelope, faEnvelope,
faGear, faGear,
faPlus, faPlus,
faRightFromBracket, faRightFromBracket
faUpRightFromSquare, } 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 { Menu, Transition } from "@headlessui/react";
import logout from "~/pages/api/auth/Logout"; import getOrganization from '../../pages/api/organization/GetOrg';
import getOrganizations from '../../pages/api/organization/getOrgs';
import getOrganization from "../../pages/api/organization/GetOrg"; import getUser from '../../pages/api/user/getUser';
import getOrganizations from "../../pages/api/organization/getOrgs"; import guidGenerator from '../utilities/randomId';
import getUser from "../../pages/api/user/getUser";
import guidGenerator from "../utilities/randomId";
const supportOptions = (t: TFunction) => [ const supportOptions = (t: TFunction) => [
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
t("nav:support.slack"), t('nav:support.slack'),
"https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA", 'https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA'
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
t("nav:support.docs"), t('nav:support.docs'),
"https://infisical.com/docs/getting-started/introduction", 'https://infisical.com/docs/getting-started/introduction'
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
t("nav:support.issue"), t('nav:support.issue'),
"https://github.com/Infisical/infisical-cli/issues", 'https://github.com/Infisical/infisical-cli/issues'
], ],
[ [
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />, <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
t("nav:support.email"), t('nav:support.email'),
"mailto:support@infisical.com", 'mailto:support@infisical.com'
], ]
]; ];
export interface ICurrentOrg { export interface ICurrentOrg {
@@ -79,17 +78,17 @@ export default function Navbar() {
setUser(userData); setUser(userData);
const orgsData = await getOrganizations(); const orgsData = await getOrganizations();
setOrgs(orgsData); setOrgs(orgsData);
const currentOrg = await getOrganization({ const currentUserOrg = await getOrganization({
orgId: String(localStorage.getItem("orgData.id")), orgId: String(localStorage.getItem('orgData.id'))
}); });
setCurrentOrg(currentOrg); setCurrentOrg(currentUserOrg);
})(); })();
}, []); }, []);
const closeApp = async () => { const closeApp = async () => {
console.log("Logging out..."); console.log('Logging out...');
await logout(); await logout();
router.push("/login"); router.push('/login');
}; };
return ( return (
@@ -97,28 +96,26 @@ export default function Navbar() {
<div className="m-auto flex justify-start items-center mx-4"> <div className="m-auto flex justify-start items-center mx-4">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<div className="flex justify-center py-4"> <div className="flex justify-center py-4">
<Image <Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
src="/images/logotransparent.png"
height={23}
width={57}
alt="logo"
/>
</div> </div>
<a className="text-2xl text-white font-semibold mx-2">Infisical</a> <a href="#" className="text-2xl text-white font-semibold mx-2">
Infisical
</a>
</div> </div>
</div> </div>
<div className="relative flex justify-start items-center mx-2 z-40"> <div className="relative flex justify-start items-center mx-2 z-40">
<a <a
href="https://infisical.com/docs/getting-started/introduction" href="https://infisical.com/docs/getting-started/introduction"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center"> className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center"
>
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" /> <FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
Docs Docs
</a> </a>
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
<div className="mr-4"> <div className="mr-4">
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 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"> <Menu.Button className="inline-flex w-full justify-center px-2 py-2 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">
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} /> <FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
</Menu.Button> </Menu.Button>
</div> </div>
@@ -151,7 +148,7 @@ export default function Navbar() {
</Menu> </Menu>
<Menu as="div" className="relative inline-block text-left mr-4"> <Menu as="div" className="relative inline-block text-left mr-4">
<div> <div>
<Menu.Button className="inline-flex w-full justify-center rounded-md pr-2 pl-2 py-2 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"> <Menu.Button className="inline-flex w-full justify-center pr-2 pl-2 py-2 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">
{user?.firstName} {user?.lastName} {user?.firstName} {user?.lastName}
<FontAwesomeIcon <FontAwesomeIcon
icon={faAngleDown} icon={faAngleDown}
@@ -171,12 +168,13 @@ export default function Navbar() {
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none"> <Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
<div className="px-1 py-1 "> <div className="px-1 py-1 ">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> <div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
{t("nav:user.signed-in-as")} {t('nav:user.signed-in-as')}
</div> </div>
<div <div
onClick={() => onKeyDown={() => null}
router.push("/settings/personal/" + router.query.id) role="button"
} tabIndex={0}
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md" className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
> >
<div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300"> <div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300">
@@ -185,13 +183,10 @@ export default function Navbar() {
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<div> <div>
<p className="text-gray-300 px-2 pt-1 text-sm"> <p className="text-gray-300 px-2 pt-1 text-sm">
{" "} {' '}
{user?.firstName} {user?.lastName} {user?.firstName} {user?.lastName}
</p> </p>
<p className="text-gray-400 px-2 pb-1 text-xs"> <p className="text-gray-400 px-2 pb-1 text-xs"> {user?.email}</p>
{" "}
{user?.email}
</p>
</div> </div>
<FontAwesomeIcon <FontAwesomeIcon
icon={faGear} icon={faGear}
@@ -202,21 +197,20 @@ export default function Navbar() {
</div> </div>
<div className="px-2 pt-2"> <div className="px-2 pt-2">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> <div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
{t("nav:user.current-organization")} {t('nav:user.current-organization')}
</div> </div>
<div <div
onClick={() => onKeyDown={() => null}
router.push("/settings/org/" + router.query.id) role="button"
} tabIndex={0}
onClick={() => router.push(`/settings/org/${router.query.id}`)}
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md" className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
> >
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300"> <div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
{currentOrg?.name?.charAt(0)} {currentOrg?.name?.charAt(0)}
</div> </div>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<p className="text-gray-300 px-2 text-sm"> <p className="text-gray-300 px-2 text-sm">{currentOrg?.name}</p>
{currentOrg?.name}
</p>
<FontAwesomeIcon <FontAwesomeIcon
icon={faGear} icon={faGear}
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10" className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
@@ -225,56 +219,57 @@ export default function Navbar() {
</div> </div>
<button <button
// onClick={buttonAction} // onClick={buttonAction}
type="button"
className="cursor-pointer w-full" className="cursor-pointer w-full"
> >
<div <div
onClick={() => onKeyDown={() => null}
router.push("/settings/billing/" + router.query.id) role="button"
} tabIndex={0}
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200" className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
> >
<FontAwesomeIcon <FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faCoins} />
className="text-lg pl-1.5 pr-3" <div className="text-sm">{t('nav:user.usage-billing')}</div>
icon={faCoins}
/>
<div className="text-sm">{t("nav:user.usage-billing")}</div>
</div> </div>
</button> </button>
<button <button
type="button"
// onClick={buttonAction} // onClick={buttonAction}
className="cursor-pointer w-full mb-2" className="cursor-pointer w-full mb-2"
> >
<div <div
onClick={() => onKeyDown={() => null}
router.push( role="button"
"/settings/org/" + router.query.id + "?invite" tabIndex={0}
) onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
}
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1" className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
> >
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4"> <span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
<FontAwesomeIcon icon={faPlus} className="ml-1" /> <FontAwesomeIcon icon={faPlus} className="ml-1" />
</span> </span>
<div className="text-sm ml-1">{t("nav:user.invite")}</div> <div className="text-sm ml-1">{t('nav:user.invite')}</div>
</div> </div>
</button> </button>
</div> </div>
{orgs?.length > 1 && ( {orgs?.length > 1 && (
<div className="px-1 pt-1"> <div className="px-1 pt-1">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide"> <div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
{t("nav:user.other-organizations")} {t('nav:user.other-organizations')}
</div> </div>
<div className="flex flex-col items-start px-1 mt-3 mb-2"> <div className="flex flex-col items-start px-1 mt-3 mb-2">
{orgs {orgs
.filter( .filter(
(org: { _id: string }) => (org: { _id: string }) => org._id !== localStorage.getItem('orgData.id')
org._id != localStorage.getItem("orgData.id")
) )
.map((org: { _id: string; name: string }) => ( .map((org: { _id: string; name: string }) => (
<div <div
onKeyDown={() => null}
role="button"
tabIndex={0}
key={guidGenerator()} key={guidGenerator()}
onClick={() => { onClick={() => {
localStorage.setItem("orgData.id", org._id); localStorage.setItem('orgData.id', org._id);
router.reload(); router.reload();
}} }}
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md" className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
@@ -283,9 +278,7 @@ export default function Navbar() {
{org.name.charAt(0)} {org.name.charAt(0)}
</div> </div>
<div className="flex items-center justify-between w-full"> <div className="flex items-center justify-between w-full">
<p className="text-gray-300 px-2 text-sm"> <p className="text-gray-300 px-2 text-sm">{org.name}</p>
{org.name}
</p>
</div> </div>
</div> </div>
))} ))}
@@ -296,11 +289,10 @@ export default function Navbar() {
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<button <button
type="button"
onClick={closeApp} onClick={closeApp}
className={`${ className={`${
active active ? 'bg-red font-semibold text-white' : 'text-gray-400'
? "bg-red font-semibold text-white"
: "text-gray-400"
} group flex w-full items-center rounded-md px-2 py-2 text-sm`} } group flex w-full items-center rounded-md px-2 py-2 text-sm`}
> >
<div className="relative flex justify-start items-center cursor-pointer select-none"> <div className="relative flex justify-start items-center cursor-pointer select-none">
@@ -308,7 +300,7 @@ export default function Navbar() {
className="text-lg ml-1.5 mr-3" className="text-lg ml-1.5 mr-3"
icon={faRightFromBracket} icon={faRightFromBracket}
/> />
{t("common:logout")} {t('common:logout')}
</div> </div>
</button> </button>
)} )}

View File

@@ -1,11 +1,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import getOrganization from '@app/pages/api/organization/GetOrg';
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getOrganization from '~/pages/api/organization/GetOrg';
import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
/** /**
* This is the component at the top of almost every page. * This is the component at the top of almost every page.
* It shows how to navigate to a certain page. * It shows how to navigate to a certain page.
@@ -17,7 +16,7 @@ import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
*/ */
export default function NavHeader({ export default function NavHeader({
pageName, pageName,
isProjectRelated, isProjectRelated
}: { }: {
pageName: string; pageName: string;
isProjectRelated?: boolean; isProjectRelated?: boolean;
@@ -30,12 +29,12 @@ export default function NavHeader({
(async () => { (async () => {
const orgId = localStorage.getItem('orgData.id'); const orgId = localStorage.getItem('orgData.id');
const org = await getOrganization({ const org = await getOrganization({
orgId: orgId ? orgId : '', orgId: orgId || ''
}); });
setOrgName(org.name); setOrgName(org.name);
const workspace = await getProjectInfo({ const workspace = await getProjectInfo({
projectId: String(router.query.id), projectId: String(router.query.id)
}); });
setWorkspaceName(workspace.name); setWorkspaceName(workspace.name);
})(); })();
@@ -43,27 +42,19 @@ export default function NavHeader({
}, []); }, []);
return ( return (
<div className='pt-20 ml-6 flex flex-row items-center'> <div className="pt-20 ml-6 flex flex-row items-center">
<div className='bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2'> <div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2">
{orgName?.charAt(0)} {orgName?.charAt(0)}
</div> </div>
<div className='text-primary text-sm font-semibold'>{orgName}</div> <div className="text-primary text-sm font-semibold">{orgName}</div>
{isProjectRelated && ( {isProjectRelated && (
<> <>
<FontAwesomeIcon <FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
icon={faAngleRight} <div className="font-semibold text-primary text-sm">{workspaceName}</div>
className='ml-3 text-sm text-gray-400 mr-3'
/>
<div className='font-semibold text-primary text-sm'>
{workspaceName}
</div>
</> </>
)} )}
<FontAwesomeIcon <FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
icon={faAngleRight} <div className="text-gray-400 text-sm">{pageName}</div>
className='ml-3 text-sm text-gray-400 mr-3'
/>
<div className='text-gray-400 text-sm'>{pageName}</div>
</div> </div>
); );
} }

View File

@@ -1,49 +1,48 @@
import React, { useState } from "react"; /* eslint-disable react/jsx-props-no-spreading */
import ReactCodeInput from "react-code-input"; import React, { useState } from 'react';
import { useTranslation } from "next-i18next"; import ReactCodeInput from 'react-code-input';
import { useTranslation } from 'next-i18next';
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail"; import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
// The style for the verification code input // The style for the verification code input
const props = { const props = {
inputStyle: { inputStyle: {
fontFamily: "monospace", fontFamily: 'monospace',
margin: "4px", margin: '4px',
MozAppearance: "textfield", MozAppearance: 'textfield',
width: "55px", width: '55px',
borderRadius: "5px", borderRadius: '5px',
fontSize: "24px", fontSize: '24px',
height: "55px", height: '55px',
paddingLeft: "7", paddingLeft: '7',
backgroundColor: "#0d1117", backgroundColor: '#0d1117',
color: "white", color: 'white',
border: "1px solid #2d2f33", border: '1px solid #2d2f33',
textAlign: "center", textAlign: 'center',
outlineColor: "#8ca542", outlineColor: '#8ca542',
borderColor: "#2d2f33" borderColor: '#2d2f33'
}, }
} as const; } as const;
const propsPhone = { const propsPhone = {
inputStyle: { inputStyle: {
fontFamily: "monospace", fontFamily: 'monospace',
margin: "4px", margin: '4px',
MozAppearance: "textfield", MozAppearance: 'textfield',
width: "40px", width: '40px',
borderRadius: "5px", borderRadius: '5px',
fontSize: "24px", fontSize: '24px',
height: "40px", height: '40px',
paddingLeft: "7", paddingLeft: '7',
backgroundColor: "#0d1117", backgroundColor: '#0d1117',
color: "white", color: 'white',
border: "1px solid #2d2f33", border: '1px solid #2d2f33',
textAlign: "center", textAlign: 'center',
outlineColor: "#8ca542", outlineColor: '#8ca542',
borderColor: "#2d2f33" borderColor: '#2d2f33'
}, }
} as const; } as const;
interface CodeInputStepProps { interface CodeInputStepProps {
@@ -55,17 +54,21 @@ interface CodeInputStepProps {
/** /**
* This is the second step of sign up where users need to verify their email * This is the second step of sign up where users need to verify their email
* @param {object} obj * @param {object} obj
* @param {string} obj.email - user's email to which we just sent a verification email * @param {string} obj.email - user's email to which we just sent a verification email
* @param {function} obj.incrementStep - goes to the next step of signup * @param {function} obj.incrementStep - goes to the next step of signup
* @param {function} obj.setCode - state updating function that set the current value of the emai verification code * @param {function} obj.setCode - state updating function that set the current value of the emai verification code
* @param {boolean} obj.codeError - whether the code was inputted wrong or now * @param {boolean} obj.codeError - whether the code was inputted wrong or now
* @returns * @returns
*/ */
export default function CodeInputStep({ email, incrementStep, setCode, codeError }: CodeInputStepProps): JSX.Element { export default function CodeInputStep({
email,
incrementStep,
setCode,
codeError
}: CodeInputStepProps): JSX.Element {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const resendVerificationEmail = async () => { const resendVerificationEmail = async () => {
@@ -80,12 +83,8 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
return ( 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"> <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"> <p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
{t("signup:step2-message")} <p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">
{email}{" "}
</p>
<div className="hidden md:block"> <div className="hidden md:block">
<ReactCodeInput <ReactCodeInput
name="" name=""
@@ -108,28 +107,28 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
className="mt-2 mb-6" className="mt-2 mb-6"
/> />
</div> </div>
{codeError && <Error text={t("signup:step2-code-error")} />} {codeError && <Error text={t('signup:step2-code-error')} />}
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2"> <div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<Button <Button text={t('signup:verify') ?? ''} onButtonPressed={incrementStep} size="lg" />
text={t("signup: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 text-sm"> <div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400"> <span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
{t("signup:step2-resend-alert")} <u
</span> className={`font-normal ${
<u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}> isResendingVerificationEmail
<button disabled={isLoading} onClick={resendVerificationEmail}> ? 'text-bunker-400'
{isResendingVerificationEmail ? t("signup:step2-resend-progress") : t("signup:step2-resend-submit")} : 'text-primary-700 hover:text-primary duration-200'
}`}
>
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
{isResendingVerificationEmail
? t('signup:step2-resend-progress')
: t('signup:step2-resend-submit')}
</button> </button>
</u> </u>
</div> </div>
<p className="text-sm text-bunker-400 pb-2"> <p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
{t("signup:step2-spam-alert")}
</p>
</div> </div>
</div> </div>
); );

View File

@@ -25,37 +25,37 @@ export default function DonwloadBackupPDFStep({
incrementStep, incrementStep,
email, email,
password, password,
name, name
}: DownloadBackupPDFStepProps): JSX.Element { }: DownloadBackupPDFStepProps): JSX.Element {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className='bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl'> <div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className='text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary'> <p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
{t('signup:step4-message')} {t('signup:step4-message')}
</p> </p>
<div className='flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2'> <div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
<div>{t('signup:step4-description1')}</div> <div>{t('signup:step4-description1')}</div>
<div className='mt-3'>{t('signup:step4-description2')}</div> <div className="mt-3">{t('signup:step4-description2')}</div>
</div> </div>
<div className='w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4'> <div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className='ml-2 mr-4 text-4xl' /> <FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
{t('signup:step4-description3')} {t('signup:step4-description3')}
</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'> <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">
<Button <Button
text='Download PDF' text="Download PDF"
onButtonPressed={async () => { onButtonPressed={async () => {
await issueBackupKey({ await issueBackupKey({
email, email,
password, password,
personalName: name, personalName: name,
setBackupKeyError: (value: boolean) => {}, setBackupKeyError: () => {},
setBackupKeyIssued: (value: boolean) => {}, setBackupKeyIssued: () => {}
}); });
incrementStep(); incrementStep();
}} }}
size='lg' size="lg"
/> />
</div> </div>
</div> </div>

View File

@@ -1,30 +1,32 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import Link from "next/link"; import Link from 'next/link';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
import Button from "../basic/buttons/Button";
import InputField from "../basic/InputField";
import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField';
interface DownloadBackupPDFStepProps { interface DownloadBackupPDFStepProps {
incrementStep: () => void; incrementStep: () => void;
email: string; email: string;
setEmail: (value: string) => void; setEmail: (value: string) => void;
} }
/** /**
* This is the first step of the sign up process - users need to enter their email * This is the first step of the sign up process - users need to enter their email
* @param {object} obj * @param {object} obj
* @param {string} obj.email - email of a user signing up * @param {string} obj.email - email of a user signing up
* @param {function} obj.setEmail - funciton that manages the state of the email variable * @param {function} obj.setEmail - funciton that manages the state of the email variable
* @param {function} obj.incrementStep - function to go to the next step of the signup flow * @param {function} obj.incrementStep - function to go to the next step of the signup flow
* @returns * @returns
*/ */
export default function EnterEmailStep({ email, setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element { export default function EnterEmailStep({
email,
setEmail,
incrementStep
}: DownloadBackupPDFStepProps): JSX.Element {
const [emailError, setEmailError] = useState(false); const [emailError, setEmailError] = useState(false);
const [emailErrorMessage, setEmailErrorMessage] = useState(""); const [emailErrorMessage, setEmailErrorMessage] = useState('');
const { t } = useTranslation(); const { t } = useTranslation();
/** /**
@@ -34,15 +36,11 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
let emailCheckBool = false; let emailCheckBool = false;
if (!email) { if (!email) {
setEmailError(true); setEmailError(true);
setEmailErrorMessage("Please enter your email."); setEmailErrorMessage('Please enter your email.');
emailCheckBool = true; emailCheckBool = true;
} else if ( } else if (!email.includes('@') || !email.includes('.') || !/[a-z]/.test(email)) {
!email.includes("@") ||
!email.includes(".") ||
!/[a-z]/.test(email)
) {
setEmailError(true); setEmailError(true);
setEmailErrorMessage("Please enter a valid email."); setEmailErrorMessage('Please enter a valid email.');
emailCheckBool = true; emailCheckBool = true;
} else { } else {
setEmailError(false); setEmailError(false);
@@ -57,13 +55,13 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
return ( return (
<div> <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"> <div className="bg-bunker w-full max-w-md 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"> <p className="text-4xl font-semibold flex justify-center text-primary">
{t("signup:step1-start")} {t('signup:step1-start')}
</p> </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"> <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 <InputField
label={t("common:email") ?? ""} label={t('common:email') ?? ''}
onChangeHandler={setEmail} onChangeHandler={setEmail}
type="email" type="email"
value={email} value={email}
@@ -75,11 +73,14 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
/> />
</div> </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"> <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"> <p className="text-gray-400 mt-2 md:mx-0.5">{t('signup:step1-privacy')}</p>
{t("signup:step1-privacy")}
</p>
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg"> <div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
<Button text={t("signup:step1-submit") ?? ""} type="submit" onButtonPressed={emailCheck} size="lg" /> <Button
text={t('signup:step1-submit') ?? ''}
type="submit"
onButtonPressed={emailCheck}
size="lg"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -87,7 +88,7 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
<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-sm text-primary-500"> <u className="font-normal text-sm text-primary-500">
{t("signup:already-have-account")} {t('signup:already-have-account')}
</u> </u>
</button> </button>
</Link> </Link>

View File

@@ -1,18 +1,16 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import { useRouter } from "next/router"; import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
import addUserToOrg from "~/pages/api/organization/addUserToOrg"; import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
import Button from "../basic/buttons/Button";
import Button from '../basic/buttons/Button';
/** /**
* This is the last step of the signup flow. People can optionally invite their teammates here. * This is the last step of the signup flow. People can optionally invite their teammates here.
*/ */
export default function TeamInviteStep(): JSX.Element { export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState(""); const [emails, setEmails] = useState('');
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
@@ -20,34 +18,31 @@ export default function TeamInviteStep(): JSX.Element {
const redirectToHome = async () => { const redirectToHome = async () => {
const userWorkspaces = await getWorkspaces(); const userWorkspaces = await getWorkspaces();
const userWorkspace = userWorkspaces[0]._id; const userWorkspace = userWorkspaces[0]._id;
router.push("/home/" + userWorkspace); router.push(`/home/${userWorkspace}`);
};
} const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
inviteEmails
const inviteUsers = async ({ emails }: { emails: string; }) => {
emails
.split(',') .split(',')
.map(email => email.trim()) .map((email) => email.trim())
.map(async (email) => await addUserToOrg(email, String(localStorage.getItem('orgData.id')))); .map(async (email) => addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
await redirectToHome(); await redirectToHome();
} };
return ( return (
<div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32"> <div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32">
<p className="text-4xl font-semibold flex justify-center text-primary"> <p className="text-4xl font-semibold flex justify-center text-primary">
{t("signup:step5-invite-team")} {t('signup:step5-invite-team')}
</p> </p>
<p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4"> <p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4">
{t("signup:step5-subtitle")} {t('signup:step5-subtitle')}
</p> </p>
<div> <div>
<div className="overflow-auto bg-bunker-800"> <div className="overflow-auto bg-bunker-800">
<div className="whitespace-pre-wrap break-words bg-transparent"> <div className="whitespace-pre-wrap break-words bg-transparent" />
</div>
</div> </div>
<textarea <textarea
className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70" className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
value={emails} value={emails}
onChange={(e) => setEmails(e.target.value)} onChange={(e) => setEmails(e.target.value)}
@@ -55,15 +50,18 @@ export default function TeamInviteStep(): JSX.Element {
/> />
</div> </div>
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2"> <div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
<div <div
onKeyDown={() => null}
role="button"
tabIndex={0}
className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200" className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200"
onClick={redirectToHome} onClick={redirectToHome}
> >
{t("signup:step5-skip")} {t('signup:step5-skip')}
</div> </div>
<Button <Button
text={t("signup:step5-send-invites") ?? ""} text={t('signup:step5-send-invites') ?? ''}
onButtonPressed={() => inviteUsers({ emails})} onButtonPressed={() => inviteUsers({ emails })}
size="lg" size="lg"
/> />
</div> </div>

View File

@@ -1,10 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
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';
import jsrp from 'jsrp';
import completeAccountInformationSignup from '~/pages/api/auth/CompleteAccountInformationSignup'; import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util';
import Button from '../basic/buttons/Button'; import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField'; import InputField from '../basic/InputField';
@@ -12,9 +14,7 @@ import attemptLogin from '../utilities/attemptLogin';
import passwordCheck from '../utilities/checks/PasswordCheck'; import passwordCheck from '../utilities/checks/PasswordCheck';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
const nacl = require('tweetnacl'); // eslint-disable-next-line new-cap
const jsrp = require('jsrp');
nacl.util = require('tweetnacl-util');
const client = new jsrp.client(); const client = new jsrp.client();
interface UserInfoStepProps { interface UserInfoStepProps {
@@ -51,7 +51,7 @@ export default function UserInfoStep({
firstName, firstName,
setFirstName, setFirstName,
lastName, lastName,
setLastName, setLastName
}: UserInfoStepProps): JSX.Element { }: UserInfoStepProps): JSX.Element {
const [firstNameError, setFirstNameError] = useState(false); const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false);
@@ -85,7 +85,7 @@ export default function UserInfoStep({
setPasswordErrorLength, setPasswordErrorLength,
setPasswordErrorNumber, setPasswordErrorNumber,
setPasswordErrorLowerCase, setPasswordErrorLowerCase,
errorCheck, errorCheck
}); });
if (!errorCheck) { if (!errorCheck) {
@@ -93,17 +93,14 @@ export default function UserInfoStep({
const pair = nacl.box.keyPair(); const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey; const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey; const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array); const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array); const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({ const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY, text: PRIVATE_KEY,
secret: password secret: password
.slice(0, 32) .slice(0, 32)
.padStart( .padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
32 + (password.slice(0, 32).length - new Blob([password]).size),
'0'
),
}) as { ciphertext: string; iv: string; tag: string }; }) as { ciphertext: string; iv: string; tag: string };
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY); localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
@@ -111,50 +108,41 @@ export default function UserInfoStep({
client.init( client.init(
{ {
username: email, username: email,
password: password, password
}, },
async () => { async () => {
client.createVerifier( client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
async (err: any, result: { salt: string; verifier: string }) => { const response = await completeAccountInformationSignup({
const response = await completeAccountInformationSignup({ email,
email, firstName,
firstName, lastName,
lastName, organizationName: `${firstName}'s organization`,
organizationName: firstName + "'s organization", publicKey: PUBLIC_KEY,
publicKey: PUBLIC_KEY, ciphertext,
ciphertext, iv,
iv, tag,
tag, salt: result.salt,
salt: result.salt, verifier: result.verifier,
verifier: result.verifier, token: verificationToken
token: verificationToken, });
});
// if everything works, go the main dashboard page. // if everything works, go the main dashboard page.
if (response.status === 200) { if (response.status === 200) {
// response = await response.json(); // response = await response.json();
localStorage.setItem('publicKey', PUBLIC_KEY); localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext); localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv); localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag); localStorage.setItem('tag', tag);
try { try {
await attemptLogin( await attemptLogin(email, password, () => {}, router, true, false);
email, incrementStep();
password, } catch (error) {
(value: boolean) => {}, setIsLoading(false);
router,
true,
false
);
incrementStep();
} catch (error) {
setIsLoading(false);
}
} }
} }
); });
} }
); );
} else { } else {
@@ -163,142 +151,108 @@ export default function UserInfoStep({
}; };
return ( return (
<div className='bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16'> <div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
<p className='text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary'> <p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
{t('signup:step3-message')} {t('signup:step3-message')}
</p> </p>
<div className='relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24'> <div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
<InputField <InputField
label={t('common:first-name')} label={t('common:first-name')}
onChangeHandler={setFirstName} onChangeHandler={setFirstName}
type='name' type="name"
value={firstName} value={firstName}
isRequired isRequired
errorText={ errorText={
t('common:validate-required', { t('common:validate-required', {
name: t('common:first-name'), name: t('common:first-name')
}) as string }) as string
} }
error={firstNameError} error={firstNameError}
autoComplete='given-name' autoComplete="given-name"
/> />
</div> </div>
<div className='mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24'> <div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
<InputField <InputField
label={t('common:last-name')} label={t('common:last-name')}
onChangeHandler={setLastName} onChangeHandler={setLastName}
type='name' type="name"
value={lastName} value={lastName}
isRequired isRequired
errorText={ errorText={
t('common:validate-required', { t('common:validate-required', {
name: t('common:last-name'), name: t('common:last-name')
}) as string }) as string
} }
error={lastNameError} error={lastNameError}
autoComplete='family-name' autoComplete="family-name"
/> />
</div> </div>
<div className='mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60'> <div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
<InputField <InputField
label={t('section-password:password')} label={t('section-password:password')}
onChangeHandler={(password: string) => { onChangeHandler={(pass: string) => {
setPassword(password); setPassword(pass);
passwordCheck({ passwordCheck({
password, password: pass,
setPasswordErrorLength, setPasswordErrorLength,
setPasswordErrorNumber, setPasswordErrorNumber,
setPasswordErrorLowerCase, setPasswordErrorLowerCase,
errorCheck: false, errorCheck: false
}); });
}} }}
type='password' type="password"
value={password} value={password}
isRequired isRequired
error={ error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase autoComplete="new-password"
} id="new-password"
autoComplete='new-password'
id='new-password'
/> />
{passwordErrorLength || {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
passwordErrorLowerCase || <div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
passwordErrorNumber ? ( <div className="text-gray-400 text-sm mb-1">{t('section-password:validate-base')}</div>
<div className='w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md'> <div className="flex flex-row justify-start items-center ml-1">
<div className={`text-gray-400 text-sm mb-1`}>
{t('section-password:validate-base')}
</div>
<div className='flex flex-row justify-start items-center ml-1'>
{passwordErrorLength ? ( {passwordErrorLength ? (
<FontAwesomeIcon <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
icon={faX}
className='text-md text-red mr-2.5'
/>
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
icon={faCheck}
className='text-md text-primary mr-2'
/>
)} )}
<div <div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
className={`${
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section-password:validate-length')} {t('section-password:validate-length')}
</div> </div>
</div> </div>
<div className='flex flex-row justify-start items-center ml-1'> <div className="flex flex-row justify-start items-center ml-1">
{passwordErrorLowerCase ? ( {passwordErrorLowerCase ? (
<FontAwesomeIcon <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
icon={faX}
className='text-md text-red mr-2.5'
/>
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
icon={faCheck}
className='text-md text-primary mr-2'
/>
)} )}
<div <div
className={`${ className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
> >
{t('section-password:validate-case')} {t('section-password:validate-case')}
</div> </div>
</div> </div>
<div className='flex flex-row justify-start items-center ml-1'> <div className="flex flex-row justify-start items-center ml-1">
{passwordErrorNumber ? ( {passwordErrorNumber ? (
<FontAwesomeIcon <FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
icon={faX}
className='text-md text-red mr-2.5'
/>
) : ( ) : (
<FontAwesomeIcon <FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
icon={faCheck}
className='text-md text-primary mr-2'
/>
)} )}
<div <div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
className={`${
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section-password:validate-number')} {t('section-password:validate-number')}
</div> </div>
</div> </div>
</div> </div>
) : ( ) : (
<div className='py-2'></div> <div className="py-2" />
)} )}
</div> </div>
<div className='flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3'> <div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3">
<Button <Button
text={t('signup:signup') ?? ''} text={t('signup:signup') ?? ''}
loading={isLoading} loading={isLoading}
onButtonPressed={signupErrorCheck} onButtonPressed={signupErrorCheck}
size='lg' size="lg"
/> />
</div> </div>
</div> </div>

View File

@@ -1,32 +1,27 @@
import token from '~/pages/api/auth/Token'; import token from '@app/pages/api/auth/Token';
export default class SecurityClient { export default class SecurityClient {
static #token = ''; static #token = '';
constructor() {} static setToken(tokenStr: string) {
this.#token = tokenStr;
static setToken(token: string) {
this.#token = token;
} }
static async fetchCall( static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
resource: RequestInfo,
options?: RequestInit | undefined
) {
const req = new Request(resource, options); const req = new Request(resource, options);
if (this.#token == '') { if (this.#token === '') {
try { try {
// TODO: This should be moved to a context to do it only once when app loads // TODO: This should be moved to a context to do it only once when app loads
// this try catch saves route guard from a stuck state // this try catch saves route guard from a stuck state
this.setToken(await token()); this.setToken(await token());
} catch (error) { } catch (error) {
console.error("Unauthorized access"); console.error('Unauthorized access');
} }
} }
if (this.#token) { if (this.#token) {
req.headers.set('Authorization', 'Bearer ' + this.#token); req.headers.set('Authorization', `Bearer ${this.#token}`);
} }
return fetch(req); return fetch(req);

View File

@@ -1,13 +1,16 @@
import { SecretDataProps } from 'public/data/frequentInterfaces'; /* eslint-disable prefer-destructuring */
import crypto from 'crypto';
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import login1 from '~/pages/api/auth/Login1'; import login1 from '@app/pages/api/auth/Login1';
import login2 from '~/pages/api/auth/Login2'; import login2 from '@app/pages/api/auth/Login2';
import addSecrets from '~/pages/api/files/AddSecrets'; import addSecrets from '@app/pages/api/files/AddSecrets';
import getOrganizations from '~/pages/api/organization/getOrgs'; import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects'; import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import getUser from '~/pages/api/user/getUser'; import getUser from '@app/pages/api/user/getUser';
import uploadKeys from '~/pages/api/workspace/uploadKeys'; import uploadKeys from '@app/pages/api/workspace/uploadKeys';
import jsrp from 'jsrp';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import { encryptAssymmetric } from './cryptography/crypto'; import { encryptAssymmetric } from './cryptography/crypto';
import encryptSecrets from './secrets/encryptSecrets'; import encryptSecrets from './secrets/encryptSecrets';
@@ -15,11 +18,7 @@ import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage'; import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient'; import SecurityClient from './SecurityClient';
// eslint-disable-next-line new-cap
const crypto = require("crypto");
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const jsrp = require('jsrp');
const client = new jsrp.client(); const client = new jsrp.client();
/** /**
@@ -46,7 +45,7 @@ const attemptLogin = async (
client.init( client.init(
{ {
username: email, username: email,
password: password password
}, },
async () => { async () => {
const clientPublicKey = client.getPublicKey(); const clientPublicKey = client.getPublicKey();
@@ -59,8 +58,10 @@ const attemptLogin = async (
const clientProof = client.getProof(); // called M1 const clientProof = client.getProof(); // called M1
// if everything works, go the main dashboard page. // if everything works, go the main dashboard page.
const { token, publicKey, encryptedPrivateKey, iv, tag } = const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(
await login2(email, clientProof); email,
clientProof
);
SecurityClient.setToken(token); SecurityClient.setToken(token);
@@ -70,10 +71,7 @@ const attemptLogin = async (
tag, tag,
secret: password secret: password
.slice(0, 32) .slice(0, 32)
.padStart( .padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
32 + (password.slice(0, 32).length - new Blob([password]).size),
'0'
)
}); });
saveTokenToLocalStorage({ saveTokenToLocalStorage({
@@ -83,9 +81,9 @@ const attemptLogin = async (
tag, tag,
privateKey privateKey
}); });
const userOrgs = await getOrganizations(); const userOrgs = await getOrganizations();
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id); const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
let orgToLogin; let orgToLogin;
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) { if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
@@ -99,11 +97,9 @@ const attemptLogin = async (
orgId: orgToLogin orgId: orgToLogin
}); });
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id); orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
let projectToLogin; let projectToLogin;
if ( if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
orgUserProjects.includes(localStorage.getItem('projectData.id'))
) {
projectToLogin = localStorage.getItem('projectData.id'); projectToLogin = localStorage.getItem('projectData.id');
} else { } else {
try { try {
@@ -113,90 +109,104 @@ const attemptLogin = async (
console.log('ERROR: User likely has no projects. ', error); console.log('ERROR: User likely has no projects. ', error);
} }
} }
if (email) { if (email) {
telemetry.identify(email); telemetry.identify(email);
telemetry.capture('User Logged In'); telemetry.capture('User Logged In');
} }
if (isSignUp) { if (isSignUp) {
const randomBytes = crypto.randomBytes(16).toString("hex"); const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY")); const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
const myUser = await getUser(); const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({ const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes, plaintext: randomBytes,
publicKey: myUser.publicKey, publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY
}) as { ciphertext: string; nonce: string }; }) as { ciphertext: string; nonce: string };
await uploadKeys( await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
projectToLogin,
myUser._id,
ciphertext,
nonce
);
const secretsToBeAdded: SecretDataProps[] = [{ const secretsToBeAdded: SecretDataProps[] = [
pos: 0, {
key: "DATABASE_URL", pos: 0,
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net", key: 'DATABASE_URL',
valueOverride: undefined, // eslint-disable-next-line no-template-curly-in-string
comment: "This is an example of secret referencing.", value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
id: '' valueOverride: undefined,
}, { comment: 'This is an example of secret referencing.',
pos: 1, id: ''
key: "DB_USERNAME", },
value: "OVERRIDE_THIS", {
valueOverride: undefined, pos: 1,
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", key: 'DB_USERNAME',
id: '' value: 'OVERRIDE_THIS',
}, { valueOverride: undefined,
pos: 2, comment:
key: "DB_PASSWORD", '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',
value: "OVERRIDE_THIS", id: ''
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: 2,
}, { key: 'DB_PASSWORD',
pos: 3, value: 'OVERRIDE_THIS',
key: "DB_USERNAME", valueOverride: undefined,
value: "user1234", comment:
valueOverride: "user1234", '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',
comment: "", id: ''
id: '' },
}, { {
pos: 4, pos: 3,
key: "DB_PASSWORD", key: 'DB_USERNAME',
value: "example_password", value: 'user1234',
valueOverride: "example_password", valueOverride: 'user1234',
comment: "", comment: '',
id: '' id: ''
}, { },
pos: 5, {
key: "TWILIO_AUTH_TOKEN", pos: 4,
value: "example_twillio_token", key: 'DB_PASSWORD',
valueOverride: undefined, value: 'example_password',
comment: "", valueOverride: 'example_password',
id: '' comment: '',
}, { id: ''
pos: 6, },
key: "WEBSITE_URL", {
value: "http://localhost:3000", pos: 5,
valueOverride: undefined, key: 'TWILIO_AUTH_TOKEN',
comment: "", value: 'example_twillio_token',
id: '' valueOverride: undefined,
}] comment: '',
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId: String(localStorage.getItem('projectData.id')), env: 'dev' }) id: ''
await addSecrets({ secrets: secrets ?? [], env: "dev", workspaceId: String(localStorage.getItem('projectData.id')) }); },
{
pos: 6,
key: 'WEBSITE_URL',
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: ''
}
];
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: String(localStorage.getItem('projectData.id')),
env: 'dev'
});
await addSecrets({
secrets: secrets ?? [],
env: 'dev',
workspaceId: String(localStorage.getItem('projectData.id'))
});
} }
if (isLogin) { if (isLogin) {
router.push('/dashboard/' + localStorage.getItem('projectData.id')); router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
setErrorLogin(true); setErrorLogin(true);
console.log('Login response not available'); console.log('Login response not available');
} }

View File

@@ -1,5 +1,5 @@
import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers'; import getOrganizationUsers from '@app/pages/api/organization/GetOrgUsers';
import checkUserAction from '~/pages/api/userActions/checkUserAction'; import checkUserAction from '@app/pages/api/userActions/checkUserAction';
interface OnboardingCheckProps { interface OnboardingCheckProps {
setTotalOnboardingActionsDone?: (value: number) => void; setTotalOnboardingActionsDone?: (value: number) => void;
@@ -26,46 +26,43 @@ const onboardingCheck = async ({
action: 'slack_cta_clicked' action: 'slack_cta_clicked'
}); });
if (userActionSlack) { if (userActionSlack) {
countActions = countActions + 1; countActions += 1;
} }
setHasUserClickedSlack && if (setHasUserClickedSlack) setHasUserClickedSlack(!!userActionSlack);
setHasUserClickedSlack(userActionSlack ? true : false);
const userActionSecrets = await checkUserAction({ const userActionSecrets = await checkUserAction({
action: 'first_time_secrets_pushed' action: 'first_time_secrets_pushed'
}); });
if (userActionSecrets) { if (userActionSecrets) {
countActions = countActions + 1; countActions += 1;
} }
setHasUserPushedSecrets && if (setHasUserPushedSecrets) setHasUserPushedSecrets(!!userActionSecrets);
setHasUserPushedSecrets(userActionSecrets ? true : false);
const userActionIntro = await checkUserAction({ const userActionIntro = await checkUserAction({
action: 'intro_cta_clicked' action: 'intro_cta_clicked'
}); });
if (userActionIntro) { if (userActionIntro) {
countActions = countActions + 1; countActions += 1;
} }
setHasUserClickedIntro && if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
setHasUserClickedIntro(userActionIntro ? true : false);
const userActionStar = await checkUserAction({ const userActionStar = await checkUserAction({
action: 'star_cta_clicked' action: 'star_cta_clicked'
}); });
if (userActionStar) { if (userActionStar) {
countActions = countActions + 1; countActions += 1;
} }
setHasUserStarred && setHasUserStarred(userActionStar ? true : false); if (setHasUserStarred) setHasUserStarred(!!userActionStar);
const orgId = localStorage.getItem('orgData.id'); const orgId = localStorage.getItem('orgData.id');
const orgUsers = await getOrganizationUsers({ const orgUsers = await getOrganizationUsers({
orgId: orgId ? orgId : '' orgId: orgId || ''
}); });
if (orgUsers.length > 1) { if (orgUsers.length > 1) {
countActions = countActions + 1; countActions += 1;
} }
setUsersInOrg && setUsersInOrg(orgUsers.length > 1); if (setUsersInOrg) setUsersInOrg(orgUsers.length > 1);
setTotalOnboardingActionsDone && setTotalOnboardingActionsDone(countActions); if (setTotalOnboardingActionsDone) setTotalOnboardingActionsDone(countActions);
}; };
export default onboardingCheck; export default onboardingCheck;

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-param-reassign */
interface PasswordCheckProps { interface PasswordCheckProps {
password: string; password: string;
errorCheck: boolean; errorCheck: boolean;
@@ -14,7 +15,7 @@ const passwordCheck = ({
setPasswordErrorLength, setPasswordErrorLength,
setPasswordErrorNumber, setPasswordErrorNumber,
setPasswordErrorLowerCase, setPasswordErrorLowerCase,
errorCheck, errorCheck
}: PasswordCheckProps) => { }: PasswordCheckProps) => {
if (!password || password.length < 14) { if (!password || password.length < 14) {
setPasswordErrorLength(true); setPasswordErrorLength(true);

View File

@@ -32,7 +32,6 @@ class Aes256Gcm {
/** /**
* No need to run the constructor. The class only has static methods. * No need to run the constructor. The class only has static methods.
*/ */
constructor() {}
/** /**
* Encrypts text with AES 256 GCM. * Encrypts text with AES 256 GCM.
@@ -65,11 +64,7 @@ class Aes256Gcm {
* @returns {string} * @returns {string}
*/ */
static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string { static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
const decipher = crypto.createDecipheriv( const decipher = crypto.createDecipheriv(ALGORITHM, secret, Buffer.from(iv, 'base64'));
ALGORITHM,
secret,
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64')); decipher.setAuthTag(Buffer.from(tag, 'base64'));
let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); let cleartext = decipher.update(ciphertext, 'base64', 'utf8');

View File

@@ -1,8 +1,8 @@
/* eslint-disable new-cap */
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
import SRP1 from '@app/pages/api/auth/SRP1';
import jsrp from 'jsrp'; import jsrp from 'jsrp';
import changePassword2 from '~/pages/api/auth/ChangePassword2';
import SRP1 from '~/pages/api/auth/SRP1';
import Aes256Gcm from './aes-256-gcm'; import Aes256Gcm from './aes-256-gcm';
const clientOldPassword = new jsrp.client(); const clientOldPassword = new jsrp.client();
@@ -33,15 +33,16 @@ const changePassword = async (
clientOldPassword.init( clientOldPassword.init(
{ {
username: email, username: email,
password: currentPassword, password: currentPassword
}, },
async () => { async () => {
const clientPublicKey = clientOldPassword.getPublicKey(); const clientPublicKey = clientOldPassword.getPublicKey();
let serverPublicKey, salt; let serverPublicKey;
let salt;
try { try {
const res = await SRP1({ const res = await SRP1({
clientPublicKey: clientPublicKey, clientPublicKey
}); });
serverPublicKey = res.serverPublicKey; serverPublicKey = res.serverPublicKey;
salt = res.salt; salt = res.salt;
@@ -57,7 +58,7 @@ const changePassword = async (
clientNewPassword.init( clientNewPassword.init(
{ {
username: email, username: email,
password: newPassword, password: newPassword
}, },
async () => { async () => {
clientNewPassword.createVerifier(async (err, result) => { clientNewPassword.createVerifier(async (err, result) => {
@@ -67,11 +68,9 @@ const changePassword = async (
secret: newPassword secret: newPassword
.slice(0, 32) .slice(0, 32)
.padStart( .padStart(
32 + 32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
(newPassword.slice(0, 32).length -
new Blob([newPassword]).size),
'0' '0'
), )
}); });
if (ciphertext) { if (ciphertext) {
@@ -87,18 +86,18 @@ const changePassword = async (
tag, tag,
salt: result.salt, salt: result.salt,
verifier: result.verifier, verifier: result.verifier,
clientProof, clientProof
}); });
if (res && res.status == 400) { if (res && res.status === 400) {
setCurrentPasswordError(true); setCurrentPasswordError(true);
} else if (res && res.status == 200) { } else if (res && res.status === 200) {
setPasswordChanged(true); setPasswordChanged(true);
setCurrentPassword(''); setCurrentPassword('');
setNewPassword(''); setNewPassword('');
} }
} catch (err) { } catch (error) {
setCurrentPasswordError(true); setCurrentPasswordError(true);
console.log(err); console.log(error);
} }
} }
}); });

View File

@@ -1,8 +1,9 @@
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
import aes from './aes-256-gcm'; import aes from './aes-256-gcm';
type encryptAsymmetricProps = { const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
type EncryptAsymmetricProps = {
plaintext: string; plaintext: string;
publicKey: string; publicKey: string;
privateKey: string; privateKey: string;
@@ -22,8 +23,8 @@ type encryptAsymmetricProps = {
const encryptAssymmetric = ({ const encryptAssymmetric = ({
plaintext, plaintext,
publicKey, publicKey,
privateKey, privateKey
}: encryptAsymmetricProps): { }: EncryptAsymmetricProps): {
ciphertext: string; ciphertext: string;
nonce: string; nonce: string;
} => { } => {
@@ -37,11 +38,11 @@ const encryptAssymmetric = ({
return { return {
ciphertext: nacl.util.encodeBase64(ciphertext), ciphertext: nacl.util.encodeBase64(ciphertext),
nonce: nacl.util.encodeBase64(nonce), nonce: nacl.util.encodeBase64(nonce)
}; };
}; };
type decryptAsymmetricProps = { type DecryptAsymmetricProps = {
ciphertext: string; ciphertext: string;
nonce: string; nonce: string;
publicKey: string; publicKey: string;
@@ -61,8 +62,8 @@ const decryptAssymmetric = ({
ciphertext, ciphertext,
nonce, nonce,
publicKey, publicKey,
privateKey, privateKey
}: decryptAsymmetricProps): string => { }: DecryptAsymmetricProps): string => {
const plaintext = nacl.box.open( const plaintext = nacl.box.open(
nacl.util.decodeBase64(ciphertext), nacl.util.decodeBase64(ciphertext),
nacl.util.decodeBase64(nonce), nacl.util.decodeBase64(nonce),
@@ -73,24 +74,23 @@ const decryptAssymmetric = ({
return nacl.util.encodeUTF8(plaintext); return nacl.util.encodeUTF8(plaintext);
}; };
type encryptSymmetricProps = { type EncryptSymmetricProps = {
plaintext: string; plaintext: string;
key: string; key: string;
}; };
type encryptSymmetricReturn = { type EncryptSymmetricReturn = {
ciphertext:string; ciphertext: string;
iv:string; iv: string;
tag:string; tag: string;
}; };
/** /**
* Return symmetrically encrypted [plaintext] using [key]. * Return symmetrically encrypted [plaintext] using [key].
*/ */
const encryptSymmetric = ({ const encryptSymmetric = ({ plaintext, key }: EncryptSymmetricProps): EncryptSymmetricReturn => {
plaintext, let ciphertext;
key, let iv;
}: encryptSymmetricProps): encryptSymmetricReturn => { let tag;
let ciphertext, iv, tag;
try { try {
const obj = aes.encrypt({ text: plaintext, secret: key }); const obj = aes.encrypt({ text: plaintext, secret: key });
ciphertext = obj.ciphertext; ciphertext = obj.ciphertext;
@@ -105,11 +105,11 @@ const encryptSymmetric = ({
return { return {
ciphertext, ciphertext,
iv, iv,
tag, tag
}; };
}; };
type decryptSymmetricProps = { type DecryptSymmetricProps = {
ciphertext: string; ciphertext: string;
iv: string; iv: string;
tag: string; tag: string;
@@ -126,12 +126,7 @@ type decryptSymmetricProps = {
* @param {String} obj.key - 32-byte hex key * @param {String} obj.key - 32-byte hex key
* *
*/ */
const decryptSymmetric = ({ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
ciphertext,
iv,
tag,
key,
}: decryptSymmetricProps): string => {
let plaintext; let plaintext;
try { try {
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key }); plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
@@ -143,9 +138,4 @@ const decryptSymmetric = ({
return plaintext; return plaintext;
}; };
export { export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
decryptAssymmetric,
decryptSymmetric,
encryptAssymmetric,
encryptSymmetric,
};

View File

@@ -1,15 +1,15 @@
import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey'; /* eslint-disable new-cap */
import SRP1 from '~/pages/api/auth/SRP1'; import crypto from 'crypto';
import issueBackupPrivateKey from '@app/pages/api/auth/IssueBackupPrivateKey';
import SRP1 from '@app/pages/api/auth/SRP1';
import jsrp from 'jsrp';
import generateBackupPDF from '../generateBackupPDF'; import generateBackupPDF from '../generateBackupPDF';
import Aes256Gcm from './aes-256-gcm'; import Aes256Gcm from './aes-256-gcm';
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
const jsrp = require('jsrp');
const clientPassword = new jsrp.client(); const clientPassword = new jsrp.client();
const clientKey = new jsrp.client(); const clientKey = new jsrp.client();
const crypto = require('crypto');
interface BackupKeyProps { interface BackupKeyProps {
email: string; email: string;
@@ -42,15 +42,16 @@ const issueBackupKey = async ({
clientPassword.init( clientPassword.init(
{ {
username: email, username: email,
password: password password
}, },
async () => { async () => {
const clientPublicKey = clientPassword.getPublicKey(); const clientPublicKey = clientPassword.getPublicKey();
let serverPublicKey, salt; let serverPublicKey;
let salt;
try { try {
const res = await SRP1({ const res = await SRP1({
clientPublicKey: clientPublicKey clientPublicKey
}); });
serverPublicKey = res.serverPublicKey; serverPublicKey = res.serverPublicKey;
salt = res.salt; salt = res.salt;
@@ -87,9 +88,9 @@ const issueBackupKey = async ({
clientProof clientProof
}); });
if (res?.status == 400) { if (res?.status === 400) {
setBackupKeyError(true); setBackupKeyError(true);
} else if (res?.status == 200) { } else if (res?.status === 200) {
generateBackupPDF({ generateBackupPDF({
personalName, personalName,
personalEmail: email, personalEmail: email,

File diff suppressed because one or more lines are too long

View File

@@ -29,6 +29,7 @@ export function parseDotEnv(src: ArrayBuffer) {
let match; let match;
let item: [string, string, string[]] | [] = []; let item: [string, string, string[]] | [] = [];
// eslint-disable-next-line no-cond-assign
while ((match = LINE.exec(line)) !== null) { while ((match = LINE.exec(line)) !== null) {
const key = match[1]; const key = match[1];
@@ -58,8 +59,8 @@ export function parseDotEnv(src: ArrayBuffer) {
}) })
.filter((line) => line.length > 1) .filter((line) => line.length > 1)
.forEach((line) => { .forEach((line) => {
const [key, value, comments] = line; const [key, value, cmnts] = line;
object[key as string] = { value, comments }; object[key as string] = { value, comments: cmnts };
}); });
return object; return object;

View File

@@ -3,23 +3,11 @@
* @returns * @returns
*/ */
const guidGenerator = () => { const guidGenerator = () => {
const S4 = function () { const S4 = () => {
// eslint-disable-next-line no-bitwise
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}; };
return ( return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
S4() +
S4() +
'-' +
S4() +
'-' +
S4() +
'-' +
S4() +
'-' +
S4() +
S4() +
S4()
);
}; };
export default guidGenerator; export default guidGenerator;

View File

@@ -22,7 +22,7 @@ export const saveTokenToLocalStorage = ({
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
throw new Error( throw new Error(
"Unable to send the tokens in local storage:" + err.message `Unable to send the tokens in local storage:${ err.message}`
); );
} }
} }

View File

@@ -1,26 +1,32 @@
import { SecretDataProps } from "public/data/frequentInterfaces"; import { SecretDataProps } from 'public/data/frequentInterfaces';
/** /**
* This function downloads the secrets as a .env file * This function downloads the secrets as a .env file
* @param {object} obj * @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides * @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
* @returns * @returns
*/ */
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => { const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => {
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret)); let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter( const overridenSecrets = data!.filter((secret) =>
(secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal' secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
: 'personal'
); );
if (overridenSecrets.length) { if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => { overridenSecrets.forEach((secret) => {
const index = secrets!.findIndex( const index = secrets!.findIndex(
(_secret) => _secret.key === secret.key && (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) (_secret) =>
_secret.key === secret.key &&
(secret.valueOverride === undefined || secret?.value !== secret?.valueOverride)
); );
secrets![index].value = secret.value; secrets![index].value = secret.value;
}); });
secrets = secrets!.filter((secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride)); secrets = secrets!.filter(
(secret) => secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
);
} }
return secrets; return secrets;
} };
export default checkOverrides; export default checkOverrides;

View File

@@ -1,16 +1,14 @@
import { envMapping } from "public/data/frequentConstants"; import { SecretDataProps } from 'public/data/frequentInterfaces';
import { SecretDataProps } from "public/data/frequentInterfaces";
import checkOverrides from './checkOverrides'; import checkOverrides from './checkOverrides';
/** /**
* This function downloads the secrets as a .env file * This function downloads the secrets as a .env file
* @param {object} obj * @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to download * @param {SecretDataProps[]} obj.data - secrets that we want to download
* @param {string} obj.env - the environment which we're downloading (used for naming the file) * @param {string} obj.env - the environment which we're downloading (used for naming the file)
*/ */
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => { const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
if (!data) return; if (!data) return;
const secrets = await checkOverrides({ data }); const secrets = await checkOverrides({ data });
@@ -19,21 +17,21 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
(item: SecretDataProps) => (item: SecretDataProps) =>
`${ `${
item.comment item.comment
? item.comment ? `${item.comment
.split('\n') .split('\n')
.map((comment) => '# '.concat(comment)) .map((comment) => '# '.concat(comment))
.join('\n') + '\n' .join('\n')}\n`
: '' : ''
}` + [item.key, item.value].join('=') }${[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);
const alink = document.createElement('a'); const alink = document.createElement('a');
alink.href = fileDownloadUrl; alink.href = fileDownloadUrl;
alink.download = env + '.env'; alink.download = `${env}.env`;
alink.click(); alink.click();
} };
export default downloadDotEnv; export default downloadDotEnv;

View File

@@ -1,20 +1,20 @@
// import YAML from 'yaml'; // import YAML from 'yaml';
// import { YAMLSeq } from 'yaml/types'; // import { YAMLSeq } from 'yaml/types';
import { SecretDataProps } from "public/data/frequentInterfaces"; import { SecretDataProps } from 'public/data/frequentInterfaces';
// import { envMapping } from "../../../public/data/frequentConstants"; // import { envMapping } from "../../../public/data/frequentConstants";
// import checkOverrides from './checkOverrides'; // import checkOverrides from './checkOverrides';
/** /**
* This function downloads the secrets as a .yml file * This function downloads the secrets as a .yml file
* @param {object} obj * @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to download * @param {SecretDataProps[]} obj.data - secrets that we want to download
* @param {string} obj.env - used for naming the file * @param {string} obj.env - used for naming the file
* @returns * @returns
*/ */
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
// if (!data) return; // if (!data) return;
// const doc = new YAML.Document(); // const doc = new YAML.Document();
// doc.contents = new YAMLSeq(); // doc.contents = new YAMLSeq();
@@ -27,20 +27,17 @@ const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: strin
// .join('\n'); // .join('\n');
// doc.add(pair); // doc.add(pair);
// }); // });
// const file = doc // const file = doc
// .toString() // .toString()
// .split('\n') // .split('\n')
// .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line)) // .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
// .join('\n'); // .join('\n');
// const blob = new Blob([file]); // const blob = new Blob([file]);
// const fileDownloadUrl = URL.createObjectURL(blob); // const fileDownloadUrl = URL.createObjectURL(blob);
// const alink = document.createElement('a'); // const alink = document.createElement('a');
// alink.href = fileDownloadUrl; // alink.href = fileDownloadUrl;
// alink.download = envMapping[env] + '.yml'; // alink.download = envMapping[env] + '.yml';
// alink.click(); // alink.click();
return; };
}
export default downloadYaml; export default downloadYaml;

View File

@@ -1,15 +1,9 @@
import { SecretDataProps } from "public/data/frequentInterfaces"; import crypto from 'crypto';
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import { SecretDataProps } from 'public/data/frequentInterfaces';
const crypto = require("crypto");
const {
decryptAssymmetric,
encryptSymmetric,
} = require("../cryptography/crypto");
const nacl = require("tweetnacl");
nacl.util = require("tweetnacl-util");
import { decryptAssymmetric, encryptSymmetric } from '../cryptography/crypto';
interface EncryptedSecretProps { interface EncryptedSecretProps {
id: string; id: string;
@@ -24,22 +18,30 @@ interface EncryptedSecretProps {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
type: "personal" | "shared"; type: 'personal' | 'shared';
} }
/** /**
* Encypt secrets before pushing the to the DB * Encypt secrets before pushing the to the DB
* @param {object} obj * @param {object} obj
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt * @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets * @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
* @returns * @returns
*/ */
const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsToEncrypt: SecretDataProps[]; workspaceId: string; env: string; }) => { const encryptSecrets = async ({
secretsToEncrypt,
workspaceId,
env
}: {
secretsToEncrypt: SecretDataProps[];
workspaceId: string;
env: string;
}) => {
let secrets; let secrets;
try { try {
const sharedKey = await getLatestFileKey({ workspaceId }); const sharedKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
let randomBytes: string; let randomBytes: string;
if (Object.keys(sharedKey).length > 0) { if (Object.keys(sharedKey).length > 0) {
@@ -48,42 +50,42 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
ciphertext: sharedKey.latestKey.encryptedKey, ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce, nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey, publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY, privateKey: PRIVATE_KEY
}); });
} else { } else {
// case: a (shared) key does not exist for the workspace // case: a (shared) key does not exist for the workspace
randomBytes = crypto.randomBytes(16).toString("hex"); randomBytes = crypto.randomBytes(16).toString('hex');
} }
secrets = secretsToEncrypt.map((secret) => { secrets = secretsToEncrypt.map((secret) => {
// encrypt key // encrypt key
const { const {
ciphertext: secretKeyCiphertext, ciphertext: secretKeyCiphertext,
iv: secretKeyIV, iv: secretKeyIV,
tag: secretKeyTag, tag: secretKeyTag
} = encryptSymmetric({ } = encryptSymmetric({
plaintext: secret.key, plaintext: secret.key,
key: randomBytes, key: randomBytes
}); });
// encrypt value // encrypt value
const { const {
ciphertext: secretValueCiphertext, ciphertext: secretValueCiphertext,
iv: secretValueIV, iv: secretValueIV,
tag: secretValueTag, tag: secretValueTag
} = encryptSymmetric({ } = encryptSymmetric({
plaintext: secret.value, plaintext: secret.value,
key: randomBytes, key: randomBytes
}); });
// encrypt comment // encrypt comment
const { const {
ciphertext: secretCommentCiphertext, ciphertext: secretCommentCiphertext,
iv: secretCommentIV, iv: secretCommentIV,
tag: secretCommentTag, tag: secretCommentTag
} = encryptSymmetric({ } = encryptSymmetric({
plaintext: secret.comment ?? '', plaintext: secret.comment ?? '',
key: randomBytes, key: randomBytes
}); });
const result: EncryptedSecretProps = { const result: EncryptedSecretProps = {
@@ -99,17 +101,19 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal', type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
: 'personal'
}; };
return result; return result;
}); });
} catch (error) { } catch (error) {
console.log("Error while encrypting secrets"); console.log('Error while encrypting secrets');
} }
return secrets; return secrets;
};
}
export default encryptSecrets; export default encryptSecrets;

View File

@@ -1,12 +1,7 @@
import getSecrets from '~/pages/api/files/GetSecrets'; import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey'; import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
const { import { decryptAssymmetric, decryptSymmetric } from '../cryptography/crypto';
decryptAssymmetric,
decryptSymmetric
} = require('../cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface EncryptedSecretProps { interface EncryptedSecretProps {
_id: string; _id: string;
@@ -21,14 +16,14 @@ interface EncryptedSecretProps {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
type: "personal" | "shared"; type: 'personal' | 'shared';
} }
interface SecretProps { interface SecretProps {
key: string; key: string;
value: string; value: string;
type: 'personal' | 'shared'; type: 'personal' | 'shared';
comment: string; comment: string;
id: string; id: string;
} }
@@ -61,11 +56,11 @@ const getSecretsForProject = async ({
console.log('ERROR: Not able to access the latest version of secrets'); console.log('ERROR: Not able to access the latest version of secrets');
} }
const latestKey = await getLatestFileKey({ workspaceId }) const latestKey = await getLatestFileKey({ workspaceId });
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs // This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
setIsKeyAvailable(!latestKey ? encryptedSecrets.length == 0 : true); setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
const tempDecryptedSecrets: SecretProps[] = []; const tempDecryptedSecrets: SecretProps[] = [];
if (latestKey) { if (latestKey) {
@@ -78,7 +73,7 @@ const getSecretsForProject = async ({
}); });
// decrypt secret keys, values, and comments // decrypt secret keys, values, and comments
encryptedSecrets.map((secret: EncryptedSecretProps) => { encryptedSecrets.forEach((secret: EncryptedSecretProps) => {
const plainTextKey = decryptSymmetric({ const plainTextKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
@@ -102,7 +97,7 @@ const getSecretsForProject = async ({
key key
}); });
} else { } else {
plainTextComment = ""; plainTextComment = '';
} }
tempDecryptedSecrets.push({ tempDecryptedSecrets.push({
@@ -115,20 +110,26 @@ const getSecretsForProject = async ({
}); });
} }
const secretKeys = [...new Set(tempDecryptedSecrets.map(secret => secret.key))]; const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))];
const result = secretKeys.map((key, index) => ({
const result = secretKeys.map((key, index) => { id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
return { ?.id,
id: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.id, idOverride: tempDecryptedSecrets.filter(
idOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.id, (secret) => secret.key === key && secret.type === 'personal'
pos: index, )[0]?.id,
key: key, pos: index,
value: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.value, key,
valueOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.value, value: tempDecryptedSecrets.filter(
comment: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.comment, (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); setData(result);
return result; return result;

View File

@@ -1,7 +1,7 @@
/* eslint-disable */ /* eslint-disable */
import { PostHog } from 'posthog-js'; import { PostHog } from 'posthog-js';
import { initPostHog } from '~/components/analytics/posthog'; import { initPostHog } from '@app/components/analytics/posthog';
import { ENV } from '~/components/utilities/config'; import { ENV } from '@app/components/utilities/config';
declare let TELEMETRY_CAPTURING_ENABLED: any; declare let TELEMETRY_CAPTURING_ENABLED: any;
@@ -13,7 +13,7 @@ class Capturer {
} }
capture(item: string) { capture(item: string) {
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
try { try {
this.api.capture(item); this.api.capture(item);
} catch (error) { } catch (error) {
@@ -23,7 +23,7 @@ class Capturer {
} }
identify(id: string) { identify(id: string) {
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) { if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
try { try {
this.api.identify(id); this.api.identify(id);
} catch (error) { } catch (error) {

View File

@@ -1,8 +1,7 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface WorkspaceProps {
interface workspaceProps { actionId: string;
actionId: string;
} }
/** /**
@@ -11,21 +10,18 @@ interface workspaceProps {
* @param {string} obj.actionId - id of an action for which we are trying to get data * @param {string} obj.actionId - id of an action for which we are trying to get data
* @returns * @returns
*/ */
const getActionData = async ({ actionId }: workspaceProps) => { const getActionData = async ({ actionId }: WorkspaceProps) =>
return SecurityClient.fetchCall( SecurityClient.fetchCall(`/api/v1/action/${actionId}`, {
'/api/v1/action/' + actionId, { method: 'GET',
method: 'GET', headers: {
headers: { 'Content-Type': 'application/json'
'Content-Type': 'application/json'
}
} }
).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return (await res.json()).action; return (await res.json()).action;
} else {
console.log('Failed to get the info about an action');
} }
console.log('Failed to get the info about an action');
return undefined;
}); });
};
export default getActionData; export default getActionData;

View File

@@ -1,8 +1,7 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface WorkspaceProps {
interface workspaceProps { workspaceId: string;
workspaceId: string;
offset: number; offset: number;
limit: number; limit: number;
userId: string; userId: string;
@@ -12,48 +11,53 @@ interface workspaceProps {
/** /**
* This function fetches the activity logs for a certain project * This function fetches the activity logs for a certain project
* @param {object} obj * @param {object} obj
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log * @param {string} obj.workspaceId - workspace id for which we are trying to get project log
* @param {object} obj.offset - teh starting point of logs that we want to pull * @param {object} obj.offset - teh starting point of logs that we want to pull
* @param {object} obj.limit - how many logs will we output * @param {object} obj.limit - how many logs will we output
* @param {object} obj.userId - optional userId filter - will only query logs for that user * @param {object} obj.userId - optional userId filter - will only query logs for that user
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions * @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
* @returns * @returns
*/ */
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => { const getProjectLogs = async ({
workspaceId,
offset,
limit,
userId,
actionNames
}: WorkspaceProps) => {
let payload; let payload;
if (userId != "" && actionNames != '') { if (userId !== '' && actionNames !== '') {
payload = { payload = {
offset: String(offset), offset: String(offset),
limit: String(limit), limit: String(limit),
sortBy: 'recent', sortBy: 'recent',
userId: JSON.stringify(userId), userId: JSON.stringify(userId),
actionNames: actionNames actionNames
} };
} else if (userId != "") { } else if (userId !== '') {
payload = { payload = {
offset: String(offset), offset: String(offset),
limit: String(limit), limit: String(limit),
sortBy: 'recent', sortBy: 'recent',
userId: JSON.stringify(userId) userId: JSON.stringify(userId)
} };
} else if (actionNames != "") { } else if (actionNames !== '') {
payload = { payload = {
offset: String(offset), offset: String(offset),
limit: String(limit), limit: String(limit),
sortBy: 'recent', sortBy: 'recent',
actionNames: actionNames actionNames
} };
} else { } else {
payload = { payload = {
offset: String(offset), offset: String(offset),
limit: String(limit), limit: String(limit),
sortBy: 'recent' sortBy: 'recent'
} };
} }
return SecurityClient.fetchCall( return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/logs?' + `/api/v1/workspace/${workspaceId}/logs?${new URLSearchParams(payload)}`,
new URLSearchParams(payload),
{ {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -61,11 +65,11 @@ const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames
} }
} }
).then(async (res) => { ).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return (await res.json()).logs; return (await res.json()).logs;
} else {
console.log('Failed to get project logs');
} }
console.log('Failed to get project logs');
return undefined;
}); });
}; };

View File

@@ -1,8 +1,7 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface WorkspaceProps {
interface workspaceProps { workspaceId: string;
workspaceId: string;
offset: number; offset: number;
limit: number; limit: number;
} }
@@ -10,30 +9,29 @@ interface workspaceProps {
/** /**
* This function fetches the secret snapshots for a certain project * This function fetches the secret snapshots for a certain project
* @param {object} obj * @param {object} obj
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots * @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
* @param {object} obj.offset - teh starting point of snapshots that we want to pull * @param {object} obj.offset - teh starting point of snapshots that we want to pull
* @param {object} obj.limit - how many snapshots will we output * @param {object} obj.limit - how many snapshots will we output
* @returns * @returns
*/ */
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => { const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: WorkspaceProps) =>
return SecurityClient.fetchCall( SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' + `/api/v1/workspace/${workspaceId}/secret-snapshots?${new URLSearchParams({
new URLSearchParams({
offset: String(offset), offset: String(offset),
limit: String(limit) limit: String(limit)
}), { })}`,
{
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
} }
).then(async (res) => { ).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return (await res.json()).secretSnapshots; return (await res.json()).secretSnapshots;
} else {
console.log('Failed to get project secret snapshots');
} }
console.log('Failed to get project secret snapshots');
return undefined;
}); });
};
export default getProjectSecretShanpshots; export default getProjectSecretShanpshots;

View File

@@ -1,31 +1,27 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface WorkspaceProps {
interface workspaceProps { workspaceId: string;
workspaceId: string;
} }
/** /**
* This function fetches the count of secret snapshots for a certain project * This function fetches the count of secret snapshots for a certain project
* @param {object} obj * @param {object} obj
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots * @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
* @returns * @returns
*/ */
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => { const getProjectSercetSnapshotsCount = async ({ workspaceId }: WorkspaceProps) =>
return SecurityClient.fetchCall( SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/count`, {
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', { method: 'GET',
method: 'GET', headers: {
headers: { 'Content-Type': 'application/json'
'Content-Type': 'application/json'
}
} }
).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return (await res.json()).count; return (await res.json()).count;
} else {
console.log('Failed to get the count of project secret snapshots');
} }
console.log('Failed to get the count of project secret snapshots');
return undefined;
}); });
};
export default getProjectSercetSnapshotsCount; export default getProjectSercetSnapshotsCount;

View File

@@ -1,8 +1,7 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface SnapshotProps { interface SnapshotProps {
secretSnapshotId: string; secretSnapshotId: string;
} }
/** /**
@@ -11,21 +10,18 @@ interface SnapshotProps {
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets * @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
* @returns * @returns
*/ */
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => { const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) =>
return SecurityClient.fetchCall( SecurityClient.fetchCall(`/api/v1/secret-snapshot/${secretSnapshotId}`, {
'/api/v1/secret-snapshot/' + secretSnapshotId, { method: 'GET',
method: 'GET', headers: {
headers: { 'Content-Type': 'application/json'
'Content-Type': 'application/json'
}
} }
).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return (await res.json()).secretSnapshot; return (await res.json()).secretSnapshot;
} else {
console.log('Failed to get the secrets of a certain snapshot');
} }
console.log('Failed to get the secrets of a certain snapshot');
return undefined;
}); });
};
export default getSecretSnapshotData; export default getSecretSnapshotData;

View File

@@ -1,9 +1,8 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface SecretVersionProps {
interface secretVersionProps { secretId: string;
secretId: string; offset: number;
offset: number;
limit: number; limit: number;
} }
@@ -15,13 +14,12 @@ interface secretVersionProps {
* @param {number} obj.limit - how far our query goes * @param {number} obj.limit - how far our query goes
* @returns * @returns
*/ */
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => { const getSecretVersions = async ({ secretId, offset, limit }: SecretVersionProps) =>
return SecurityClient.fetchCall( SecurityClient.fetchCall(
'/api/v1/secret/' + secretId + '/secret-versions?' + `/api/v1/secret/${secretId}/secret-versions?${new URLSearchParams({
new URLSearchParams({ offset: String(offset),
offset: String(offset), limit: String(limit)
limit: String(limit) })}`,
}),
{ {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -29,12 +27,11 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
} }
} }
).then(async (res) => { ).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return await res.json(); return res.json();
} else {
console.log('Failed to get secret version history');
} }
console.log('Failed to get secret version history');
return undefined;
}); });
};
export default getSecretVersions; export default getSecretVersions;

View File

@@ -1,4 +1,4 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
/** /**
* This function performs a rollback of secrets in a certain project * This function performs a rollback of secrets in a certain project
@@ -7,24 +7,27 @@ import SecurityClient from '~/utilities/SecurityClient';
* @param {number} obj.version - version to which we are rolling back * @param {number} obj.version - version to which we are rolling back
* @returns * @returns
*/ */
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => { const performSecretRollback = async ({
return SecurityClient.fetchCall( workspaceId,
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", { version
method: 'POST', }: {
headers: { workspaceId: string;
'Content-Type': 'application/json' version: number;
}, }) =>
body: JSON.stringify({ SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`, {
version method: 'POST',
}) headers: {
} 'Content-Type': 'application/json'
).then(async (res) => { },
if (res && res.status == 200) { body: JSON.stringify({
return (await res.json()); version
} else { })
console.log('Failed to perform the secret rollback'); }).then(async (res) => {
if (res && res.status === 200) {
return res.json();
} }
console.log('Failed to perform the secret rollback');
return undefined;
}); });
};
export default performSecretRollback; export default performSecretRollback;

View File

@@ -1,27 +1,21 @@
import { useEffect, useState } from "react"; import { 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 { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import getActionData from "@app/ee/api/secrets/GetActionData"; import getActionData from '@app/ee/api/secrets/GetActionData';
import patienceDiff from '@app/ee/utilities/findTextDifferences'; import patienceDiff from '@app/ee/utilities/findTextDifferences';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import DashboardInputField from '../../components/dashboard/DashboardInputField'; import DashboardInputField from '../../components/dashboard/DashboardInputField';
import {
const {
decryptAssymmetric, decryptAssymmetric,
decryptSymmetric decryptSymmetric
} = require('../../components/utilities/cryptography/crypto'); } from '../../components/utilities/cryptography/crypto';
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
interface SideBarProps { interface SideBarProps {
toggleSidebar: (value: string) => void; toggleSidebar: (value: string) => void;
currentAction: string; currentAction: string;
} }
@@ -41,11 +35,11 @@ interface DecryptedSecretProps {
newSecretVersion: { newSecretVersion: {
key: string; key: string;
value: string; value: string;
} };
oldSecretVersion: { oldSecretVersion: {
key: string; key: string;
value: string; value: string;
} };
} }
interface ActionProps { interface ActionProps {
@@ -58,10 +52,7 @@ interface ActionProps {
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed * @param {string} obj.currentAction - the action id for which a sidebar is being displayed
* @returns the sidebar with the payload of user activity logs * @returns the sidebar with the payload of user activity logs
*/ */
const ActivitySideBar = ({ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
toggleSidebar,
currentAction
}: SideBarProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const [actionData, setActionData] = useState<DecryptedSecretProps[]>(); const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
@@ -72,7 +63,7 @@ const ActivitySideBar = ({
const getLogData = async () => { const getLogData = async () => {
setIsLoading(true); setIsLoading(true);
const tempActionData = await getActionData({ actionId: currentAction }); const tempActionData = await getActionData({ actionId: currentAction });
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
// #TODO: make this a separate function and reuse across the app // #TODO: make this a separate function and reuse across the app
@@ -86,12 +77,12 @@ const ActivitySideBar = ({
privateKey: String(PRIVATE_KEY) privateKey: String(PRIVATE_KEY)
}); });
} }
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: { const decryptedSecretVersions = tempActionData.payload.secretVersions.map(
newSecretVersion?: SecretProps; (encryptedSecretVersion: {
oldSecretVersion?: SecretProps; newSecretVersion?: SecretProps;
}) => { oldSecretVersion?: SecretProps;
return { }) => ({
newSecretVersion: { newSecretVersion: {
key: decryptSymmetric({ key: decryptSymmetric({
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext, ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
@@ -107,79 +98,134 @@ const ActivitySideBar = ({
}) })
}, },
oldSecretVersion: { oldSecretVersion: {
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
? decryptSymmetric({ ? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext, ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV, iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag, tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
key: decryptedLatestKey key: decryptedLatestKey
}): undefined, })
: undefined,
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
? decryptSymmetric({ ? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext, ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV, iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag, tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
key: decryptedLatestKey key: decryptedLatestKey
}): undefined })
: undefined
} }
} })
}) );
setActionData(decryptedSecretVersions); setActionData(decryptedSecretVersions);
setActionMetaData({name: tempActionData.name}); setActionMetaData({ name: tempActionData.name });
setIsLoading(false); setIsLoading(false);
} };
getLogData(); getLogData();
}, [currentAction]); }, [currentAction]);
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`}> return (
{isLoading ? ( <div
<div className="flex items-center justify-center h-full mb-8"> className={`absolute border-l border-mineshaft-500 ${
<Image isLoading ? 'bg-bunker-800' : 'bg-bunker'
src="/images/loading/loading.gif" } fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
height={60} >
width={100} {isLoading ? (
alt="infisical loading indicator" <div className="flex items-center justify-center h-full mb-8">
></Image> <Image
</div> src="/images/loading/loading.gif"
) : ( height={60}
<div className='h-min overflow-y-auto'> width={100}
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center"> alt="infisical loading indicator"
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p> />
<div className='p-1' onClick={() => toggleSidebar("")}> </div>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/> ) : (
<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">
{t(`activity:event.${actionMetaData?.name}`)}
</p>
<div
className="p-1"
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => toggleSidebar('')}
>
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div>
</div>
<div className="flex flex-col px-4">
{(actionMetaData?.name === 'readSecrets' ||
actionMetaData?.name === 'addSecrets' ||
actionMetaData?.name === 'deleteSecrets') &&
actionData?.map((item, id) => (
<div key={`secret.${id + 1}`}>
<div className="text-xs text-bunker-200 mt-4 pl-1">
{item.newSecretVersion.key}
</div>
<DashboardInputField
onChangeHandler={() => {}}
type="value"
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
</div>
))}
{actionMetaData?.name === 'updateSecrets' &&
actionData?.map((item, id) => (
<>
<div className="text-xs text-bunker-200 mt-4 pl-1">
{item.newSecretVersion.key}
</div>
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
<div className="bg-red/30 px-2">
-{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
item.newSecretVersion.value.split(''),
false
).lines.map(
(character, lineId) =>
character.bIndex !== -1 && (
<span
key={`actionData.${id + 1}.line.${lineId + 1}`}
className={`${character.aIndex === -1 && 'bg-red-700/80'}`}
>
{character.line}
</span>
)
)}
</div>
<div className="bg-green-500/30 px-2">
+{' '}
{patienceDiff(
item.oldSecretVersion.value.split(''),
item.newSecretVersion.value.split(''),
false
).lines.map(
(character, lineId) =>
character.aIndex !== -1 && (
<span
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
className={`${character.bIndex === -1 && 'bg-green-700/80'}`}
>
{character.line}
</span>
)
)}
</div>
</div>
</>
))}
</div> </div>
</div> </div>
<div className='flex flex-col px-4'> )}
{(actionMetaData?.name == 'readSecrets' </div>
|| actionMetaData?.name == 'addSecrets' );
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
<div key={id}>
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
<DashboardInputField
key={id}
onChangeHandler={() => {}}
type="value"
position={1}
value={item.newSecretVersion.value}
isDuplicate={false}
blurred={false}
/>
</div>
)}
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
<>
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
</div>
</>
)}
</div>
</div>
)}
</div>
}; };
export default ActivitySideBar; export default ActivitySideBar;

View File

@@ -1,24 +1,20 @@
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import React, { useState } from 'react'; import React, { useState } from 'react';
import Image from 'next/image'; import Image from 'next/image';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import timeSince from '@app/ee/utilities/timeSince'; import timeSince from '@app/ee/utilities/timeSince';
import { import { faAngleDown, faAngleRight, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
faAngleDown,
faAngleRight,
faUpRightFromSquare
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../../components/utilities/randomId'; import guidGenerator from '../../components/utilities/randomId';
interface PayloadProps { interface PayloadProps {
_id: string; _id: string;
name: string; name: string;
secretVersions: string[]; secretVersions: string[];
} }
interface logData { interface LogData {
_id: string; _id: string;
channel: string; channel: string;
createdAt: string; createdAt: string;
@@ -27,15 +23,20 @@ interface logData {
payload: PayloadProps[]; payload: PayloadProps[];
} }
/** /**
* This is a single row of the activity table * This is a single row of the activity table
* @param obj * @param obj
* @param {logData} obj.row - data for a certain event * @param {LogData} obj.row - data for a certain event
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event * @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
* @returns * @returns
*/ */
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => { const ActivityLogsRow = ({
row,
toggleSidebar
}: {
row: LogData;
toggleSidebar: (value: string) => void;
}) => {
const [payloadOpened, setPayloadOpened] = useState(false); const [payloadOpened, setPayloadOpened] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -43,6 +44,7 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
<> <>
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm"> <tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
<td <td
onKeyDown={() => null}
onClick={() => setPayloadOpened(!payloadOpened)} onClick={() => setPayloadOpened(!payloadOpened)}
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
> >
@@ -54,40 +56,58 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
/> />
</td> </td>
<td className="py-3 border-mineshaft-700 border-t text-gray-300"> <td className="py-3 border-mineshaft-700 border-t text-gray-300">
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")} {row.payload
</td> ?.map(
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300"> (action) =>
{row.user} `${String(action.secretVersions.length)} ${t(`activity:event.${action.name}`)}`
</td> )
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300"> .join(' and ')}
{row.channel}
</td> </td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.user}</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.channel}</td>
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300"> <td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
{timeSince(new Date(row.createdAt))} {timeSince(new Date(row.createdAt))}
</td> </td>
</tr> </tr>
{payloadOpened && {payloadOpened && (
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'> <tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td></td> <td />
<td>{String(t("common:timestamp"))}</td> <td>{String(t('common:timestamp'))}</td>
<td>{row.createdAt}</td> <td>{row.createdAt}</td>
</tr>}
{payloadOpened && row.payload?.map((action, index) => {
return action.secretVersions.length > 0 && <tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td></td>
<td className="">{t("activity:event." + action.name)}</td>
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
</td>
</tr> </tr>
})} )}
{payloadOpened && {payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'> row.payload?.map(
<td></td> (action) =>
<td>{String(t("activity:ip-address"))}</td> action.secretVersions.length > 0 && (
<td>{row.ipAddress}</td> <tr
</tr>} key={action._id}
className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm"
>
<td />
<td className="">{t(`activity:event.${action.name}`)}</td>
<td
onKeyDown={() => null}
className="text-primary-300 cursor-pointer hover:text-primary duration-200"
onClick={() => toggleSidebar(action._id)}
>
{action.secretVersions.length +
(action.secretVersions.length !== 1 ? ' secrets' : ' secret')}
<FontAwesomeIcon
icon={faUpRightFromSquare}
className="ml-2 mb-0.5 font-light w-3 h-3"
/>
</td>
</tr>
)
)}
{payloadOpened && (
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td />
<td>{String(t('activity:ip-address'))}</td>
<td>{row.ipAddress}</td>
</tr>
)}
</> </>
); );
}; };
@@ -100,37 +120,61 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not * @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
* @returns * @returns
*/ */
const ActivityTable = ({ data, toggleSidebar, isLoading }: { data: logData[], toggleSidebar: (value: string) => void; isLoading: boolean; }) => { const ActivityTable = ({
data,
toggleSidebar,
isLoading
}: {
data: LogData[];
toggleSidebar: (value: string) => void;
isLoading: boolean;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="w-full px-6 mt-8"> <div className="w-full px-6 mt-8">
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative"> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div> <div className="absolute rounded-t-md w-full h-[3rem] bg-white/5" />
<table className="w-full my-1"> <table className="w-full my-1">
<thead className="text-bunker-300"> <thead className="text-bunker-300">
<tr className='text-sm'> <tr className="text-sm">
<th className="text-left pl-6 pt-2.5 pb-3"></th> <th aria-label="actions" className="text-left pl-6 pt-2.5 pb-3" />
<th className="text-left font-semibold pt-2.5 pb-3">{String(t("common:event")).toUpperCase()}</th> <th className="text-left font-semibold pt-2.5 pb-3">
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:user")).toUpperCase()}</th> {String(t('common:event')).toUpperCase()}
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:source")).toUpperCase()}</th> </th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:time")).toUpperCase()}</th> <th className="text-left font-semibold pl-6 pt-2.5 pb-3">
<th></th> {String(t('common:user')).toUpperCase()}
</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
{String(t('common:source')).toUpperCase()}
</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
{String(t('common:time')).toUpperCase()}
</th>
<th aria-label="action" />
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data?.map((row, index) => { {data?.map((row, index) => (
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />; <ActivityLogsRow
})} key={`activity.${index + 1}.${row._id}`}
row={row}
toggleSidebar={toggleSidebar}
/>
))}
</tbody> </tbody>
</table> </table>
</div> </div>
{isLoading && <div className='w-full flex justify-center mb-8 mt-4'><Image {isLoading && (
src="/images/loading/loading.gif" <div className="w-full flex justify-center mb-8 mt-4">
height={60} <Image
width={100} src="/images/loading/loading.gif"
alt="loading animation" height={60}
></Image></div>} width={100}
alt="loading animation"
/>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -1,18 +1,20 @@
import { useEffect, useState } from "react"; /* eslint-disable no-nested-ternary */
import Image from "next/image"; import { useEffect, useState } from 'react';
import { useRouter } from "next/router"; import Image from 'next/image';
import { useTranslation } from "next-i18next"; import { useRouter } from 'next/router';
import getProjectSecretShanpshots from "@app/ee/api/secrets/GetProjectSercetShanpshots"; import { useTranslation } from 'next-i18next';
import getSecretSnapshotData from "@app/ee/api/secrets/GetSecretSnapshotData"; import Button from '@app/components/basic/buttons/Button';
import timeSince from "@app/ee/utilities/timeSince"; import {
decryptAssymmetric,
decryptSymmetric
} from '@app/components/utilities/cryptography/crypto';
import getProjectSecretShanpshots from '@app/ee/api/secrets/GetProjectSercetShanpshots';
import getSecretSnapshotData from '@app/ee/api/secrets/GetSecretSnapshotData';
import timeSince from '@app/ee/utilities/timeSince';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import { faX } from '@fortawesome/free-solid-svg-icons'; import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from "~/components/basic/buttons/Button";
import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto";
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
export interface SecretDataProps { export interface SecretDataProps {
pos: number; pos: number;
key: string; key: string;
@@ -23,7 +25,7 @@ export interface SecretDataProps {
} }
interface SideBarProps { interface SideBarProps {
toggleSidebar: (value: boolean) => void; toggleSidebar: (value: boolean) => void;
setSnapshotData: (value: any) => void; setSnapshotData: (value: any) => void;
chosenSnapshot: string; chosenSnapshot: string;
} }
@@ -44,7 +46,7 @@ interface EncrypetedSecretVersionListProps {
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
environment: string; environment: string;
type: "personal" | "shared"; type: 'personal' | 'shared';
} }
/** /**
@@ -54,11 +56,7 @@ interface EncrypetedSecretVersionListProps {
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected * @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
* @returns the sidebar with the options for point-in-time recovery (commits) * @returns the sidebar with the options for point-in-time recovery (commits)
*/ */
const PITRecoverySidebar = ({ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }: SideBarProps) => {
toggleSidebar,
setSnapshotData,
chosenSnapshot
}: SideBarProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const router = useRouter(); const router = useRouter();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -68,22 +66,26 @@ const PITRecoverySidebar = ({
const loadMoreSnapshots = () => { const loadMoreSnapshots = () => {
setCurrentOffset(currentOffset + currentLimit); setCurrentOffset(currentOffset + currentLimit);
} };
useEffect(() => { useEffect(() => {
const getLogData = async () => { const getLogData = async () => {
setIsLoading(true); setIsLoading(true);
const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset }) const results = await getProjectSecretShanpshots({
workspaceId: String(router.query.id),
limit: currentLimit,
offset: currentOffset
});
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results)); setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
setIsLoading(false); setIsLoading(false);
} };
getLogData(); getLogData();
}, [currentOffset]); }, [currentOffset]);
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => { const exploreSnapshot = async ({ snapshotId }: { snapshotId: string }) => {
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId }); const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
let decryptedLatestKey: string; let decryptedLatestKey: string;
@@ -97,10 +99,10 @@ const PITRecoverySidebar = ({
}); });
} }
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => { const decryptedSecretVersions = secretSnapshotData.secretVersions.map(
return { (encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
id: encryptedSecretVersion._id, id: encryptedSecretVersion._id,
pos: pos, pos,
type: encryptedSecretVersion.type, type: encryptedSecretVersion.type,
environment: encryptedSecretVersion.environment, environment: encryptedSecretVersion.environment,
key: decryptSymmetric({ key: decryptSymmetric({
@@ -115,69 +117,132 @@ const PITRecoverySidebar = ({
tag: encryptedSecretVersion.secretValueTag, tag: encryptedSecretVersion.secretValueTag,
key: decryptedLatestKey key: decryptedLatestKey
}) })
} })
}) );
const secretKeys = [
...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))
];
const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))]; const result = secretKeys.map((key, index) => ({
id: decryptedSecretVersions.filter(
const result = secretKeys.map((key, index) => { (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
return { )[0].id,
id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id, pos: index,
pos: index, key,
key: key, environment: decryptedSecretVersions.filter(
environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment, (secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value, )[0].environment,
valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value, 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: ''
}); });
};
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' }) return (
} <div
className={`absolute border-l border-mineshaft-500 ${
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`}> isLoading ? 'bg-bunker-800' : 'bg-bunker'
{isLoading ? ( } fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
<div className="flex items-center justify-center h-full mb-8"> >
<Image {isLoading ? (
src="/images/loading/loading.gif" <div className="flex items-center justify-center h-full mb-8">
height={60} <Image
width={100} src="/images/loading/loading.gif"
alt="infisical loading indicator" height={60}
></Image> width={100}
</div> alt="infisical loading indicator"
) : ( />
<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>
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'> ) : (
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => <div className="h-min">
<div <div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
key={snapshot._id} <p className="font-semibold text-lg text-bunker-200">{t('Point-in-time Recovery')}</p>
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })} <div
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`} onKeyDown={() => null}
role="button"
tabIndex={0}
className="p-1"
onClick={() => toggleSidebar(false)}
> >
<div className="flex flex-row items-start"> <FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
<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>
<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
onKeyDown={() => null}
role="button"
tabIndex={0}
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>
<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`}> <div className="flex justify-center w-full mb-14">
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"} <div className="items-center w-40">
<Button
text="View More"
textDisabled="End of History"
active={secretSnapshotsMetadata.length % 15 === 0}
onButtonPressed={loadMoreSnapshots}
size="md"
color="mineshaft"
/>
</div> </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> </div>
</div> )}
)} </div>
</div> );
}; };
export default PITRecoverySidebar; export default PITRecoverySidebar;

View File

@@ -1,14 +1,16 @@
import { useEffect, useState } from 'react'; import { 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 { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import {
decryptAssymmetric,
decryptSymmetric
} from '@app/components/utilities/cryptography/crypto';
import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions'; import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons'; import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { decryptAssymmetric, decryptSymmetric } from '~/components/utilities/cryptography/crypto';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
interface DecryptedSecretVersionListProps { interface DecryptedSecretVersionListProps {
createdAt: string; createdAt: string;
value: string; value: string;
@@ -21,24 +23,23 @@ interface EncrypetedSecretVersionListProps {
secretValueTag: string; secretValueTag: string;
} }
/** /**
* @param {string} secretId - the id of a secret for which are querying version history * @param {string} secretId - the id of a secret for which are querying version history
* @returns a list of versions for a specific secret * @returns a list of versions for a specific secret
*/ */
const SecretVersionList = ({ secretId }: { secretId: string; }) => { const SecretVersionList = ({ secretId }: { secretId: string }) => {
const router = useRouter(); const router = useRouter();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]); const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
useEffect(() => { useEffect(() => {
const getSecretVersionHistory = async () => { const getSecretVersionHistory = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10}); const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10 });
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
let decryptedLatestKey: string; let decryptedLatestKey: string;
@@ -52,8 +53,8 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
}); });
} }
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => { const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map(
return { (encryptedSecretVersion: EncrypetedSecretVersionListProps) => ({
createdAt: encryptedSecretVersion.createdAt, createdAt: encryptedSecretVersion.createdAt,
value: decryptSymmetric({ value: decryptSymmetric({
ciphertext: encryptedSecretVersion.secretValueCiphertext, ciphertext: encryptedSecretVersion.secretValueCiphertext,
@@ -61,63 +62,76 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
tag: encryptedSecretVersion.secretValueTag, tag: encryptedSecretVersion.secretValueTag,
key: decryptedLatestKey key: decryptedLatestKey
}) })
} })
}) );
setSecretVersions(decryptedSecretVersions); setSecretVersions(decryptedSecretVersions);
setIsLoading(false); setIsLoading(false);
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
}; };
getSecretVersionHistory(); getSecretVersionHistory();
}, [secretId]); }, [secretId]);
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'> return (
<p className=''>{t("dashboard:sidebar.version-history")}</p> <div className="w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'> <p className="">{t('dashboard:sidebar.version-history')}</p>
{isLoading ? ( <div className="p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
<div className="flex items-center justify-center h-full"> {isLoading ? (
<Image <div className="flex items-center justify-center h-full">
src="/images/loading/loading.gif" <Image
height={60} src="/images/loading/loading.gif"
width={100} height={60}
alt="infisical loading indicator" width={100}
></Image> alt="infisical loading indicator"
</div> />
) : ( </div>
<div className='h-48 overflow-y-auto overflow-x-none'> ) : (
{secretVersions <div className="h-48 overflow-y-auto overflow-x-none">
? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt)) {secretVersions ? (
.map((version: DecryptedSecretVersionListProps, index: number) => secretVersions
<div key={index} className='flex flex-row'> ?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
<div className='pr-1 flex flex-col items-center'> .map((version: DecryptedSecretVersionListProps, index: number) => (
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div> <div key={`${version.createdAt}.${index + 1}`} className="flex flex-row">
<div className='w-0 h-full border-l mt-1'></div> <div className="pr-1 flex flex-col items-center">
</div> <div className="p-1">
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'> <FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
<div className='pr-2 pt-1'> </div>
{(new Date(version.createdAt)).toLocaleDateString('en-US', { <div className="w-0 h-full border-l mt-1" />
year: 'numeric', </div>
month: '2-digit', <div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
day: '2-digit', <div className="pr-2 pt-1">
hour: '2-digit', {new Date(version.createdAt).toLocaleDateString('en-US', {
minute: '2-digit', year: 'numeric',
second: '2-digit' 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> </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="w-full h-full flex items-center justify-center text-bunker-400">
No version history yet.
</div> </div>
) )}
: ( </div>
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div> )}
) </div>
}
</div>
)}
</div> </div>
</div> );
}; };
export default SecretVersionList; export default SecretVersionList;

View File

@@ -1,270 +1,284 @@
/* eslint-disable */
// TODO(akhilmhdh): Danger. This file functions needs to simplified.
// Disabled eslinting as below algos uses unnary operator and need to be careful on handling it
// Will revisit this and change to simplier non-recursive to dynamic programming with space allocation strategy
/** /**
* *
* @param textOld - old secret * @param textOld - old secret
* @param textNew - new (updated) secret * @param textNew - new (updated) secret
* @param diffPlusFlag - a flag for whether we want to detect moving segments * @param diffPlusFlag - a flag for whether we want to detect moving segments
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text) * - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
* @returns * @returns
*/ */
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) { function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
/** /**
* findUnique finds all unique values in arr[lo..hi], inclusive. This * findUnique finds all unique values in arr[lo..hi], inclusive. This
* function is used in preparation for determining the longest common * function is used in preparation for determining the longest common
* subsequence. Specifically, it first reduces the array range in question * subsequence. Specifically, it first reduces the array range in question
* to unique values. * to unique values.
* @param chars - an array of characters * @param chars - an array of characters
* @param lo * @param lo
* @param hi * @param hi
* @returns - an ordered Map, with the arr[i] value as the Map key and the * @returns - an ordered Map, with the arr[i] value as the Map key and the
* array index i as the Map value. * array index i as the Map value.
*/ */
function findUnique(chars: string[], lo: number, hi: number) { function findUnique(chars: string[], lo: number, hi: number) {
const characterMap = new Map(); const characterMap = new Map();
for (let i=lo; i<=hi; i++) { for (let i = lo; i <= hi; i += 1) {
const character = chars[i]; const character = chars[i];
if (characterMap.has(character)) {
characterMap.get(character).count++;
characterMap.get(character).index = i;
} else {
characterMap.set(character, { count: 1, index: i });
}
}
characterMap.forEach((val, key, map) => { if (characterMap.has(character)) {
if (val.count !== 1) { characterMap.get(character).count += 1;
map.delete(key); characterMap.get(character).index = i;
} else { } else {
map.set(key, val.index); characterMap.set(character, { count: 1, index: i });
} }
}); }
return characterMap; characterMap.forEach((val, key, map) => {
} if (val.count !== 1) {
map.delete(key);
} else {
map.set(key, val.index);
}
});
return characterMap;
}
/** /**
* @param aArray * @param aArray
* @param aLo * @param aLo
* @param aHi * @param aHi
* @param bArray * @param bArray
* @param bLo * @param bLo
* @param bHi * @param bHi
* @returns an ordered Map, with the Map key as the common line between aArray * @returns an ordered Map, with the Map key as the common line between aArray
* and bArray, with the Map value as an object containing the array indexes of * and bArray, with the Map value as an object containing the array indexes of
* the matching unique lines. * the matching unique lines.
* *
*/ */
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) { function uniqueCommon(
const ma = findUnique(aArray, aLo, aHi); aArray: string[],
const mb = findUnique(bArray, bLo, bHi); aLo: number,
aHi: number,
bArray: string[],
bLo: number,
bHi: number
) {
const ma = findUnique(aArray, aLo, aHi);
const mb = findUnique(bArray, bLo, bHi);
ma.forEach((val, key, map) => { ma.forEach((val, key, map) => {
if (mb.has(key)) { if (mb.has(key)) {
map.set(key, { map.set(key, {
indexA: val, indexA: val,
indexB: mb.get(key) indexB: mb.get(key)
}); });
} else { } else {
map.delete(key); map.delete(key);
} }
}); });
return ma; return ma;
} }
/** /**
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon * longestCommonSubsequence takes an ordered Map from the function uniqueCommon
* and determines the Longest Common Subsequence (LCS). * and determines the Longest Common Subsequence (LCS).
* @param abMap * @param abMap
* @returns an ordered array of objects containing the array indexes of the * @returns an ordered array of objects containing the array indexes of the
* matching lines for a LCS. * matching lines for a LCS.
*/ */
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) { function longestCommonSubsequence(
const ja: any = []; abMap: Map<number, { indexA: number; indexB: number; prev?: number }>
) {
const ja: any = [];
// First, walk the list creating the jagged array. // First, walk the list creating the jagged array.
abMap.forEach((val, key, map) => { abMap.forEach((val, key, map) => {
let i = 0; let i = 0;
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
i++;
}
if (!ja[i]) { while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
ja[i] = []; i += 1;
} }
if (0 < i) { if (!ja[i]) {
val.prev = ja[i-1][ja[i - 1].length - 1]; ja[i] = [];
} }
ja[i].push(val);
});
// Now, pull out the longest common subsequence. if (i > 0) {
let lcs: any[] = []; val.prev = ja[i - 1][ja[i - 1].length - 1];
}
if (0 < ja.length) { ja[i].push(val);
const n = ja.length - 1; });
lcs = [ja[n][ja[n].length - 1]];
while (lcs[lcs.length - 1].prev) {
lcs.push(lcs[lcs.length - 1].prev);
}
}
return lcs.reverse(); // Now, pull out the longest common subsequence.
} let lcs: any[] = [];
// "result" is the array used to accumulate the textOld that are deleted, the if (ja.length > 0) {
// lines that are shared between textOld and textNew, and the textNew that were const n = ja.length - 1;
// inserted. lcs = [ja[n][ja[n].length - 1]];
const result: any[] = [];
let deleted = 0;
let inserted = 0;
// aMove and bMove will contain the lines that don't match, and will be returned while (lcs[lcs.length - 1].prev) {
// for possible searching of lines that moved. lcs.push(lcs[lcs.length - 1].prev);
}
const aMove: any[] = []; }
const aMoveIndex: any[] = [];
const bMove: any[] = []; return lcs.reverse();
const bMoveIndex: any[] = []; }
// "result" is the array used to accumulate the textOld that are deleted, the
// lines that are shared between textOld and textNew, and the textNew that were
// inserted.
const result: any[] = [];
let deleted = 0;
let inserted = 0;
// aMove and bMove will contain the lines that don't match, and will be returned
// for possible searching of lines that moved.
const aMove: any[] = [];
const aMoveIndex: any[] = [];
const bMove: any[] = [];
const bMoveIndex: any[] = [];
/** /**
* addToResult simply pushes the latest value onto the "result" array. This * addToResult simply pushes the latest value onto the "result" array. This
* array captures the diff of the line, aIndex, and bIndex from the textOld * array captures the diff of the line, aIndex, and bIndex from the textOld
* and textNew array. * and textNew array.
* @param aIndex * @param aIndex
* @param bIndex * @param bIndex
*/ */
function addToResult(aIndex: number, bIndex: number) { function addToResult(aIndex: number, bIndex: number) {
if (bIndex < 0) { if (bIndex < 0) {
aMove.push(textOld[aIndex]); aMove.push(textOld[aIndex]);
aMoveIndex.push(result.length); aMoveIndex.push(result.length);
deleted++; deleted += 1;
} else if (aIndex < 0) { } else if (aIndex < 0) {
bMove.push(textNew[bIndex]); bMove.push(textNew[bIndex]);
bMoveIndex.push(result.length); bMoveIndex.push(result.length);
inserted++; inserted += 1;
} }
result.push({
line: aIndex >= 0 ? textOld[aIndex] : textNew[bIndex],
aIndex,
bIndex
});
}
result.push({
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
aIndex: aIndex,
bIndex: bIndex,
});
}
/** /**
* addSubMatch handles the lines between a pair of entries in the LCS. Thus, * addSubMatch handles the lines between a pair of entries in the LCS. Thus,
* this function might recursively call recurseLCS to further match the lines * this function might recursively call recurseLCS to further match the lines
* between textOld and textNew. * between textOld and textNew.
* @param aLo * @param aLo
* @param aHi * @param aHi
* @param bLo * @param bLo
* @param bHi * @param bHi
*/ */
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) { function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
// Match any lines at the beginning of textOld and textNew. // Match any lines at the beginning of textOld and textNew.
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) { while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
addToResult(aLo++, bLo++); addToResult(aLo++, bLo++);
} }
// Match any lines at the end of textOld and textNew, but don't place them // Match any lines at the end of textOld and textNew, but don't place them
// in the "result" array just yet, as the lines between these matches at // in the "result" array just yet, as the lines between these matches at
// the beginning and the end need to be analyzed first. // the beginning and the end need to be analyzed first.
const aHiTemp = aHi;
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
aHi--;
bHi--;
}
// Now, check to determine with the remaining lines in the subsequence const aHiTemp = aHi;
// whether there are any unique common lines between textOld and textNew. while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
// aHi -= 1;
// If not, add the subsequence to the result (all textOld having been bHi -= 1;
// deleted, and all textNew having been inserted). }
//
// If there are unique common lines between textOld and textNew, then let's
// recursively perform the patience diff on the subsequence.
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
if (uniqueCommonMap.size === 0) {
while (aLo <= aHi) {
addToResult(aLo++, -1);
}
while (bLo <= bHi) { // Now, check to determine with the remaining lines in the subsequence
addToResult(-1, bLo++); // whether there are any unique common lines between textOld and textNew.
} //
} else { // If not, add the subsequence to the result (all textOld having been
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap); // deleted, and all textNew having been inserted).
} //
// If there are unique common lines between textOld and textNew, then let's
// recursively perform the patience diff on the subsequence.
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
if (uniqueCommonMap.size === 0) {
while (aLo <= aHi) {
addToResult(aLo++, -1);
}
while (bLo <= bHi) {
addToResult(-1, bLo++);
}
} else {
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
}
// Finally, let's add the matches at the end to the result.
while (aHi < aHiTemp) {
addToResult(++aHi, ++bHi);
}
}
// Finally, let's add the matches at the end to the result.
while (aHi < aHiTemp) {
addToResult(++aHi, ++bHi);
}
}
/** /**
* recurseLCS finds the longest common subsequence (LCS) between the arrays * recurseLCS finds the longest common subsequence (LCS) between the arrays
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence * textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
* recursively performs another LCS search (via addSubMatch), until there are * recursively performs another LCS search (via addSubMatch), until there are
* none found, at which point the subsequence is dumped to the result. * none found, at which point the subsequence is dumped to the result.
* @param aLo * @param aLo
* @param aHi * @param aHi
* @param bLo * @param bLo
* @param bHi * @param bHi
* @param uniqueCommonMap * @param uniqueCommonMap
*/ */
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) { function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi)); const x = longestCommonSubsequence(
uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi)
if (x.length === 0) { );
addSubMatch(aLo, aHi, bLo, bHi);
} else {
if (aLo < x[0].indexA || bLo < x[0].indexB) {
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
}
let i; if (x.length === 0) {
for (i = 0; i < x.length - 1; i++) { addSubMatch(aLo, aHi, bLo, bHi);
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1); } else {
} if (aLo < x[0].indexA || bLo < x[0].indexB) {
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
}
if (x[i].indexA <= aHi || x[i].indexB <= bHi) { let i;
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi); for (i = 0; i < x.length - 1; i += 1) {
} addSubMatch(x[i].indexA, x[i + 1].indexA - 1, x[i].indexB, x[i + 1].indexB - 1);
} }
}
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1); if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
}
}
}
if (diffPlusFlag) { recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0,
aMove: aMove,
aMoveIndex: aMoveIndex,
bMove: bMove,
bMoveIndex: bMoveIndex,
};
}
return { if (diffPlusFlag) {
lines: result, return {
lineCountDeleted: deleted, lines: result,
lineCountInserted: inserted, lineCountDeleted: deleted,
lineCountMoved: 0, lineCountInserted: inserted,
}; lineCountMoved: 0,
aMove,
aMoveIndex,
bMove,
bMoveIndex
};
}
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0
};
} }
/** /**
@@ -289,58 +303,54 @@ function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boole
*/ */
function patienceDiffPlus(textOld: string[], textNew: string[]) { function patienceDiffPlus(textOld: string[], textNew: string[]) {
const difference = patienceDiff(textOld, textNew, true);
const difference = patienceDiff(textOld, textNew, true); let aMoveNext = difference.aMove;
let aMoveIndexNext = difference.aMoveIndex;
let bMoveNext = difference.bMove;
let bMoveIndexNext = difference.bMoveIndex;
let aMoveNext = difference.aMove; delete difference.aMove;
let aMoveIndexNext = difference.aMoveIndex; delete difference.aMoveIndex;
let bMoveNext = difference.bMove; delete difference.bMove;
let bMoveIndexNext = difference.bMoveIndex; delete difference.bMoveIndex;
delete difference.aMove; let lastLineCountMoved;
delete difference.aMoveIndex;
delete difference.bMove;
delete difference.bMoveIndex;
let lastLineCountMoved;
do { do {
const aMove = aMoveNext; const aMove = aMoveNext;
const aMoveIndex = aMoveIndexNext; const aMoveIndex = aMoveIndexNext;
const bMove = bMoveNext; const bMove = bMoveNext;
const bMoveIndex = bMoveIndexNext; const bMoveIndex = bMoveIndexNext;
aMoveNext = []; aMoveNext = [];
aMoveIndexNext = []; aMoveIndexNext = [];
bMoveNext = []; bMoveNext = [];
bMoveIndexNext = []; bMoveIndexNext = [];
const subDiff = patienceDiff(aMove!, bMove!); const subDiff = patienceDiff(aMove!, bMove!);
lastLineCountMoved = difference.lineCountMoved;
subDiff.lines.forEach((v, i) => { lastLineCountMoved = difference.lineCountMoved;
if (0 <= v.aIndex && 0 <= v.bIndex) { subDiff.lines.forEach((v, i) => {
if (v.aIndex >= 0 && v.bIndex >= 0) {
difference.lines[aMoveIndex![v.aIndex]].moved = true; difference.lines[aMoveIndex![v.aIndex]].moved = true;
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex]; difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
difference.lines[bMoveIndex![v.bIndex]].moved = true; difference.lines[bMoveIndex![v.bIndex]].moved = true;
difference.lineCountInserted--; difference.lineCountInserted -= 1;
difference.lineCountDeleted--; difference.lineCountDeleted -= 1;
difference.lineCountMoved++; difference.lineCountMoved += 1;
} else if (v.bIndex < 0) { } else if (v.bIndex < 0) {
aMoveNext!.push(aMove![v.aIndex]); aMoveNext!.push(aMove![v.aIndex]);
aMoveIndexNext!.push(aMoveIndex![v.aIndex]); aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
} else { } else {
bMoveNext!.push(bMove![v.bIndex]); bMoveNext!.push(bMove![v.bIndex]);
bMoveIndexNext!.push(bMoveIndex![v.bIndex]); bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
} }
}); });
} while (0 < difference.lineCountMoved - lastLineCountMoved); } while (difference.lineCountMoved - lastLineCountMoved > 0);
return difference;
return difference;
} }
export default patienceDiff; export default patienceDiff;

View File

@@ -11,25 +11,25 @@ function timeSince(date: Date) {
let interval = seconds / 31536000; let interval = seconds / 31536000;
if (interval > 1) { if (interval > 1) {
return Math.floor(interval) + ' years ago'; return `${Math.floor(interval) } years ago`;
} }
interval = seconds / 2592000; interval = seconds / 2592000;
if (interval > 1) { if (interval > 1) {
return Math.floor(interval) + ' months ago'; return `${Math.floor(interval) } months ago`;
} }
interval = seconds / 86400; interval = seconds / 86400;
if (interval > 1) { if (interval > 1) {
return Math.floor(interval) + ' days ago'; return `${Math.floor(interval) } days ago`;
} }
interval = seconds / 3600; interval = seconds / 3600;
if (interval > 1) { if (interval > 1) {
return Math.floor(interval) + ' hours ago'; return `${Math.floor(interval) } hours ago`;
} }
interval = seconds / 60; interval = seconds / 60;
if (interval > 1) { if (interval > 1) {
return Math.floor(interval) + ' minutes ago'; return `${Math.floor(interval) } minutes ago`;
} }
return Math.floor(seconds) + ' seconds ago'; return `${Math.floor(seconds) } seconds ago`;
} }
export default timeSince; export default timeSince;

View File

@@ -1,6 +1,6 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
interface usePopUpProps { interface UsePopUpProps {
name: Readonly<string>; name: Readonly<string>;
isOpen: boolean; isOpen: boolean;
} }
@@ -10,18 +10,18 @@ interface usePopUpProps {
* checks which type of inputProps were given and converts them into key-names * checks which type of inputProps were given and converts them into key-names
* SIDENOTE: On inputting give it as const and not string with (as const) * SIDENOTE: On inputting give it as const and not string with (as const)
*/ */
type usePopUpState<T extends Readonly<string[]> | usePopUpProps[]> = { type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
[P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: { [P in T extends UsePopUpProps[] ? T[number]['name'] : T[number]]: {
isOpen: boolean; isOpen: boolean;
data?: unknown; data?: unknown;
}; };
}; };
interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> { interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
popUp: usePopUpState<T>; popUp: UsePopUpState<T>;
handlePopUpOpen: (popUpName: keyof usePopUpState<T>, data?: unknown) => void; handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
handlePopUpClose: (popUpName: keyof usePopUpState<T>) => void; handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
handlePopUpToggle: (popUpName: keyof usePopUpState<T>) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<T>) => void;
} }
/** /**
@@ -29,34 +29,31 @@ interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
* Provides api to open,close,toggle and also store temporary data for the popUp * Provides api to open,close,toggle and also store temporary data for the popUp
* @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}] * @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}]
*/ */
export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>( export const usePopUp = <T extends Readonly<string[]> | UsePopUpProps[]>(
popUpNames: T popUpNames: T
): usePopUpReturn<T> => { ): UsePopUpReturn<T> => {
const [popUp, setPopUp] = useState<usePopUpState<T>>( const [popUp, setPopUp] = useState<UsePopUpState<T>>(
Object.fromEntries( Object.fromEntries(
popUpNames.map((popUpName) => popUpNames.map((popUpName) =>
typeof popUpName === 'string' typeof popUpName === 'string'
? [popUpName, { isOpen: false }] ? [popUpName, { isOpen: false }]
: [popUpName.name, { isOpen: popUpName.isOpen }] : [popUpName.name, { isOpen: popUpName.isOpen }]
) // convert into an array of [[popUpName,state]] then into Object ) // convert into an array of [[popUpName,state]] then into Object
) as usePopUpState<T> // to override generic string return type of the function ) as UsePopUpState<T> // to override generic string return type of the function
); );
const handlePopUpOpen = useCallback( const handlePopUpOpen = useCallback((popUpName: keyof UsePopUpState<T>, data?: unknown) => {
(popUpName: keyof usePopUpState<T>, data?: unknown) => { setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: true, data } }));
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } }));
},
[]
);
const handlePopUpClose = useCallback((popUpName: keyof usePopUpState<T>) => {
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } }));
}, []); }, []);
const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => { const handlePopUpClose = useCallback((popUpName: keyof UsePopUpState<T>) => {
setPopUp((popUp) => ({ setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: false } }));
...popUp, }, []);
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState<T>) => {
setPopUp((oldState) => ({
...oldState,
[popUpName]: { isOpen: !oldState[popUpName].isOpen }
})); }));
}, []); }, []);
@@ -64,6 +61,6 @@ export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>(
popUp, popUp,
handlePopUpOpen, handlePopUpOpen,
handlePopUpClose, handlePopUpClose,
handlePopUpToggle, handlePopUpToggle
}; };
}; };

View File

@@ -32,7 +32,7 @@ export default function Custom404() {
height={554} height={554}
width={942} width={942}
alt='infisical dragon - page not found' alt='infisical dragon - page not found'
></Image> />
</div> </div>
</div> </div>
); );

View File

@@ -1,15 +1,15 @@
/* eslint-disable react/jsx-props-no-spreading */
import { useEffect } from 'react'; import { useEffect } from 'react';
import { AppProps } from 'next/app'; import { AppProps } from 'next/app';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { appWithTranslation } from 'next-i18next'; import { appWithTranslation } from 'next-i18next';
import Layout from '@app/components/basic/Layout';
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
import RouteGuard from '@app/components/RouteGuard';
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
import { publicPaths } from '@app/const';
import { config } from '@fortawesome/fontawesome-svg-core'; import { config } from '@fortawesome/fontawesome-svg-core';
import Layout from '~/components/basic/Layout';
import NotificationProvider from '~/components/context/Notifications/NotificationProvider';
import RouteGuard from '~/components/RouteGuard';
import { publicPaths } from '~/const';
import Telemetry from '~/utilities/telemetry/Telemetry';
import '@fortawesome/fontawesome-svg-core/styles.css'; import '@fortawesome/fontawesome-svg-core/styles.css';
import '../styles/globals.css'; import '../styles/globals.css';
@@ -19,18 +19,14 @@ type NextAppProp = AppProps & {
Component: AppProps['Component'] & { requireAuth: boolean }; Component: AppProps['Component'] & { requireAuth: boolean };
}; };
const App = ({ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => {
Component,
pageProps,
...appProps
}: NextAppProp): JSX.Element => {
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
const storedLang = localStorage.getItem('lang'); const storedLang = localStorage.getItem('lang');
if (router.locale ?? 'en' !== storedLang ?? 'en') { if (router.locale ?? storedLang !== 'en' ?? 'en') {
router.push(router.asPath, router.asPath, { router.push(router.asPath, router.asPath, {
locale: storedLang ?? 'en', locale: storedLang ?? 'en'
}); });
} }
}, [router.locale, router.pathname]); }, [router.locale, router.pathname]);
@@ -54,7 +50,7 @@ const App = ({
// If it's one of these routes, don't add the layout (e.g., these routes are external) // If it's one of these routes, don't add the layout (e.g., these routes are external)
if ( if (
publicPaths.includes('/' + appProps.router.pathname.split('/')[1]) || publicPaths.includes(`/${appProps.router.pathname.split('/')[1]}`) ||
!Component.requireAuth !Component.requireAuth
) { ) {
return <Component {...pageProps} />; return <Component {...pageProps} />;
@@ -73,8 +69,7 @@ const App = ({
export default appWithTranslation(App); export default appWithTranslation(App);
{ /* <Script
/* <Script
src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1" src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1"
strategy="afterInteractive" strategy="afterInteractive"
/> />
@@ -87,4 +82,3 @@ strategy="afterInteractive"
gtag('config', 'G-DQ1XLJJGG1'); gtag('config', 'G-DQ1XLJJGG1');
`} `}
</Script> */ </Script> */
}

View File

@@ -1,18 +1,16 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next"; import { useTranslation } from 'next-i18next';
import Button from '@app/components/basic/buttons/Button';
import EventFilter from '@app/components/basic/EventFilter';
import NavHeader from '@app/components/navigation/NavHeader';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import ActivitySideBar from '@app/ee/components/ActivitySideBar'; import ActivitySideBar from '@app/ee/components/ActivitySideBar';
import Button from '~/components/basic/buttons/Button';
import EventFilter from '~/components/basic/EventFilter';
import NavHeader from '~/components/navigation/NavHeader';
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs'; import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
import ActivityTable from '../../ee/components/ActivityTable'; import ActivityTable from '../../ee/components/ActivityTable';
interface LogData {
interface logData {
_id: string; _id: string;
channel: string; channel: string;
createdAt: string; createdAt: string;
@@ -25,17 +23,17 @@ interface logData {
name: string; name: string;
payload: { payload: {
secretVersions: string[]; secretVersions: string[];
} };
}[] }[];
} }
interface PayloadProps { interface PayloadProps {
_id: string; _id: string;
name: string; name: string;
secretVersions: string[]; secretVersions: string[];
} }
interface logDataPoint { interface LogDataPoint {
_id: string; _id: string;
channel: string; channel: string;
createdAt: string; createdAt: string;
@@ -50,11 +48,11 @@ interface logDataPoint {
export default function Activity() { export default function Activity() {
const router = useRouter(); const router = useRouter();
const [eventChosen, setEventChosen] = useState(''); const [eventChosen, setEventChosen] = useState('');
const [logsData, setLogsData] = useState<logDataPoint[]>([]); const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [currentOffset, setCurrentOffset] = useState(0); const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 10; const currentLimit = 10;
const [currentSidebarAction, toggleSidebar] = useState<string>() const [currentSidebarAction, toggleSidebar] = useState<string>();
const { t } = useTranslation(); const { t } = useTranslation();
// this use effect updates the data in case of a new filter being added // this use effect updates the data in case of a new filter being added
@@ -62,25 +60,29 @@ export default function Activity() {
setCurrentOffset(0); setCurrentOffset(0);
const getLogData = async () => { const getLogData = async () => {
setIsLoading(true); setIsLoading(true);
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen }) const tempLogsData = await getProjectLogs({
setLogsData(tempLogsData.map((log: logData) => { workspaceId: String(router.query.id),
return { offset: 0,
_id: log._id, limit: currentLimit,
channel: log.channel, userId: '',
createdAt: log.createdAt, actionNames: eventChosen
ipAddress: log.ipAddress, });
user: log.user.email, setLogsData(
payload: log.actions.map(action => { tempLogsData.map((log: LogData) => ({
return { _id: log._id,
_id: action._id, channel: log.channel,
name: action.name, createdAt: log.createdAt,
secretVersions: action.payload.secretVersions ipAddress: log.ipAddress,
} user: log.user.email,
}) payload: log.actions.map((action) => ({
} _id: action._id,
})) name: action.name,
secretVersions: action.payload.secretVersions
}))
}))
);
setIsLoading(false); setIsLoading(false);
} };
getLogData(); getLogData();
}, [eventChosen]); }, [eventChosen]);
@@ -88,58 +90,64 @@ export default function Activity() {
useEffect(() => { useEffect(() => {
const getLogData = async () => { const getLogData = async () => {
setIsLoading(true); setIsLoading(true);
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen }) const tempLogsData = await getProjectLogs({
setLogsData(logsData.concat(tempLogsData.map((log: logData) => { workspaceId: String(router.query.id),
return { offset: currentOffset,
_id: log._id, limit: currentLimit,
channel: log.channel, userId: '',
createdAt: log.createdAt, actionNames: eventChosen
ipAddress: log.ipAddress, });
user: log.user.email, setLogsData(
payload: log.actions.map(action => { logsData.concat(
return { tempLogsData.map((log: LogData) => ({
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log.user.email,
payload: log.actions.map((action) => ({
_id: action._id, _id: action._id,
name: action.name, name: action.name,
secretVersions: action.payload.secretVersions secretVersions: action.payload.secretVersions
} }))
}) }))
} )
}))) );
setIsLoading(false); setIsLoading(false);
} };
getLogData(); getLogData();
}, [currentLimit, currentOffset]); }, [currentLimit, currentOffset]);
const loadMoreLogs = () => { const loadMoreLogs = () => {
setCurrentOffset(currentOffset + currentLimit); setCurrentOffset(currentOffset + currentLimit);
} };
return ( return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen"> <div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
<NavHeader pageName="Project Activity" isProjectRelated={true} /> <NavHeader pageName="Project Activity" isProjectRelated />
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />} {currentSidebarAction && (
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
)}
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2"> <div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
<div className="flex flex-row justify-start items-center text-3xl"> <div className="flex flex-row justify-start items-center text-3xl">
<p className="font-semibold mr-4 text-bunker-100">{t("activity:title")}</p> <p className="font-semibold mr-4 text-bunker-100">{t('activity:title')}</p>
</div> </div>
<p className="mr-4 text-base text-gray-400"> <p className="mr-4 text-base text-gray-400">{t('activity:subtitle')}</p>
{t("activity:subtitle")}
</p>
</div> </div>
<div className="px-6 h-8 mt-2"> <div className="px-6 h-8 mt-2">
<EventFilter <EventFilter selected={eventChosen} select={setEventChosen} />
selected={eventChosen}
select={setEventChosen}
/>
</div> </div>
<ActivityTable <ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
data={logsData} <div className="flex justify-center w-full mb-6">
toggleSidebar={toggleSidebar} <div className="items-center w-60">
isLoading={isLoading} <Button
/> text={String(t('common:view-more'))}
<div className='flex justify-center w-full mb-6'> textDisabled={String(t('common:end-of-history'))}
<div className='items-center w-60'> active={logsData.length % 10 === 0}
<Button text={String(t("common:view-more"))} textDisabled={String(t("common:end-of-history"))} active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/> onButtonPressed={loadMoreLogs}
size="md"
color="mineshaft"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -148,4 +156,4 @@ export default function Activity() {
Activity.requireAuth = true; Activity.requireAuth = true;
export const getServerSideProps = getTranslatedServerSideProps(["activity", "common"]); export const getServerSideProps = getTranslatedServerSideProps(['activity', 'common']);

View File

@@ -1,4 +1,4 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
name: string; name: string;
@@ -12,11 +12,8 @@ interface Props {
* @param {string} obj.expiresIn - how soon the API key expires in ms * @param {string} obj.expiresIn - how soon the API key expires in ms
* @returns * @returns
*/ */
const addAPIKey = ({ const addAPIKey = ({ name, expiresIn }: Props) =>
name, SecurityClient.fetchCall('/api/v2/api-key/', {
expiresIn,
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -26,12 +23,11 @@ const addAPIKey = ({
expiresIn expiresIn
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return (await res.json()); return res.json();
} else {
console.log('Failed to add API key');
} }
console.log('Failed to add API key');
return undefined;
}); });
};
export default addAPIKey; export default addAPIKey;

View File

@@ -1,4 +1,4 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
apiKeyId: string; apiKeyId: string;
@@ -10,21 +10,18 @@ interface Props {
* @param {string} obj.apiKeyId - id of the API key to delete * @param {string} obj.apiKeyId - id of the API key to delete
* @returns * @returns
*/ */
const deleteAPIKey = ({ const deleteAPIKey = ({ apiKeyId }: Props) =>
apiKeyId SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/' + apiKeyId, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
},
}).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to delete API key');
} }
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log('Failed to delete API key');
return undefined;
}); });
};
export default deleteAPIKey; export default deleteAPIKey;

View File

@@ -1,26 +1,22 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
/** /**
* This route gets API keys for the user * This route gets API keys for the user
* @param {*} param0 * @param {*} param0
* @returns * @returns
*/ */
const getAPIKeys = () => { const getAPIKeys = () =>
return SecurityClient.fetchCall( SecurityClient.fetchCall('/api/v2/api-key', {
'/api/v2/api-key', method: 'GET',
{ headers: {
method: 'GET', 'Content-Type': 'application/json'
headers: {
'Content-Type': 'application/json'
}
} }
).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return (await res.json()).apiKeyData; return (await res.json()).apiKeyData;
} else {
console.log('Failed to get API keys');
} }
console.log('Failed to get API keys');
return undefined;
}); });
};
export default getAPIKeys; export default getAPIKeys;

View File

@@ -1,4 +1,4 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
encryptedPrivateKey: string; encryptedPrivateKey: string;
@@ -14,34 +14,26 @@ interface Props {
* @param {*} clientPublicKey * @param {*} clientPublicKey
* @returns * @returns
*/ */
const changePassword2 = ({ const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
encryptedPrivateKey, SecurityClient.fetchCall('/api/v1/password/change-password', {
iv,
tag,
salt,
verifier,
clientProof
}: Props) => {
return SecurityClient.fetchCall('/api/v1/password/change-password', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
clientProof: clientProof, clientProof,
encryptedPrivateKey: encryptedPrivateKey, encryptedPrivateKey,
iv: iv, iv,
tag: tag, tag,
salt: salt, salt,
verifier: verifier verifier
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status === 200) {
return res; return res;
} else {
console.log('Failed to change the password');
} }
console.log('Failed to change the password');
return undefined;
}); });
};
export default changePassword2; export default changePassword2;

View File

@@ -1,16 +1,15 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
/** /**
* This function is used to check if the user is authenticated. * This function is used to check if the user is authenticated.
* To do that, we get their tokens from cookies, and verify if they are good. * To do that, we get their tokens from cookies, and verify if they are good.
*/ */
const checkAuth = async () => { const checkAuth = async () =>
return SecurityClient.fetchCall('/api/v1/auth/checkAuth', { SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}).then((res) => res); }).then((res) => res);
};
export default checkAuth; export default checkAuth;

View File

@@ -10,17 +10,15 @@ interface Props {
* @param {string} obj.code * @param {string} obj.code
* @returns * @returns
*/ */
const checkEmailVerificationCode = ({ email, code }: Props) => { const checkEmailVerificationCode = ({ email, code }: Props) => fetch('/api/v1/signup/email/verify', {
return fetch('/api/v1/signup/email/verify', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
email: email, email,
code: code code
}) })
}); });
};
export default checkEmailVerificationCode; export default checkEmailVerificationCode;

View File

@@ -41,12 +41,11 @@ const completeAccountInformationSignup = ({
salt, salt,
verifier, verifier,
token token
}: Props) => { }: Props) => fetch('/api/v1/signup/complete-account/signup', {
return fetch('/api/v1/signup/complete-account/signup', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: 'Bearer ' + token Authorization: `Bearer ${ token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
email, email,
@@ -61,6 +60,5 @@ const completeAccountInformationSignup = ({
verifier verifier
}) })
}); });
};
export default completeAccountInformationSignup; export default completeAccountInformationSignup;

View File

@@ -38,25 +38,23 @@ const completeAccountInformationSignupInvite = ({
salt, salt,
verifier, verifier,
token token
}: Props) => { }: Props) => fetch('/api/v1/signup/complete-account/invite', {
return fetch('/api/v1/signup/complete-account/invite', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
Authorization: 'Bearer ' + token Authorization: `Bearer ${ token}`
}, },
body: JSON.stringify({ body: JSON.stringify({
email: email, email,
firstName: firstName, firstName,
lastName: lastName, lastName,
publicKey: publicKey, publicKey,
encryptedPrivateKey: ciphertext, encryptedPrivateKey: ciphertext,
iv: iv, iv,
tag: tag, tag,
salt: salt, salt,
verifier: verifier verifier
}) })
}); });
};
export default completeAccountInformationSignupInvite; export default completeAccountInformationSignupInvite;

View File

@@ -18,8 +18,8 @@ const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
email: email, email,
code: code code
}) })
}); });
if (response?.status === 200) { if (response?.status === 200) {

View File

@@ -1,4 +1,4 @@
import SecurityClient from '~/utilities/SecurityClient'; import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props { interface Props {
encryptedPrivateKey: string; encryptedPrivateKey: string;
@@ -27,19 +27,19 @@ const issueBackupPrivateKey = ({
salt, salt,
verifier, verifier,
clientProof clientProof
}: Props) => { }: Props) =>
return SecurityClient.fetchCall('/api/v1/password/backup-private-key', { SecurityClient.fetchCall('/api/v1/password/backup-private-key', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
clientProof: clientProof, clientProof,
encryptedPrivateKey: encryptedPrivateKey, encryptedPrivateKey,
iv: iv, iv,
tag: tag, tag,
salt: salt, salt,
verifier: verifier verifier
}) })
}).then((res) => { }).then((res) => {
if (res?.status !== 200) { if (res?.status !== 200) {
@@ -47,6 +47,5 @@ const issueBackupPrivateKey = ({
} }
return res; return res;
}); });
};
export default issueBackupPrivateKey; export default issueBackupPrivateKey;

View File

@@ -16,7 +16,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
email: email, email,
clientPublicKey, clientPublicKey,
}), }),
}); });

View File

@@ -13,24 +13,24 @@ interface Login2Response {
* @returns * @returns
*/ */
const login2 = async (email: string, clientProof: string) => { const login2 = async (email: string, clientProof: string) => {
const response = await fetch("/api/v1/auth/login2", { const response = await fetch('/api/v1/auth/login2', {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json'
}, },
body: JSON.stringify({ body: JSON.stringify({
email: email, email,
clientProof, clientProof
}), }),
credentials: "include", credentials: 'include'
}); });
// need precise error handling about the status code // need precise error handling about the status code
if (response.status == 200) { if (response.status === 200) {
const data = (await response.json()) as unknown as Login2Response; const data = (await response.json()) as unknown as Login2Response;
return data; return data;
} }
throw new Error("Password verification failed"); throw new Error('Password verification failed');
}; };
export default login2; export default login2;

Some files were not shown because too many files have changed in this diff Show More