This commit is contained in:
Tuan Dang
2023-01-18 10:54:07 +07:00
184 changed files with 6284 additions and 6099 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)$"]
]
}
]
}
}

73
frontend/.eslintrc.js Normal file
View File

@ -0,0 +1,73 @@
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'],
['^@app'],
// 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",
"start": "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"
},
"dependencies": {
@ -62,14 +62,23 @@
"@types/jsrp": "^0.2.4",
"@types/node": "18.11.9",
"@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",
"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-prettier": "^8.6.0",
"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",
"postcss": "^8.4.14",
"prettier": "^2.8.3",
"tailwindcss": "^3.1.4",
"typescript": "^4.9.3"
}

View File

@ -1,8 +1,8 @@
import { ReactNode, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { publicPaths } from '~/const';
import checkAuth from '~/pages/api/auth/CheckAuth';
import { publicPaths } from '@app/const';
import checkAuth from '@app/pages/api/auth/CheckAuth';
// #TODO: finish spinner only when the data loads fully
// #TODO: Redirect somewhere if the page does not exist
@ -13,8 +13,36 @@ type Prop = {
export default function RouteGuard({ children }: Prop): JSX.Element {
const router = useRouter();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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(() => {
// on initial load - run auth check
(async () => {
@ -40,35 +68,5 @@ export default function RouteGuard({ children }: Prop): JSX.Element {
// 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;
}

View File

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

View File

@ -1,16 +1,13 @@
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
export default function Error({ text }: { text: string }): JSX.Element {
const Error = ({ text }: { text: string }): JSX.Element => {
return (
<div className='relative flex flex-row justify-center m-auto items-center w-fit rounded-full'>
<FontAwesomeIcon
icon={faExclamationTriangle}
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 className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
<FontAwesomeIcon icon={faExclamationTriangle} 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>
);
}
};
export default Error;

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import React, { Fragment } from 'react';
import { useTranslation } from 'next-i18next';
import {
faAngleDown,
faEye,
@ -42,27 +41,20 @@ const eventOptions = [
* @param {string} obj.selected - the event that is currently selected
* @param {function} obj.select - an action that happens when an item is selected
*/
export default function EventFilter({
selected,
select
}: ListBoxProps): JSX.Element {
const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
const { t } = useTranslation();
return (
<Listbox value={t("activity:event." + selected)} onChange={select}>
<Listbox value={t(`activity:event.${selected}`)} onChange={select}>
<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">
{selected != '' ? (
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
{selected !== '' ? (
<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 != '' ? (
<FontAwesomeIcon
icon={faX}
className="pl-2 w-2 p-2"
onClick={() => select('')}
/>
{selected !== '' ? (
<FontAwesomeIcon icon={faX} className="pl-2 w-2 p-2" onClick={() => select('')} />
) : (
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
)}
@ -74,33 +66,29 @@ export default function EventFilter({
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">
{eventOptions.map((event, id) => {
return (
<Listbox.Option
key={id}
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'
}`}
value={event.name}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? 'font-semibold' : 'font-normal'
}`}
>
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
{t("activity:event." + event.name)}
</span>
</>
)}
</Listbox.Option>
);
})}
{eventOptions.map((event, id) => (
<Listbox.Option
key={`${event.name}.${id + 1}`}
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'
}`}
value={event.name}
>
{({ selected: isSelected }) => (
<span
className={`block truncate ${isSelected ? 'font-semibold' : 'font-normal'}`}
>
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
{t(`activity:event.${event.name}`)}
</span>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
);
}
};
export default EventFilter;

View File

@ -5,13 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import guidGenerator from '../utilities/randomId';
interface InputFieldProps {
static?: boolean;
isStatic?: boolean;
label: string;
type: string;
value: string;
placeholder?: string;
isRequired: boolean;
disabled?: boolean;
error?: boolean;
text?: string;
name?: string;
@ -20,46 +19,54 @@ interface InputFieldProps {
onChangeHandler: (value: string) => void;
}
const InputField = (
props: InputFieldProps &
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>
) => {
const InputField = ({
isRequired,
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);
if (props.static === true) {
if (isStatic === true) {
return (
<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'>
{props.label}
</p>
{props.text && (
<p className='text-xs text-gray-400 mb-2'>{props.text}</p>
)}
<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">{label}</p>
{text && <p className="text-xs text-gray-400 mb-2">{text}</p>}
<input
onChange={(e) => props.onChangeHandler(e.target.value)}
type={props.type}
placeholder={props.placeholder}
value={props.value}
required={props.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'
name={props.name}
onChange={(e) => onChangeHandler(e.target.value)}
type={type}
placeholder={placeholder}
value={value}
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"
name={name}
readOnly
autoComplete={props.autoComplete}
id={props.id}
autoComplete={autoComplete}
id={id}
/>
</div>
);
} else {
return (
<div className='flex-col w-full'>
<div className='flex flex-row text-mineshaft-300 items-center mb-0.5'>
<p className='text-sm font-semibold mr-1'>{props.label}</p>
{/* {props.label == "Password" && router.asPath != "/login" && (
}
return (
<div className="flex-col w-full">
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
<p className="text-sm font-semibold mr-1">{label}</p>
{/* {label === "Password" && router.asPath !== "/login" && (
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
<FontAwesomeIcon
icon={faCircleExclamation}
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">
@ -70,74 +77,69 @@ const InputField = (
</span>
</div>
)} */}
</div>
<div
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
props.error ? 'border-red' : 'border-mineshaft-500'
} rounded-md`}
>
<input
onChange={(e) => props.onChangeHandler(e.target.value)}
type={passwordVisible === false ? props.type : 'text'}
placeholder={props.placeholder}
value={props.value}
required={props.isRequired}
className={`${
props.blurred
? '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'
} 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}
spellCheck='false'
autoComplete={props.autoComplete}
id={props.id}
/>
{props.label?.includes('Password') && (
<button
type='button'
onClick={() => {
setPasswordVisible(!passwordVisible);
}}
className='absolute self-end mr-3 text-gray-400 cursor-pointer'
>
{passwordVisible ? (
<FontAwesomeIcon icon={faEyeSlash} />
) : (
<FontAwesomeIcon icon={faEye} />
)}
</button>
)}
{props.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'>
<p className='ml-2'></p>
{props.value
.split('')
.slice(0, 54)
.map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className='text-xxs mx-0.5'
icon={faCircle}
/>
))}
</div>
)}
{/* {props.error && (
</div>
<div
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
error ? 'border-red' : 'border-mineshaft-500'
} rounded-md`}
>
<input
onChange={(e) => onChangeHandler(e.target.value)}
type={passwordVisible === false ? type : 'text'}
placeholder={placeholder}
value={value}
required={isRequired}
className={`${
blurred
? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400'
: ''
} ${
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`}
name={name}
spellCheck="false"
autoComplete={autoComplete}
id={id}
/>
{label?.includes('Password') && (
<button
type="button"
onClick={() => {
setPasswordVisible(!passwordVisible);
}}
className="absolute self-end mr-3 text-gray-400 cursor-pointer"
>
{passwordVisible ? (
<FontAwesomeIcon icon={faEyeSlash} />
) : (
<FontAwesomeIcon icon={faEye} />
)}
</button>
)}
{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">
<p className="ml-2" />
{value
.split('')
.slice(0, 54)
.map(() => (
<FontAwesomeIcon
key={guidGenerator()}
className="text-xxs mx-0.5"
icon={faCircle}
/>
))}
</div>
)}
{/* {error && (
<div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end">
<Error />
</div>
)} */}
</div>
{props.error && (
<p className='text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs'>
{props.errorText}
</p>
)}
</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);

View File

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

View File

@ -1,15 +1,10 @@
import React from 'react';
import { Fragment } from 'react';
import {
faAngleDown,
faCheck,
faPlus,
} from '@fortawesome/free-solid-svg-icons';
import React, { Fragment } from 'react';
import { faAngleDown, faCheck, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Listbox, Transition } from '@headlessui/react';
interface ListBoxProps {
selected: string;
isSelected: string;
onChange: (arg: string) => void;
data: string[] | null;
text?: string;
@ -20,55 +15,55 @@ interface ListBoxProps {
/**
* This is the component that we use for drop down lists.
* @param {object} obj
* @param {string} obj.selected - the item that is currently selected
* @param {string} obj.isSelected - the item that is currently selected
* @param {function} obj.onChange - what happends if you select the item inside a list
* @param {string[]} obj.data - all the options available
* @param {string} obj.text - the text that shows us in front of the select option
* @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
*/
export default function ListBox({
selected,
const ListBox = ({
isSelected,
onChange,
data,
text,
buttonAction,
isFull,
}: ListBoxProps): JSX.Element {
isFull
}: ListBoxProps): JSX.Element => {
return (
<Listbox value={selected} onChange={onChange}>
<div className='relative'>
<Listbox value={isSelected} onChange={onChange}>
<div className="relative">
<Listbox.Button
className={`text-gray-400 relative ${
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`}
>
<div className='flex flex-row'>
<div className="flex flex-row">
{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">
{' '}
{selected}
{isSelected}
</span>
</div>
{data && (
<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' />
<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" />
</div>
)}
</Listbox.Button>
{data && (
<Transition
as={Fragment}
leave='transition ease-in duration-100'
leaveFrom='opacity-100'
leaveTo='opacity-0'
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
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) => (
<Listbox.Option
key={personIdx}
key={`${person}.${personIdx + 1}`}
className={({ active, selected }) =>
`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 ${
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
} ${
active && !selected
@ -88,11 +83,8 @@ export default function ListBox({
{person}
</span>
{selected ? (
<span className='text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3'>
<FontAwesomeIcon
icon={faCheck}
className='text-md ml-1'
/>
<span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3">
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
</span>
) : null}
</>
@ -100,13 +92,10 @@ export default function ListBox({
</Listbox.Option>
))}
{buttonAction && (
<button
onClick={buttonAction}
className='cursor-pointer w-full'
>
<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' />
<button type="button" onClick={buttonAction} className="cursor-pointer w-full">
<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>
Add Project
</div>
@ -118,4 +107,6 @@ export default function ListBox({
</div>
</Listbox>
);
}
};
export default ListBox;

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Dialog, Transition } from '@headlessui/react';
import addAPIKey from '~/pages/api/apiKey/addAPIKey';
import addAPIKey from '@app/pages/api/apiKey/addAPIKey';
import Button from '../buttons/Button';
import InputField from '../InputField';
@ -99,7 +99,7 @@ const AddApiKeyDialog = ({
leaveFrom='opacity-100 scale-100'
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.Title
as='h3'
@ -126,7 +126,7 @@ const AddApiKeyDialog = ({
</div>
<div className='max-h-28'>
<ListBox
selected={apiKeyExpiresIn}
isSelected={apiKeyExpiresIn}
onChange={setApiKeyExpiresIn}
data={[
'1 day',
@ -135,7 +135,7 @@ const AddApiKeyDialog = ({
'6 months',
'12 months',
]}
isFull={true}
isFull
text={`${t('common:expired-in')}: `}
/>
</div>
@ -149,7 +149,7 @@ const AddApiKeyDialog = ({
t('section-api-key:add-dialog.add') as string
}
size='md'
active={apiKeyName == '' ? false : true}
active={apiKeyName !== ''}
/>
</div>
</div>
@ -176,15 +176,16 @@ const AddApiKeyDialog = ({
<input
type='text'
value={apiKey}
disabled={true}
disabled
id='apiKey'
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'>
{apiKey}
</div>
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button
type="button"
onClick={copyToClipboard}
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
>

View File

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

View File

@ -27,122 +27,105 @@ const AddProjectMemberDialog = ({
const { t } = useTranslation();
return (
<div className='z-50'>
<div className="z-50">
<Transition appear show={isOpen} as={Fragment}>
<Dialog as='div' className='relative' onClose={closeModal}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-200'
leaveFrom='opacity-100'
leaveTo='opacity-0'
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
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>
<div className='fixed inset-0 overflow-y-auto'>
<div className='flex min-h-full items-center justify-center p-4 text-center'>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter='ease-out duration-300'
enterFrom='opacity-0 scale-95'
enterTo='opacity-100 scale-100'
leave='ease-in duration-200'
leaveFrom='opacity-100 scale-100'
leaveTo='opacity-0 scale-95'
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
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 ? (
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400 z-50'
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
{t('section-members:add-dialog.add-member-to-project')}
</Dialog.Title>
) : (
<Dialog.Title
as='h3'
className='text-lg font-medium leading-6 text-gray-400 z-50'
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
{t('section-members:add-dialog.already-all-invited')}
</Dialog.Title>
)}
<div className='mt-2 mb-4'>
<div className="mt-2 mb-4">
{data?.length > 0 ? (
<div className='flex flex-col'>
<p className='text-sm text-gray-500'>
<div className="flex flex-col">
<p className="text-sm text-gray-500">
{t('section-members:add-dialog.user-will-email')}
</p>
<div className=''>
<div className="">
<Trans
i18nKey='section-members:add-dialog.looking-add'
i18nKey="section-members:add-dialog.looking-add"
components={[
// eslint-disable-next-line react/jsx-key
<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'
onClick={() =>
router.push(
'/settings/org/' + router.query.id
)
}
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"
onClick={() => router.push(`/settings/org/${router.query.id}`)}
aria-label="add member"
/>,
// eslint-disable-next-line react/jsx-key
<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'
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"
onClick={() =>
router.push(
'/settings/org/' +
router.query.id +
'?invite'
)
router.push(`/settings/org/${router.query.id}?invite`)
}
/>,
aria-label="add member"
/>
]}
/>
</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')}
</p>
)}
</div>
<div className='max-h-28'>
<div className="max-h-28">
{data?.length > 0 && (
<ListBox
selected={email ? email : data[0]}
onChange={setEmail}
data={data}
isFull={true}
/>
<ListBox isSelected={email || data[0]} onChange={setEmail} data={data} isFull />
)}
</div>
<div className='max-w-max'>
<div className="max-w-max">
{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
onButtonPressed={submitModal}
color='mineshaft'
color="mineshaft"
text={t('section-members:add-member') as string}
size='md'
size="md"
/>
</div>
) : (
<Button
onButtonPressed={() =>
router.push('/settings/org/' + router.query.id)
}
color='mineshaft'
text={
t(
'section-members:add-dialog.add-user-to-org'
) as string
}
size='md'
onButtonPressed={() => router.push(`/settings/org/${router.query.id}`)}
color="mineshaft"
text={t('section-members:add-dialog.add-user-to-org') as string}
size="md"
/>
)}
</div>

View File

@ -6,8 +6,8 @@ import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Dialog, Transition } from '@headlessui/react';
import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import addServiceToken from '@app/pages/api/serviceToken/addServiceToken';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import {
decryptAssymmetric,
@ -86,7 +86,7 @@ const AddServiceTokenDialog = ({
});
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
setServiceToken(newServiceToken.serviceToken + '.' + randomBytes);
setServiceToken(`${newServiceToken.serviceToken }.${ randomBytes}`);
};
function copyToClipboard() {
@ -141,7 +141,7 @@ const AddServiceTokenDialog = ({
leaveFrom='opacity-100 scale-100'
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.Title
as='h3'
@ -170,7 +170,7 @@ const AddServiceTokenDialog = ({
</div>
<div className='max-h-28 mb-2'>
<ListBox
selected={
isSelected={
selectedServiceTokenEnv?.name
? selectedServiceTokenEnv?.name
: environments[0]?.name
@ -186,13 +186,13 @@ const AddServiceTokenDialog = ({
}
)
}
isFull={true}
isFull
text={`${t('common:environment')}: `}
/>
</div>
<div className='max-h-28'>
<ListBox
selected={serviceTokenExpiresIn}
isSelected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn}
data={[
'1 day',
@ -201,7 +201,7 @@ const AddServiceTokenDialog = ({
'6 months',
'12 months',
]}
isFull={true}
isFull
text={`${t('common:expired-in')}: `}
/>
</div>
@ -215,7 +215,7 @@ const AddServiceTokenDialog = ({
t('section-token:add-dialog.add') as string
}
size='md'
active={serviceTokenName == '' ? false : true}
active={serviceTokenName !== ''}
/>
</div>
</div>
@ -244,13 +244,14 @@ const AddServiceTokenDialog = ({
value={serviceToken}
id='serviceToken'
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'>
{serviceToken}
</div>
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
<button
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'
>
{serviceTokenCopied ? (

View File

@ -10,7 +10,6 @@ type Props = {
isOpen: boolean;
closeModal: () => void;
submitModal: (email: string) => void;
workspaceId: string;
email: string;
setEmail: (email: string) => void;
currentPlan: string;
@ -82,13 +81,13 @@ const AddUserDialog = ({
isRequired
/>
</div>
{currentPlan == STRIPE_PRODUCT_STARTER && (
{currentPlan === STRIPE_PRODUCT_STARTER && (
<div className='flex flex-row'>
<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'
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.
@ -97,7 +96,7 @@ const AddUserDialog = ({
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'
onClick={() =>
router.push('/settings/billing/' + router.query.id)
router.push(`/settings/billing/${ router.query.id}`)
}
>
Upgrade now.

View File

@ -26,7 +26,14 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
leaveFrom="opacity-100"
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>
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
@ -39,10 +46,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
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.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-400"
>
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
{t('dashboard:sidebar.delete-key-dialog.title')}
</Dialog.Title>
<div className="mt-2">

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { faX } from '@fortawesome/free-solid-svg-icons';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
import deleteAPIKey from '../../../pages/api/apiKey/deleteAPIKey';
import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button';
@ -28,49 +28,47 @@ const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
const { createNotification } = useNotificationContext();
return (
<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">
<thead className="text-bunker-300 text-sm font-light">
<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">VALID UNTIL</th>
<th></th>
<th aria-label="button" />
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data?.map((row) => {
return (
<tr
key={guidGenerator()}
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">
{row.name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}
</td>
<td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button
onButtonPressed={() => {
deleteAPIKey({ apiKeyId: row._id} );
setApiKeys(data.filter(token => token._id != row._id));
createNotification({
text: `'${row.name}' API key has been revoked.`,
type: 'error'
});
}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td>
</tr>
);
})
data?.map((row) => (
<tr
key={guidGenerator()}
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">
{row.name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}
</td>
<td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button
onButtonPressed={() => {
deleteAPIKey({ apiKeyId: row._id });
setApiKeys(data.filter((token) => token._id !== row._id));
createNotification({
text: `'${row.name}' API key has been revoked.`,
type: 'error'
});
}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td>
</tr>
))
) : (
<tr>
<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 = {
addAllUsers: boolean;
setAddAllUsers: (arg: boolean) => void;
};
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => {
return (
<>
<div className='flex flex-row items-center'>
{addAllUsers == true ? (
<input
type='checkbox'
className='accent-primary h-4 w-4'
checked
readOnly
onClick={() => setAddAllUsers(!addAllUsers)}
/>
) : (
<div
className='h-4 w-4 bg-bunker border border-gray-600 rounded-sm'
onClick={() => setAddAllUsers(!addAllUsers)}
></div>
)}
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
<div className="flex flex-row items-center">
{addAllUsers === true ? (
<input
type="checkbox"
className="accent-primary h-4 w-4"
checked
readOnly
onClick={() => setAddAllUsers(!addAllUsers)}
/>
) : (
<div
onKeyDown={() => setAddAllUsers(!addAllUsers)}
role="button"
tabIndex={0}
aria-label="add all users"
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'>
Add all members of my organization to this project.
</label>
</div>
</>
);
};
<label className="ml-2 text-gray-500 text-sm">
Add all members of my organization to this project.
</label>
</div>
);

View File

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

View File

@ -1,8 +1,8 @@
import { faX } from '@fortawesome/free-solid-svg-icons';
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
import deleteServiceToken from '../../../pages/api/serviceToken/deleteServiceToken';
import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button';
@ -33,7 +33,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
return (
<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">
<thead className="text-bunker-300 text-sm font-light">
<tr>
@ -41,49 +41,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">ENVIRONMENT</th>
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
<th></th>
<th aria-label="button" />
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data?.map((row) => {
return (
<tr
key={guidGenerator()}
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">
{row.name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{workspaceName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.environment}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}
</td>
<td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button
onButtonPressed={() => {
deleteServiceToken({ serviceTokenId: row._id} );
setServiceTokens(data.filter(token => token._id != row._id));
createNotification({
text: `'${row.name}' token has been revoked.`,
type: 'error'
});
}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td>
</tr>
);
})
data?.map((row) => (
<tr
key={guidGenerator()}
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">
{row.name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{workspaceName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.environment}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}
</td>
<td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button
onButtonPressed={() => {
deleteServiceToken({ serviceTokenId: row._id });
setServiceTokens(data.filter((token) => token._id !== row._id));
createNotification({
text: `'${row.name}' token has been revoked.`,
type: 'error'
});
}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td>
</tr>
))
) : (
<tr>
<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,219 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { faX } from '@fortawesome/free-solid-svg-icons';
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 { 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
isSelected={row.role}
onChange={(e) => handleRoleUpdate(index, e)}
data={
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
}
text="Role: "
/>
) : (
row.status !== 'invited' &&
row.status !== 'verified' && (
<Listbox
isSelected={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,6 @@
import React from 'react';
import StripeRedirect from '~/pages/api/organization/StripeRedirect';
import StripeRedirect from '@app/pages/api/organization/StripeRedirect';
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
@ -21,54 +21,43 @@ export default function Plan({ plan }: Props) {
return (
<div
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
? 'border-primary'
: 'border-chicago-700'
plan.name !== 'Starter' && plan.current === true ? 'border-primary' : 'border-chicago-700'
}
`}
>
<div className='flex flex-col'>
<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`}>
{plan.name}
</p>
<div className="flex flex-col">
<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">{plan.name}</p>
</div>
<div className='flex flwx-row items-end justify-start mb-4'>
<p className='pl-6 text-3xl font-semibold text-primary'>
{plan.price}
</p>
<p className='pl-3 mb-1 text-lg text-gray-400'>
{plan.priceExplanation}
</p>
<div className="flex flwx-row items-end justify-start mb-4">
<p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
<p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
</div>
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'>
{plan.text}
</p>
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'>
{plan.subtext}
</p>
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
</div>
<div className='flex flex-row items-center'>
{plan.current == false ? (
<div className="flex flex-row items-center">
{plan.current === false ? (
<>
{plan.buttonTextMain == 'Schedule a Demo' ? (
<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'>
{plan.buttonTextMain === 'Schedule a Demo' ? (
<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">
{plan.buttonTextMain}
</div>
</a>
) : (
<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 ${
plan.buttonTextMain == 'Downgrade'
plan.buttonTextMain === 'Downgrade'
? 'hover:bg-red hover:text-white hover:border-red'
: 'hover:bg-primary hover:text-black hover:border-primary'
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
>
<button
type="button"
onClick={() =>
StripeRedirect({
orgId: tempLocalStorage('orgData.id'),
orgId: tempLocalStorage('orgData.id')
})
}
>
@ -76,11 +65,8 @@ export default function Plan({ plan }: Props) {
</button>
</div>
)}
<a
href='https://infisical.com/pricing'
target='_blank rel="noopener"'
>
<div className='relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5'>
<a href="https://infisical.com/pricing" 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}
</div>
</a>
@ -88,12 +74,10 @@ export default function Plan({ plan }: Props) {
) : (
<div
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
plan.name != 'Starter' && plan.current == true
? 'bg-primary'
: 'bg-chicago-700'
plan.name !== 'Starter' && plan.current === true ? '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>

View File

@ -2,26 +2,26 @@ import { useEffect, useRef } from 'react';
import { faX } from '@fortawesome/free-solid-svg-icons';
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 {
notification: Required<NotificationType>;
notification: Required<TNotification>;
clearNotification: (text: string) => void;
}
const Notification = ({
notification,
clearNotification
}: NotificationProps) => {
const Notification = ({ notification, clearNotification }: NotificationProps) => {
const timeout = useRef<number>();
const handleClearNotification = () => clearNotification(notification.text);
const setNotifTimeout = () => {
timeout.current = window.setTimeout(
handleClearNotification,
notification.timeoutMs
);
timeout.current = window.setTimeout(handleClearNotification, notification.timeoutMs);
};
const cancelNotifTimeout = () => {
@ -40,25 +40,21 @@ const Notification = ({
role="alert"
>
{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' && (
<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' && (
<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">
{notification.text}
</p>
<p className="text-bunker-200 text-sm font-semibold mt-0.5">{notification.text}</p>
<button
type="button"
className="rounded-lg"
onClick={() => clearNotification(notification.text)}
>
<FontAwesomeIcon
className="text-white pl-2 w-4 h-3 hover:text-red"
icon={faX}
/>
<FontAwesomeIcon className="text-white pl-2 w-4 h-3 hover:text-red" icon={faX} />
</button>
</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';
type NotificationType = 'success' | 'error' | 'info';
export type Notification = {
text: string;
type?: NotificationType;
timeoutMs?: number;
};
type NotificationContextState = {
createNotification: (newNotification: Notification) => void;
createNotification: (newNotification: TNotification) => void;
};
const NotificationContext = createContext<NotificationContextState>({
@ -24,43 +17,33 @@ interface NotificationProviderProps {
children: ReactNode;
}
// TODO: Migration to radix toast
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) => {
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]);
};
const value = useMemo(() => ({ createNotification }), [createNotification]);
return (
<NotificationContext.Provider
value={{
createNotification
}}
>
<Notifications
notifications={notifications}
clearNotification={clearNotification}
/>
<NotificationContext.Provider value={value}>
<Notifications notifications={notifications} clearNotification={clearNotification} />
{children}
</NotificationContext.Provider>
);

View File

@ -1,27 +1,19 @@
import Notification from './Notification';
import { Notification as NotificationType } from './NotificationProvider';
import Notification, { TNotification } from './Notification';
interface NoticationsProps {
notifications: Required<NotificationType>[];
notifications: Required<TNotification>[];
clearNotification: (text: string) => void;
}
const Notifications = ({
notifications,
clearNotification
}: NoticationsProps) => {
const Notifications = ({ notifications, clearNotification }: NoticationsProps) => {
if (!notifications.length) {
return null;
}
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) => (
<Notification
key={notif.text}
notification={notif}
clearNotification={clearNotification}
/>
<Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
))}
</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.
*/
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();
return <div className={`relative mt-4 px-4 pt-6`}>
<p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p>
<textarea
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"
value={comment}
onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..."
/>
</div>
}
return (
<div className="relative mt-4 px-4 pt-6">
<p className="text-sm text-bunker-300 pl-0.5">{t('dashboard:sidebar.comments')}</p>
<textarea
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"
value={comment}
onChange={(e) => modifyComment(e.target.value, position)}
placeholder="Leave any comments here..."
/>
</div>
);
};
export default CommentField;

View File

@ -40,14 +40,14 @@ const DashboardInputField = ({
}: DashboardInputFieldProps) => {
const ref = useRef<HTMLDivElement | null>(null);
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
if (ref.current == null) return;
if (ref.current === null) return;
ref.current.scrollTop = e.currentTarget.scrollTop;
ref.current.scrollLeft = e.currentTarget.scrollLeft;
};
if (type === 'varName') {
const startsWithNumber = !isNaN(Number(value?.charAt(0))) && value != '';
const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== '';
const error = startsWithNumber || isDuplicate;
return (
@ -58,9 +58,7 @@ const DashboardInputField = ({
} rounded-md`}
>
<input
onChange={(e) =>
onChangeHandler(e.target.value.toUpperCase(), position)
}
onChange={(e) => onChangeHandler(e.target.value.toUpperCase(), position)}
type={type}
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 ${
@ -81,13 +79,16 @@ const DashboardInputField = ({
)}
</div>
);
} else if (type === 'value') {
}
if (type === 'value') {
return (
<div className="flex-col w-full">
<div
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 enabled</div>}
<div 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 enabled
</div>
)}
<input
value={value}
onChange={(e) => onChangeHandler(e.target.value, position)}
@ -105,20 +106,18 @@ const DashboardInputField = ({
blurred && !override
? '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`}
>
{value?.split(REGEX).map((word, id) => {
if (word.match(REGEX) !== null) {
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)}
<span className="ph-no-capture text-yellow-200/80">
{word.slice(2, word.length - 1)}
</span>
{word.slice(word.length - 1, word.length) == '}' ? (
{word.slice(word.length - 1, word.length) === '}' ? (
<span className="ph-no-capture text-yellow">
{word.slice(word.length - 1, word.length)}
</span>
@ -129,13 +128,12 @@ const DashboardInputField = ({
)}
</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>
{blurred && (
@ -160,7 +158,14 @@ const DashboardInputField = ({
};
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
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);

View File

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

View File

@ -1,5 +1,4 @@
import { Fragment } from 'react';
import { useTranslation } from "next-i18next";
import { faDownload } from '@fortawesome/free-solid-svg-icons';
import { Menu, Transition } from '@headlessui/react';
import { SecretDataProps } from 'public/data/frequentInterfaces';
@ -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)
* @param {object} obj
* @param {object} obj
* @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)
*/
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
const { t } = useTranslation();
return <Menu
as="div"
className="relative inline-block text-left"
>
<Menu.Button
as="div"
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"
>
<Button
color="mineshaft"
size="icon-md"
icon={faDownload}
onButtonPressed={() => {}}
/>
</Menu.Button>
<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-[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>
<Button
color="mineshaft"
onButtonPressed={() => downloadDotEnv({ data, env })}
size="md"
text="Download as .env"
/>
</Menu.Item>
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={() => downloadYaml({ data, env })}
size="md"
text="Download as .yml"
/>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
}
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string }) => {
return (
<Menu as="div" className="relative inline-block text-left">
<Menu.Button
as="div"
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"
>
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
</Menu.Button>
<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-[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>
<Button
color="mineshaft"
onButtonPressed={() => downloadDotEnv({ data, env })}
size="md"
text="Download as .env"
/>
</Menu.Item>
<Menu.Item>
<Button
color="mineshaft"
onButtonPressed={() => downloadYaml({ data, env })}
size="md"
text="Download as .yml"
/>
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
);
};
export default DownloadSecretMenu;

View File

@ -1,14 +1,15 @@
import { type ChangeEvent, type DragEvent, useState } from "react";
import Image from "next/image";
import { useTranslation } from "next-i18next";
import { faUpload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
/* eslint-disable no-nested-ternary */
import { type ChangeEvent, type DragEvent, useState } from 'react';
import Image from 'next/image';
import { useTranslation } from 'next-i18next';
import { faUpload } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { parseDocument, Scalar, YAMLMap } from 'yaml';
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
import { parseDotEnv } from '../utilities/parseDotEnv';
import guidGenerator from "../utilities/randomId";
import guidGenerator from '../utilities/randomId';
interface DropZoneProps {
// TODO: change Data type from any
@ -28,7 +29,7 @@ const DropZone = ({
errorDragAndDrop,
setButtonReady,
keysExist,
numCurrentRows,
numCurrentRows
}: DropZoneProps) => {
const { t } = useTranslation();
@ -47,7 +48,7 @@ const DropZone = ({
e.stopPropagation();
// set dropEffect to copy i.e copy of the source item
e.dataTransfer.dropEffect = "copy";
e.dataTransfer.dropEffect = 'copy';
};
const [loading, setLoading] = useState(false);
@ -57,16 +58,14 @@ const DropZone = ({
switch (fileType) {
case 'env': {
const keyPairs = parseDotEnv(file);
secrets = Object.keys(keyPairs).map((key, index) => {
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: key,
value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
type: 'shared',
};
});
secrets = Object.keys(keyPairs).map((key, index) => ({
id: guidGenerator(),
pos: numCurrentRows + index,
key,
value: keyPairs[key as keyof typeof keyPairs].value,
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
type: 'shared'
}));
break;
}
case 'yml': {
@ -79,15 +78,15 @@ const DropZone = ({
fileContent!.items
.find((item) => item.key.value === key)
?.key?.commentBefore?.split('\n')
.map((comment) => comment.trim())
.map((cmnt) => cmnt.trim())
.join('\n') ?? '';
return {
id: guidGenerator(),
pos: numCurrentRows + index,
key: key,
key,
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
comment,
type: 'shared',
type: 'shared'
};
});
break;
@ -105,7 +104,7 @@ const DropZone = ({
setTimeout(() => setLoading(false), 5000);
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = "copy";
e.dataTransfer.dropEffect = 'copy';
const file = e.dataTransfer.files[0];
const reader = new FileReader();
@ -149,16 +148,11 @@ const DropZone = ({
return loading ? (
<div className="flex items-center justify-center pt-16 mb-16">
<Image
src="/images/loading/loading.gif"
height={70}
width={120}
alt="google logo"
></Image>
<Image src="/images/loading/loading.gif" height={70} width={120} alt="google logo" />
</div>
) : keysExist ? (
<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}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
@ -171,36 +165,27 @@ const DropZone = ({
accept=".txt,.env,.yml"
onChange={handleFileSelect}
/>
{errorDragAndDrop ? (
<div className="my-3 max-w-xl opacity-80"></div>
) : (
<div className=""></div>
)}
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
<div className="flex flex-row">
<FontAwesomeIcon
icon={faUpload}
className="text-bunker-300 text-3xl mr-6"
/>
<p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
<FontAwesomeIcon icon={faUpload} className="text-bunker-300 text-3xl mr-6" />
<p className="text-bunker-300 mt-1">{t('common:drop-zone-keys')}</p>
</div>
{errorDragAndDrop ? (
{errorDragAndDrop && (
<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'" />
</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}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
<p className="">{t("common:drop-zone")}</p>
<p className="">{t('common:drop-zone')}</p>
<input
id="fileSelect"
type="file"
@ -209,14 +194,14 @@ const DropZone = ({
onChange={handleFileSelect}
/>
<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>
<div className="border-t border-gray-700 w-1/5"></div>
<div className="border-t border-gray-700 w-1/5" />
</div>
<div className="z-10 mb-6">
<Button
color="mineshaft"
text={String(t("dashboard:add-secret"))}
text={String(t('dashboard:add-secret'))}
onButtonPressed={createNewFile}
size="md"
/>

View File

@ -1,95 +1,106 @@
import { Fragment,useState } from 'react';
import { useTranslation } from "next-i18next";
import { Fragment, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Menu, Transition } from '@headlessui/react';
/**
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
* @returns the popup-menu for randomly generating secrets
*/
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
const GenerateSecretMenu = ({
modifyValue,
position
}: {
modifyValue: (value: string, position: number) => void;
position: number;
}) => {
const [randomStringLength, setRandomStringLength] = useState(32);
const { t } = useTranslation();
return <Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
</div>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
<div
onClick={() => {
if (randomStringLength > 32) {
setRandomStringLength(32);
} else if (randomStringLength < 2) {
setRandomStringLength(2);
} else {
modifyValue(
[...Array(randomStringLength)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join(''),
position
);
}
}}
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
>
<FontAwesomeIcon
className="text-lg pl-1.5 pr-3"
icon={faShuffle}
/>
<div className="text-sm justify-between flex flex-row w-full">
<p>{t("dashboard:sidebar.generate-random-hex")}</p>
<p>{t("dashboard:sidebar.digits")}</p>
return (
<Menu as="div" className="relative inline-block text-left">
<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">
<div className="py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500">
<FontAwesomeIcon icon={faShuffle} className="text-bunker-300" />
</div>
</div>
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
</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
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => {
if (randomStringLength > 1) {
setRandomStringLength(randomStringLength - 1);
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>
<input
onChange={(e) =>
setRandomStringLength(parseInt(e.target.value))
}
value={randomStringLength}
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
spellCheck="false"
/>
<div
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
onClick={() => {
if (randomStringLength < 32) {
setRandomStringLength(randomStringLength + 1);
}
}}
>
+
<div 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
onKeyDown={() => null}
role="button"
tabIndex={0}
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
onClick={() => {
if (randomStringLength > 1) {
setRandomStringLength(randomStringLength - 1);
}
}}
>
-
</div>
<input
onChange={(e) => setRandomStringLength(parseInt(e.target.value, 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>
</Menu.Items>
</Transition>
</Menu>
}
</Menu.Items>
</Transition>
</Menu>
);
};
export default GenerateSecretMenu;

View File

@ -1,4 +1,3 @@
import React from 'react';
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SecretDataProps } from 'public/data/frequentInterfaces';
@ -7,9 +6,9 @@ import DashboardInputField from './DashboardInputField';
interface KeyPairProps {
keyPair: SecretDataProps;
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string, position: number) => void;
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string, position: number) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
@ -28,7 +27,7 @@ interface KeyPairProps {
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
* @param {function} obj.toggleSidebar - open/close/switch sidebar
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
* @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
*/
const KeyPair = ({
@ -41,48 +40,61 @@ const KeyPair = ({
toggleSidebar,
sidebarSecretId,
isSnapshot
}: KeyPairProps) => {
return (
<div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
{keyPair.valueOverride && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
This secret is overriden
</span>
</div>}
<div className="min-w-xl w-96">
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={keyPair.pos}
value={keyPair.key}
isDuplicate={isDuplicate}
/>
</div>
}: KeyPairProps) => (
<div
className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && 'pointer-events-none'} ${
keyPair.id === sidebarSecretId && 'bg-mineshaft-500 duration-200'
} rounded-md`}
>
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
{keyPair.valueOverride && (
<div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
<div className="w-1 h-1 rounded-full bg-primary z-40" />
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
This secret is overriden
</span>
</div>
<div className="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 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 className="min-w-xl w-96">
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={keyPair.pos}
value={keyPair.key}
isDuplicate={isDuplicate}
/>
</div>}
</div>
</div>
<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>
);
export default KeyPair;
export default KeyPair;

View File

@ -1,10 +1,12 @@
/* eslint-disable react/no-unused-prop-types */
import { useState } from 'react';
import Image from 'next/image';
import { useTranslation } from "next-i18next";
import SecretVersionList from '@app/ee/components/SecretVersionList';
import { useTranslation } from 'next-i18next';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SecretVersionList from '@app/ee/components/SecretVersionList';
import Button from '../basic/buttons/Button';
import Toggle from '../basic/Toggle';
import CommentField from './CommentField';
@ -12,7 +14,6 @@ import DashboardInputField from './DashboardInputField';
import { DeleteActionButton } from './DeleteActionButton';
import GenerateSecretMenu from './GenerateSecretMenu';
interface SecretProps {
key: string;
value: string;
@ -22,22 +23,18 @@ interface SecretProps {
comment: string;
}
interface OverrideProps {
id: string;
valueOverride: string;
}
export interface DeleteRowFunctionProps {
ids: string[];
secretName: string;
export interface DeleteRowFunctionProps {
ids: string[];
secretName: string;
}
interface SideBarProps {
toggleSidebar: (value: string) => void;
toggleSidebar: (value: string) => void;
data: SecretProps[];
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void;
modifyKey: (value: string, position: number) => void;
modifyValue: (value: string, position: number) => void;
modifyValueOverride: (value: string | undefined, position: number) => void;
modifyComment: (value: string, position: number) => void;
buttonReady: boolean;
savePush: () => void;
sharedToHide: string[];
@ -57,112 +54,140 @@ interface SideBarProps {
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @returns the sidebar with 'secret's settings'
*/
const SideBar = ({
toggleSidebar,
data,
modifyKey,
modifyValue,
modifyValueOverride,
const SideBar = ({
toggleSidebar,
data,
modifyKey,
modifyValue,
modifyValueOverride,
modifyComment,
buttonReady,
buttonReady,
savePush,
deleteRow
}: SideBarProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoading, setIsLoading] = useState(false);
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride != undefined);
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
const { t } = useTranslation();
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between'>
{isLoading ? (
<div className="flex items-center justify-center h-full">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
return (
<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">
{isLoading ? (
<div className="flex items-center justify-center h-full">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
/>
</div>
) : (
<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("dashboard:sidebar.secret")}</p>
<div className='p-1' onClick={() => toggleSidebar("None")}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
</div>
</div>
<div className='mt-4 px-4 pointer-events-none'>
<p className='text-sm text-bunker-300'>{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={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 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('dashboard:sidebar.secret')}</p>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className="p-1"
onClick={() => toggleSidebar('None')}
>
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
</div>
</div>
<div className="mt-4 px-4 pointer-events-none">
<p className="text-sm text-bunker-300">{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>
<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 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>
);
};
export default SideBar;

View File

@ -21,12 +21,8 @@ interface IntegrationAuth {
interface Props {
cloudIntegrationOption: CloudIntegrationOption;
setSelectedIntegrationOption: (
cloudIntegration: CloudIntegrationOption
) => void;
integrationOptionPress: (
cloudIntegrationOption: CloudIntegrationOption
) => void;
setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
integrationAuths: IntegrationAuth[];
}
@ -34,11 +30,14 @@ const CloudIntegration = ({
cloudIntegrationOption,
setSelectedIntegrationOption,
integrationOptionPress,
integrationAuths,
integrationAuths
}: Props) => {
const router = useRouter();
return integrationAuths ? (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className={`relative ${
cloudIntegrationOption.isAvailable
? 'hover:bg-white/10 duration-200 cursor-pointer'
@ -55,18 +54,17 @@ const CloudIntegration = ({
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
height={70}
width={70}
alt='integration logo'
alt="integration logo"
/>
{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 className='text-base'>
{cloudIntegrationOption.name.split(' ')[1]}{' '}
{cloudIntegrationOption.name.split(' ')[2]}
<div className="text-base">
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
</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}
</div>
)}
@ -74,43 +72,45 @@ const CloudIntegration = ({
integrationAuths
.map((authorization) => authorization.integration)
.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
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={(event) => {
event.stopPropagation();
deleteIntegrationAuth({
integrationAuthId: integrationAuths
.filter(
(authorization) =>
authorization.integration ==
cloudIntegrationOption.name.toLowerCase()
authorization.integration === cloudIntegrationOption.name.toLowerCase()
)
.map((authorization) => authorization._id)[0],
.map((authorization) => authorization._id)[0]
});
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
</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'>
<FontAwesomeIcon icon={faCheck} className='text-xs mr-2' />
<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" />
Authorized
</div>
</div>
)}
{!cloudIntegrationOption.isAvailable && (
<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="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">
Coming Soon
</div>
</div>
)}
</div>
) : (
<div></div>
<div />
);
};

View File

@ -29,7 +29,7 @@ const CloudIntegrationSection = ({
return (
<>
<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'>
{t('integrations:cloud-integrations')}

View File

@ -7,33 +7,29 @@ interface Framework {
docsLink: string;
}
const FrameworkIntegration = ({ framework }: { framework: Framework }) => {
return (
<a
href={framework.docsLink}
rel='noopener'
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`}
const FrameworkIntegration = ({ framework }: { framework: Framework }) => (
<a
href={framework.docsLink}
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"
>
<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
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 ${
framework?.name?.split(' ').length > 1
? 'text-sm px-1'
: 'text-xl px-2'
} text-center w-full max-w-xs`}
>
{framework?.image && (
<Image
src={`/images/integrations/${framework.image}.png`}
height={framework?.name ? 60 : 90}
width={framework?.name ? 60 : 90}
alt='integration logo'
></Image>
)}
{framework?.name && framework?.image && <div className='h-2'></div>}
{framework?.name && framework.name}
</div>
</a>
);
};
{framework?.image && (
<Image
src={`/images/integrations/${framework.image}.png`}
height={framework?.name ? 60 : 90}
width={framework?.name ? 60 : 90}
alt="integration logo"
/>
)}
{framework?.name && framework?.image && <div className="h-2" />}
{framework?.name && framework.name}
</div>
</a>
);
export default FrameworkIntegration;

View File

@ -1,33 +1,27 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import {
faArrowRight,
faRotate,
faX,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// TODO: This needs to be moved from public folder
import {
contextNetlifyMapping,
reverseContextNetlifyMapping,
} from "public/data/frequentConstants";
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// TODO: This needs to be moved from public folder
import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
import Button from "~/components/basic/buttons/Button";
import ListBox from "~/components/basic/Listbox";
import Button from '@app/components/basic/buttons/Button';
import ListBox from '@app/components/basic/Listbox';
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps";
import updateIntegration from "../../pages/api/integrations/updateIntegration"
import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
import updateIntegration from '../../pages/api/integrations/updateIntegration';
interface Integration {
_id: string;
app?: string;
target?: string;
environment: string;
integration: string;
integrationAuth: string;
isActive: boolean;
context: string;
interface TIntegration {
_id: string;
app?: string;
target?: string;
environment: string;
integration: string;
integrationAuth: string;
isActive: boolean;
context: string;
}
interface IntegrationApp {
@ -36,219 +30,190 @@ interface IntegrationApp {
}
type Props = {
integration: Integration;
integration: TIntegration;
environments: Array<{ name: string; slug: string }>;
};
const Integration = ({
integration,
environments = []
}:Props ) => {
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<
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);
}
const Integration = ({ integration, environments = [] }: Props) => {
// set initial environment. This find will only execute when component is mounting
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
environments.find(({ slug }) => slug === integration.environment) || {
name: '',
slug: ''
}
if (!integrationApp || apps.length === 0) return <div></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={true}
/>
</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'
);
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();
}, []);
// eslint-disable-next-line @typescript-eslint/no-shadow
const renderIntegrationSpecificParams = (integration: TIntegration) => {
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}
isSelected={integrationTarget}
onChange={setIntegrationTarget}
isFull
/>
<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;
);
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
}
isSelected={integrationContext}
onChange={setIntegrationContext}
/>
</div>
);
default:
return <div />;
}
} catch (err) {
console.error(err);
}
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>
);
return <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}
isSelected={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}
isSelected={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';
@ -17,29 +17,21 @@ interface IntegrationType {
context: string;
}
const ProjectIntegrationSection = ({
integrations,
environments = [],
}: Props) => {
return integrations.length > 0 ? (
<div className='mb-12'>
<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'>
const ProjectIntegrationSection = ({ integrations, environments = [] }: Props) =>
integrations.length > 0 ? (
<div className="mb-12">
<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.
</p>
</div>
{integrations.map((integration: IntegrationType) => (
<Integration
key={guidGenerator()}
integration={integration}
environments={environments}
/>
<Integration key={guidGenerator()} integration={integration} environments={environments} />
))}
</div>
) : (
<div></div>
<div />
);
};
export default ProjectIntegrationSection;

View File

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

View File

@ -3,8 +3,8 @@ import { useRouter } from 'next/router';
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getOrganization from '~/pages/api/organization/GetOrg';
import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
import getOrganization from '@app/pages/api/organization/GetOrg';
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
/**
* This is the component at the top of almost every page.
@ -17,7 +17,7 @@ import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
*/
export default function NavHeader({
pageName,
isProjectRelated,
isProjectRelated
}: {
pageName: string;
isProjectRelated?: boolean;
@ -30,12 +30,12 @@ export default function NavHeader({
(async () => {
const orgId = localStorage.getItem('orgData.id');
const org = await getOrganization({
orgId: orgId ? orgId : '',
orgId: orgId || ''
});
setOrgName(org.name);
const workspace = await getProjectInfo({
projectId: String(router.query.id),
projectId: String(router.query.id)
});
setWorkspaceName(workspace.name);
})();
@ -43,27 +43,19 @@ export default function NavHeader({
}, []);
return (
<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="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">
{orgName?.charAt(0)}
</div>
<div className='text-primary text-sm font-semibold'>{orgName}</div>
<div className="text-primary text-sm font-semibold">{orgName}</div>
{isProjectRelated && (
<>
<FontAwesomeIcon
icon={faAngleRight}
className='ml-3 text-sm text-gray-400 mr-3'
/>
<div className='font-semibold text-primary text-sm'>
{workspaceName}
</div>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
<div className="font-semibold text-primary text-sm">{workspaceName}</div>
</>
)}
<FontAwesomeIcon
icon={faAngleRight}
className='ml-3 text-sm text-gray-400 mr-3'
/>
<div className='text-gray-400 text-sm'>{pageName}</div>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
<div className="text-gray-400 text-sm">{pageName}</div>
</div>
);
}

View File

@ -1,49 +1,49 @@
import React, { useState } from "react";
import ReactCodeInput from "react-code-input";
import { useTranslation } from "next-i18next";
/* eslint-disable react/jsx-props-no-spreading */
import React, { useState } from 'react';
import ReactCodeInput from 'react-code-input';
import { useTranslation } from 'next-i18next';
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
import Button from "../basic/buttons/Button";
import Error from "../basic/Error";
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
// The style for the verification code input
const props = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "55px",
borderRadius: "5px",
fontSize: "24px",
height: "55px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
},
fontFamily: 'monospace',
margin: '4px',
MozAppearance: 'textfield',
width: '55px',
borderRadius: '5px',
fontSize: '24px',
height: '55px',
paddingLeft: '7',
backgroundColor: '#0d1117',
color: 'white',
border: '1px solid #2d2f33',
textAlign: 'center',
outlineColor: '#8ca542',
borderColor: '#2d2f33'
}
} as const;
const propsPhone = {
inputStyle: {
fontFamily: "monospace",
margin: "4px",
MozAppearance: "textfield",
width: "40px",
borderRadius: "5px",
fontSize: "24px",
height: "40px",
paddingLeft: "7",
backgroundColor: "#0d1117",
color: "white",
border: "1px solid #2d2f33",
textAlign: "center",
outlineColor: "#8ca542",
borderColor: "#2d2f33"
},
fontFamily: 'monospace',
margin: '4px',
MozAppearance: 'textfield',
width: '40px',
borderRadius: '5px',
fontSize: '24px',
height: '40px',
paddingLeft: '7',
backgroundColor: '#0d1117',
color: 'white',
border: '1px solid #2d2f33',
textAlign: 'center',
outlineColor: '#8ca542',
borderColor: '#2d2f33'
}
} as const;
interface CodeInputStepProps {
@ -55,17 +55,21 @@ interface CodeInputStepProps {
/**
* 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 {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 {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 [isResendingVerificationEmail, setIsResendingVerificationEmail] =
useState(false);
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
const { t } = useTranslation();
const resendVerificationEmail = async () => {
@ -80,12 +84,8 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
return (
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
<p className="text-l flex justify-center text-bunker-300">
{t("signup:step2-message")}
</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">
{email}{" "}
</p>
<p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
<div className="hidden md:block">
<ReactCodeInput
name=""
@ -108,28 +108,28 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
className="mt-2 mb-6"
/>
</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">
<Button
text={t("signup:verify") ?? ""}
onButtonPressed={incrementStep}
size="lg"
/>
<Button text={t('signup:verify') ?? ''} onButtonPressed={incrementStep} size="lg" />
</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-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">
{t("signup:step2-resend-alert")}
</span>
<u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}>
<button disabled={isLoading} onClick={resendVerificationEmail}>
{isResendingVerificationEmail ? t("signup:step2-resend-progress") : t("signup:step2-resend-submit")}
<span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
<u
className={`font-normal ${
isResendingVerificationEmail
? 'text-bunker-400'
: 'text-primary-700 hover:text-primary duration-200'
}`}
>
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
{isResendingVerificationEmail
? t('signup:step2-resend-progress')
: t('signup:step2-resend-submit')}
</button>
</u>
</div>
<p className="text-sm text-bunker-400 pb-2">
{t("signup:step2-spam-alert")}
</p>
<p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
</div>
</div>
);

View File

@ -25,37 +25,37 @@ export default function DonwloadBackupPDFStep({
incrementStep,
email,
password,
name,
name
}: DownloadBackupPDFStepProps): JSX.Element {
const { t } = useTranslation();
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'>
<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'>
<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">
{t('signup:step4-message')}
</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 className='mt-3'>{t('signup:step4-description2')}</div>
<div className="mt-3">{t('signup:step4-description2')}</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'>
<FontAwesomeIcon icon={faWarning} className='ml-2 mr-4 text-4xl' />
<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" />
{t('signup:step4-description3')}
</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
text='Download PDF'
text="Download PDF"
onButtonPressed={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: (value: boolean) => {},
setBackupKeyIssued: (value: boolean) => {},
setBackupKeyError: () => {},
setBackupKeyIssued: () => {}
});
incrementStep();
}}
size='lg'
size="lg"
/>
</div>
</div>

View File

@ -1,30 +1,33 @@
import React, { useState } from "react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import React, { useState } from 'react';
import Link from 'next/link';
import { useTranslation } from 'next-i18next';
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
import Button from "../basic/buttons/Button";
import InputField from "../basic/InputField";
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField';
interface DownloadBackupPDFStepProps {
incrementStep: () => void;
email: string;
email: string;
setEmail: (value: string) => void;
}
/**
* 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 {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
* @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 [emailErrorMessage, setEmailErrorMessage] = useState("");
const [emailErrorMessage, setEmailErrorMessage] = useState('');
const { t } = useTranslation();
/**
@ -34,15 +37,11 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
let emailCheckBool = false;
if (!email) {
setEmailError(true);
setEmailErrorMessage("Please enter your email.");
setEmailErrorMessage('Please enter your email.');
emailCheckBool = true;
} else if (
!email.includes("@") ||
!email.includes(".") ||
!/[a-z]/.test(email)
) {
} else if (!email.includes('@') || !email.includes('.') || !/[a-z]/.test(email)) {
setEmailError(true);
setEmailErrorMessage("Please enter a valid email.");
setEmailErrorMessage('Please enter a valid email.');
emailCheckBool = true;
} else {
setEmailError(false);
@ -57,13 +56,13 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
return (
<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">
{t("signup:step1-start")}
{t('signup:step1-start')}
</p>
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
<InputField
label={t("common:email") ?? ""}
label={t('common:email') ?? ''}
onChangeHandler={setEmail}
type="email"
value={email}
@ -75,11 +74,14 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
/>
</div>
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
<p className="text-gray-400 mt-2 md:mx-0.5">
{t("signup:step1-privacy")}
</p>
<p className="text-gray-400 mt-2 md:mx-0.5">{t('signup:step1-privacy')}</p>
<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>
@ -87,7 +89,7 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
<Link href="/login">
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
<u className="font-normal text-sm text-primary-500">
{t("signup:already-have-account")}
{t('signup:already-have-account')}
</u>
</button>
</Link>

View File

@ -1,18 +1,17 @@
import React, { useState } from "react";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import React, { useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import addUserToOrg from "~/pages/api/organization/addUserToOrg";
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
import Button from "../basic/buttons/Button";
import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
import Button from '../basic/buttons/Button';
/**
* This is the last step of the signup flow. People can optionally invite their teammates here.
*/
export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState("");
const [emails, setEmails] = useState('');
const { t } = useTranslation();
const router = useRouter();
@ -20,34 +19,31 @@ export default function TeamInviteStep(): JSX.Element {
const redirectToHome = async () => {
const userWorkspaces = await getWorkspaces();
const userWorkspace = userWorkspaces[0]._id;
router.push("/home/" + userWorkspace);
router.push(`/home/${userWorkspace}`);
};
}
const inviteUsers = async ({ emails }: { emails: string; }) => {
emails
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
inviteEmails
.split(',')
.map(email => email.trim())
.map(async (email) => await addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
.map((email) => email.trim())
.map(async (email) => addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
await redirectToHome();
}
};
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">
<p className="text-4xl font-semibold flex justify-center text-primary">
{t("signup:step5-invite-team")}
{t('signup:step5-invite-team')}
</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">
{t("signup:step5-subtitle")}
{t('signup:step5-subtitle')}
</p>
<div>
<div className="overflow-auto bg-bunker-800">
<div className="whitespace-pre-wrap break-words bg-transparent">
</div>
<div className="whitespace-pre-wrap break-words bg-transparent" />
</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"
value={emails}
onChange={(e) => setEmails(e.target.value)}
@ -55,15 +51,18 @@ export default function TeamInviteStep(): JSX.Element {
/>
</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
<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"
onClick={redirectToHome}
>
{t("signup:step5-skip")}
{t('signup:step5-skip')}
</div>
<Button
text={t("signup:step5-send-invites") ?? ""}
onButtonPressed={() => inviteUsers({ emails})}
text={t('signup:step5-send-invites') ?? ''}
onButtonPressed={() => inviteUsers({ emails })}
size="lg"
/>
</div>

View File

@ -3,8 +3,11 @@ import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp';
import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util';
import completeAccountInformationSignup from '~/pages/api/auth/CompleteAccountInformationSignup';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
import Button from '../basic/buttons/Button';
import InputField from '../basic/InputField';
@ -12,9 +15,7 @@ import attemptLogin from '../utilities/attemptLogin';
import passwordCheck from '../utilities/checks/PasswordCheck';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
const nacl = require('tweetnacl');
const jsrp = require('jsrp');
nacl.util = require('tweetnacl-util');
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface UserInfoStepProps {
@ -51,7 +52,7 @@ export default function UserInfoStep({
firstName,
setFirstName,
lastName,
setLastName,
setLastName
}: UserInfoStepProps): JSX.Element {
const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false);
@ -85,7 +86,7 @@ export default function UserInfoStep({
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck,
errorCheck
});
if (!errorCheck) {
@ -93,17 +94,14 @@ export default function UserInfoStep({
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY,
secret: password
.slice(0, 32)
.padStart(
32 + (password.slice(0, 32).length - new Blob([password]).size),
'0'
),
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
}) as { ciphertext: string; iv: string; tag: string };
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
@ -111,50 +109,41 @@ export default function UserInfoStep({
client.init(
{
username: email,
password: password,
password
},
async () => {
client.createVerifier(
async (err: any, result: { salt: string; verifier: string }) => {
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
organizationName: firstName + "'s organization",
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken,
});
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
organizationName: `${firstName}'s organization`,
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken
});
// if everything works, go the main dashboard page.
if (response.status === 200) {
// response = await response.json();
// if everything works, go the main dashboard page.
if (response.status === 200) {
// response = await response.json();
localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
try {
await attemptLogin(
email,
password,
(value: boolean) => {},
router,
true,
false
);
incrementStep();
} catch (error) {
setIsLoading(false);
}
try {
await attemptLogin(email, password, () => {}, router, true, false);
incrementStep();
} catch (error) {
setIsLoading(false);
}
}
);
});
}
);
} else {
@ -163,142 +152,108 @@ export default function UserInfoStep({
};
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'>
<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'>
<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">
{t('signup:step3-message')}
</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
label={t('common:first-name')}
onChangeHandler={setFirstName}
type='name'
type="name"
value={firstName}
isRequired
errorText={
t('common:validate-required', {
name: t('common:first-name'),
name: t('common:first-name')
}) as string
}
error={firstNameError}
autoComplete='given-name'
autoComplete="given-name"
/>
</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
label={t('common:last-name')}
onChangeHandler={setLastName}
type='name'
type="name"
value={lastName}
isRequired
errorText={
t('common:validate-required', {
name: t('common:last-name'),
name: t('common:last-name')
}) as string
}
error={lastNameError}
autoComplete='family-name'
autoComplete="family-name"
/>
</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
label={t('section-password:password')}
onChangeHandler={(password: string) => {
setPassword(password);
onChangeHandler={(pass: string) => {
setPassword(pass);
passwordCheck({
password,
password: pass,
setPasswordErrorLength,
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false,
errorCheck: false
});
}}
type='password'
type="password"
value={password}
isRequired
error={
passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase
}
autoComplete='new-password'
id='new-password'
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
autoComplete="new-password"
id="new-password"
/>
{passwordErrorLength ||
passwordErrorLowerCase ||
passwordErrorNumber ? (
<div className='w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md'>
<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 || passwordErrorLowerCase || passwordErrorNumber ? (
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
<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 ? (
<FontAwesomeIcon
icon={faX}
className='text-md text-red mr-2.5'
/>
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon
icon={faCheck}
className='text-md text-primary mr-2'
/>
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div
className={`${
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section-password:validate-length')}
</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 ? (
<FontAwesomeIcon
icon={faX}
className='text-md text-red mr-2.5'
/>
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon
icon={faCheck}
className='text-md text-primary mr-2'
/>
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div
className={`${
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
>
{t('section-password:validate-case')}
</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 ? (
<FontAwesomeIcon
icon={faX}
className='text-md text-red mr-2.5'
/>
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon
icon={faCheck}
className='text-md text-primary mr-2'
/>
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div
className={`${
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section-password:validate-number')}
</div>
</div>
</div>
) : (
<div className='py-2'></div>
<div className="py-2" />
)}
</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
text={t('signup:signup') ?? ''}
loading={isLoading}
onButtonPressed={signupErrorCheck}
size='lg'
size="lg"
/>
</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 {
static #token = '';
constructor() {}
static setToken(token: string) {
this.#token = token;
static setToken(tokenStr: string) {
this.#token = tokenStr;
}
static async fetchCall(
resource: RequestInfo,
options?: RequestInit | undefined
) {
static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
const req = new Request(resource, options);
if (this.#token == '') {
if (this.#token === '') {
try {
// 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.setToken(await token());
} catch (error) {
console.error("Unauthorized access");
console.error('Unauthorized access');
}
}
if (this.#token) {
req.headers.set('Authorization', 'Bearer ' + this.#token);
req.headers.set('Authorization', `Bearer ${this.#token}`);
}
return fetch(req);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
/* eslint-disable new-cap */
import jsrp from 'jsrp';
import changePassword2 from '~/pages/api/auth/ChangePassword2';
import SRP1 from '~/pages/api/auth/SRP1';
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
import SRP1 from '@app/pages/api/auth/SRP1';
import Aes256Gcm from './aes-256-gcm';
@ -33,15 +34,16 @@ const changePassword = async (
clientOldPassword.init(
{
username: email,
password: currentPassword,
password: currentPassword
},
async () => {
const clientPublicKey = clientOldPassword.getPublicKey();
let serverPublicKey, salt;
let serverPublicKey;
let salt;
try {
const res = await SRP1({
clientPublicKey: clientPublicKey,
clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
@ -57,7 +59,7 @@ const changePassword = async (
clientNewPassword.init(
{
username: email,
password: newPassword,
password: newPassword
},
async () => {
clientNewPassword.createVerifier(async (err, result) => {
@ -67,11 +69,9 @@ const changePassword = async (
secret: newPassword
.slice(0, 32)
.padStart(
32 +
(newPassword.slice(0, 32).length -
new Blob([newPassword]).size),
32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
'0'
),
)
});
if (ciphertext) {
@ -87,18 +87,18 @@ const changePassword = async (
tag,
salt: result.salt,
verifier: result.verifier,
clientProof,
clientProof
});
if (res && res.status == 400) {
if (res && res.status === 400) {
setCurrentPasswordError(true);
} else if (res && res.status == 200) {
} else if (res && res.status === 200) {
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
}
} catch (err) {
} catch (error) {
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';
type encryptAsymmetricProps = {
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
type EncryptAsymmetricProps = {
plaintext: string;
publicKey: string;
privateKey: string;
@ -22,8 +23,8 @@ type encryptAsymmetricProps = {
const encryptAssymmetric = ({
plaintext,
publicKey,
privateKey,
}: encryptAsymmetricProps): {
privateKey
}: EncryptAsymmetricProps): {
ciphertext: string;
nonce: string;
} => {
@ -37,11 +38,11 @@ const encryptAssymmetric = ({
return {
ciphertext: nacl.util.encodeBase64(ciphertext),
nonce: nacl.util.encodeBase64(nonce),
nonce: nacl.util.encodeBase64(nonce)
};
};
type decryptAsymmetricProps = {
type DecryptAsymmetricProps = {
ciphertext: string;
nonce: string;
publicKey: string;
@ -61,8 +62,8 @@ const decryptAssymmetric = ({
ciphertext,
nonce,
publicKey,
privateKey,
}: decryptAsymmetricProps): string => {
privateKey
}: DecryptAsymmetricProps): string => {
const plaintext = nacl.box.open(
nacl.util.decodeBase64(ciphertext),
nacl.util.decodeBase64(nonce),
@ -73,24 +74,23 @@ const decryptAssymmetric = ({
return nacl.util.encodeUTF8(plaintext);
};
type encryptSymmetricProps = {
type EncryptSymmetricProps = {
plaintext: string;
key: string;
};
type encryptSymmetricReturn = {
ciphertext:string;
iv:string;
tag:string;
type EncryptSymmetricReturn = {
ciphertext: string;
iv: string;
tag: string;
};
/**
* Return symmetrically encrypted [plaintext] using [key].
*/
const encryptSymmetric = ({
plaintext,
key,
}: encryptSymmetricProps): encryptSymmetricReturn => {
let ciphertext, iv, tag;
const encryptSymmetric = ({ plaintext, key }: EncryptSymmetricProps): EncryptSymmetricReturn => {
let ciphertext;
let iv;
let tag;
try {
const obj = aes.encrypt({ text: plaintext, secret: key });
ciphertext = obj.ciphertext;
@ -105,11 +105,11 @@ const encryptSymmetric = ({
return {
ciphertext,
iv,
tag,
tag
};
};
type decryptSymmetricProps = {
type DecryptSymmetricProps = {
ciphertext: string;
iv: string;
tag: string;
@ -126,12 +126,7 @@ type decryptSymmetricProps = {
* @param {String} obj.key - 32-byte hex key
*
*/
const decryptSymmetric = ({
ciphertext,
iv,
tag,
key,
}: decryptSymmetricProps): string => {
const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
let plaintext;
try {
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
@ -143,9 +138,4 @@ const decryptSymmetric = ({
return plaintext;
};
export {
decryptAssymmetric,
decryptSymmetric,
encryptAssymmetric,
encryptSymmetric,
};
export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };

View File

@ -1,15 +1,16 @@
import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey';
import SRP1 from '~/pages/api/auth/SRP1';
/* eslint-disable new-cap */
import crypto from 'crypto';
import jsrp from 'jsrp';
import issueBackupPrivateKey from '@app/pages/api/auth/IssueBackupPrivateKey';
import SRP1 from '@app/pages/api/auth/SRP1';
import generateBackupPDF from '../generateBackupPDF';
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 clientKey = new jsrp.client();
const crypto = require('crypto');
interface BackupKeyProps {
email: string;
@ -42,15 +43,16 @@ const issueBackupKey = async ({
clientPassword.init(
{
username: email,
password: password
password
},
async () => {
const clientPublicKey = clientPassword.getPublicKey();
let serverPublicKey, salt;
let serverPublicKey;
let salt;
try {
const res = await SRP1({
clientPublicKey: clientPublicKey
clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
@ -87,9 +89,9 @@ const issueBackupKey = async ({
clientProof
});
if (res?.status == 400) {
if (res?.status === 400) {
setBackupKeyError(true);
} else if (res?.status == 200) {
} else if (res?.status === 200) {
generateBackupPDF({
personalName,
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 item: [string, string, string[]] | [] = [];
// eslint-disable-next-line no-cond-assign
while ((match = LINE.exec(line)) !== null) {
const key = match[1];
@ -58,8 +59,8 @@ export function parseDotEnv(src: ArrayBuffer) {
})
.filter((line) => line.length > 1)
.forEach((line) => {
const [key, value, comments] = line;
object[key as string] = { value, comments };
const [key, value, cmnts] = line;
object[key as string] = { value, comments: cmnts };
});
return object;

View File

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

View File

@ -22,7 +22,7 @@ export const saveTokenToLocalStorage = ({
} catch (err) {
if (err instanceof 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
* @param {object} obj
* @param {object} obj
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
* @returns
* @returns
*/
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter(
(secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal'
const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => {
let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret));
const overridenSecrets = data!.filter((secret) =>
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
: 'personal'
);
if (overridenSecrets.length) {
overridenSecrets.forEach((secret) => {
const index = secrets!.findIndex(
(_secret) => _secret.key === secret.key && (secret.valueOverride == undefined || secret?.value != secret?.valueOverride)
(_secret) =>
_secret.key === secret.key &&
(secret.valueOverride === undefined || secret?.value !== secret?.valueOverride)
);
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;
}
};
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';
/**
* 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 {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;
const secrets = await checkOverrides({ data });
@ -19,21 +17,21 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
(item: SecretDataProps) =>
`${
item.comment
? item.comment
? `${item.comment
.split('\n')
.map((comment) => '# '.concat(comment))
.join('\n') + '\n'
.join('\n')}\n`
: ''
}` + [item.key, item.value].join('=')
}${[item.key, item.value].join('=')}`
)
.join('\n');
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = env + '.env';
alink.click();
}
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement('a');
alink.href = fileDownloadUrl;
alink.download = `${env}.env`;
alink.click();
};
export default downloadDotEnv;

View File

@ -1,20 +1,20 @@
// import YAML from 'yaml';
// import { YAMLSeq } from 'yaml/types';
import { SecretDataProps } from "public/data/frequentInterfaces";
import { SecretDataProps } from 'public/data/frequentInterfaces';
// import { envMapping } from "../../../public/data/frequentConstants";
// import checkOverrides from './checkOverrides';
/**
* 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 {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;
// const doc = new YAML.Document();
// doc.contents = new YAMLSeq();
@ -27,20 +27,17 @@ const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: strin
// .join('\n');
// doc.add(pair);
// });
// const file = doc
// .toString()
// .split('\n')
// .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
// .join('\n');
// const blob = new Blob([file]);
// const fileDownloadUrl = URL.createObjectURL(blob);
// const alink = document.createElement('a');
// alink.href = fileDownloadUrl;
// alink.download = envMapping[env] + '.yml';
// alink.click();
return;
}
};
export default downloadYaml;

View File

@ -1,15 +1,10 @@
import { SecretDataProps } from "public/data/frequentInterfaces";
import crypto from 'crypto';
import getLatestFileKey from "~/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 getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import { decryptAssymmetric, encryptSymmetric } from '../cryptography/crypto';
interface EncryptedSecretProps {
id: string;
@ -24,22 +19,30 @@ interface EncryptedSecretProps {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
type: 'personal' | 'shared';
}
/**
* 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.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;
try {
const sharedKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
let randomBytes: string;
if (Object.keys(sharedKey).length > 0) {
@ -48,42 +51,42 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
ciphertext: sharedKey.latestKey.encryptedKey,
nonce: sharedKey.latestKey.nonce,
publicKey: sharedKey.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY,
privateKey: PRIVATE_KEY
});
} else {
// 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) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag,
tag: secretKeyTag
} = encryptSymmetric({
plaintext: secret.key,
key: randomBytes,
key: randomBytes
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag,
tag: secretValueTag
} = encryptSymmetric({
plaintext: secret.value,
key: randomBytes,
key: randomBytes
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag,
tag: secretCommentTag
} = encryptSymmetric({
plaintext: secret.comment ?? '',
key: randomBytes,
key: randomBytes
});
const result: EncryptedSecretProps = {
@ -99,17 +102,19 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal',
type:
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
? 'shared'
: 'personal'
};
return result;
});
} catch (error) {
console.log("Error while encrypting secrets");
console.log('Error while encrypting secrets');
}
return secrets;
}
};
export default encryptSecrets;

View File

@ -1,12 +1,7 @@
import getSecrets from '~/pages/api/files/GetSecrets';
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
import getSecrets from '@app/pages/api/files/GetSecrets';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
const {
decryptAssymmetric,
decryptSymmetric
} = require('../cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
import { decryptAssymmetric, decryptSymmetric } from '../cryptography/crypto';
interface EncryptedSecretProps {
_id: string;
@ -21,14 +16,14 @@ interface EncryptedSecretProps {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
type: "personal" | "shared";
type: 'personal' | 'shared';
}
interface SecretProps {
key: string;
value: string;
type: 'personal' | 'shared';
comment: string;
interface SecretProps {
key: string;
value: string;
type: 'personal' | 'shared';
comment: string;
id: string;
}
@ -61,11 +56,11 @@ const getSecretsForProject = async ({
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
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[] = [];
if (latestKey) {
@ -78,7 +73,7 @@ const getSecretsForProject = async ({
});
// decrypt secret keys, values, and comments
encryptedSecrets.map((secret: EncryptedSecretProps) => {
encryptedSecrets.forEach((secret: EncryptedSecretProps) => {
const plainTextKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
@ -102,7 +97,7 @@ const getSecretsForProject = async ({
key
});
} else {
plainTextComment = "";
plainTextComment = '';
}
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) => {
return {
id: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.id,
idOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.id,
pos: index,
key: key,
value: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.value,
valueOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.value,
comment: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.comment,
}
});
const result = secretKeys.map((key, index) => ({
id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
?.id,
idOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'personal'
)[0]?.id,
pos: index,
key,
value: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.value,
valueOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'personal'
)[0]?.value,
comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.comment
}));
setData(result);
return result;

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import SecurityClient from '~/utilities/SecurityClient';
import SecurityClient from '@app/components/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
interface WorkspaceProps {
workspaceId: string;
offset: number;
limit: number;
userId: string;
@ -12,48 +11,53 @@ interface workspaceProps {
/**
* This function fetches the activity logs for a certain project
* @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.limit - how many logs will we output
* @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
* @returns
*/
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
const getProjectLogs = async ({
workspaceId,
offset,
limit,
userId,
actionNames
}: WorkspaceProps) => {
let payload;
if (userId != "" && actionNames != '') {
if (userId !== '' && actionNames !== '') {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
userId: JSON.stringify(userId),
actionNames: actionNames
}
} else if (userId != "") {
actionNames
};
} else if (userId !== '') {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
userId: JSON.stringify(userId)
}
} else if (actionNames != "") {
};
} else if (actionNames !== '') {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent',
actionNames: actionNames
}
actionNames
};
} else {
payload = {
offset: String(offset),
limit: String(limit),
sortBy: 'recent'
}
sortBy: 'recent'
};
}
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/logs?' +
new URLSearchParams(payload),
`/api/v1/workspace/${workspaceId}/logs?${new URLSearchParams(payload)}`,
{
method: 'GET',
headers: {
@ -61,11 +65,11 @@ const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames
}
}
).then(async (res) => {
if (res && res.status == 200) {
if (res && res.status === 200) {
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 {
workspaceId: string;
interface WorkspaceProps {
workspaceId: string;
offset: number;
limit: number;
}
@ -10,30 +9,29 @@ interface workspaceProps {
/**
* This function fetches the secret snapshots for a certain project
* @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.limit - how many snapshots will we output
* @returns
*/
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' +
new URLSearchParams({
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: WorkspaceProps) =>
SecurityClient.fetchCall(
`/api/v1/workspace/${workspaceId}/secret-snapshots?${new URLSearchParams({
offset: String(offset),
limit: String(limit)
}), {
})}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
if (res && res.status === 200) {
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;

View File

@ -1,31 +1,27 @@
import SecurityClient from '~/utilities/SecurityClient';
import SecurityClient from '@app/components/utilities/SecurityClient';
interface workspaceProps {
workspaceId: string;
interface WorkspaceProps {
workspaceId: string;
}
/**
* This function fetches the count of secret snapshots for a certain project
* @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
*/
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
const getProjectSercetSnapshotsCount = async ({ workspaceId }: WorkspaceProps) =>
SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/count`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
).then(async (res) => {
if (res && res.status == 200) {
}).then(async (res) => {
if (res && res.status === 200) {
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;

View File

@ -1,8 +1,7 @@
import SecurityClient from '~/utilities/SecurityClient';
import SecurityClient from '@app/components/utilities/SecurityClient';
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
* @returns
*/
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => {
return SecurityClient.fetchCall(
'/api/v1/secret-snapshot/' + secretSnapshotId, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) =>
SecurityClient.fetchCall(`/api/v1/secret-snapshot/${secretSnapshotId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
).then(async (res) => {
if (res && res.status == 200) {
}).then(async (res) => {
if (res && res.status === 200) {
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;

View File

@ -1,9 +1,8 @@
import SecurityClient from '~/utilities/SecurityClient';
import SecurityClient from '@app/components/utilities/SecurityClient';
interface secretVersionProps {
secretId: string;
offset: number;
interface SecretVersionProps {
secretId: string;
offset: number;
limit: number;
}
@ -15,13 +14,12 @@ interface secretVersionProps {
* @param {number} obj.limit - how far our query goes
* @returns
*/
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
return SecurityClient.fetchCall(
'/api/v1/secret/' + secretId + '/secret-versions?' +
new URLSearchParams({
offset: String(offset),
limit: String(limit)
}),
const getSecretVersions = async ({ secretId, offset, limit }: SecretVersionProps) =>
SecurityClient.fetchCall(
`/api/v1/secret/${secretId}/secret-versions?${new URLSearchParams({
offset: String(offset),
limit: String(limit)
})}`,
{
method: 'GET',
headers: {
@ -29,12 +27,11 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
}
}
).then(async (res) => {
if (res && res.status == 200) {
return await res.json();
} else {
console.log('Failed to get secret version history');
if (res && res.status === 200) {
return res.json();
}
console.log('Failed to get secret version history');
return undefined;
});
};
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
@ -7,24 +7,27 @@ import SecurityClient from '~/utilities/SecurityClient';
* @param {number} obj.version - version to which we are rolling back
* @returns
*/
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version
})
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to perform the secret rollback');
const performSecretRollback = async ({
workspaceId,
version
}: {
workspaceId: string;
version: number;
}) =>
SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
version
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log('Failed to perform the secret rollback');
return undefined;
});
};
export default performSecretRollback;

View File

@ -1,27 +1,22 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import getActionData from "@app/ee/api/secrets/GetActionData";
import patienceDiff from '@app/ee/utilities/findTextDifferences';
import { useEffect, useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import getActionData from '@app/ee/api/secrets/GetActionData';
import patienceDiff from '@app/ee/utilities/findTextDifferences';
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
import DashboardInputField from '../../components/dashboard/DashboardInputField';
const {
import {
decryptAssymmetric,
decryptSymmetric
} = require('../../components/utilities/cryptography/crypto');
const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');
} from '../../components/utilities/cryptography/crypto';
interface SideBarProps {
toggleSidebar: (value: string) => void;
toggleSidebar: (value: string) => void;
currentAction: string;
}
@ -41,11 +36,11 @@ interface DecryptedSecretProps {
newSecretVersion: {
key: string;
value: string;
}
};
oldSecretVersion: {
key: string;
value: string;
}
};
}
interface ActionProps {
@ -58,10 +53,7 @@ interface ActionProps {
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
* @returns the sidebar with the payload of user activity logs
*/
const ActivitySideBar = ({
toggleSidebar,
currentAction
}: SideBarProps) => {
const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
const { t } = useTranslation();
const router = useRouter();
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
@ -72,7 +64,7 @@ const ActivitySideBar = ({
const getLogData = async () => {
setIsLoading(true);
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');
// #TODO: make this a separate function and reuse across the app
@ -86,12 +78,12 @@ const ActivitySideBar = ({
privateKey: String(PRIVATE_KEY)
});
}
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
newSecretVersion?: SecretProps;
oldSecretVersion?: SecretProps;
}) => {
return {
const decryptedSecretVersions = tempActionData.payload.secretVersions.map(
(encryptedSecretVersion: {
newSecretVersion?: SecretProps;
oldSecretVersion?: SecretProps;
}) => ({
newSecretVersion: {
key: decryptSymmetric({
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
@ -107,79 +99,134 @@ const ActivitySideBar = ({
})
},
oldSecretVersion: {
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
key: decryptedLatestKey
}): undefined,
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
key: decryptedLatestKey
})
: undefined,
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
key: decryptedLatestKey
}): undefined
? decryptSymmetric({
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
key: decryptedLatestKey
})
: undefined
}
}
})
})
);
setActionData(decryptedSecretVersions);
setActionMetaData({name: tempActionData.name});
setActionMetaData({ name: tempActionData.name });
setIsLoading(false);
}
};
getLogData();
}, [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`}>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
></Image>
</div>
) : (
<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' onClick={() => toggleSidebar("")}>
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
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 ? (
<div className="flex items-center justify-center h-full mb-8">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
/>
</div>
) : (
<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 className='flex flex-col px-4'>
{(actionMetaData?.name == 'readSecrets'
|| 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>
)}
</div>
);
};
export default ActivitySideBar;

View File

@ -1,24 +1,21 @@
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
import React, { useState } from 'react';
import Image from 'next/image';
import { useTranslation } from "next-i18next";
import timeSince from '@app/ee/utilities/timeSince';
import {
faAngleDown,
faAngleRight,
faUpRightFromSquare
} from '@fortawesome/free-solid-svg-icons';
import { useTranslation } from 'next-i18next';
import { faAngleDown, faAngleRight, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import timeSince from '@app/ee/utilities/timeSince';
import guidGenerator from '../../components/utilities/randomId';
interface PayloadProps {
_id: string;
name: string;
name: string;
secretVersions: string[];
}
interface logData {
interface LogData {
_id: string;
channel: string;
createdAt: string;
@ -27,15 +24,20 @@ interface logData {
payload: PayloadProps[];
}
/**
* This is a single row of the activity table
* @param obj
* @param {logData} obj.row - data for a certain event
* @param obj
* @param {LogData} obj.row - data for a certain 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 { t } = useTranslation();
@ -43,6 +45,7 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
<>
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
<td
onKeyDown={() => null}
onClick={() => setPayloadOpened(!payloadOpened)}
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
>
@ -54,40 +57,58 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
/>
</td>
<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 ")}
</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}
{row.payload
?.map(
(action) =>
`${String(action.secretVersions.length)} ${t(`activity:event.${action.name}`)}`
)
.join(' and ')}
</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">
{timeSince(new Date(row.createdAt))}
</td>
</tr>
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>{String(t("common:timestamp"))}</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>
{payloadOpened && (
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
<td />
<td>{String(t('common:timestamp'))}</td>
<td>{row.createdAt}</td>
</tr>
})}
)}
{payloadOpened &&
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
<td></td>
<td>{String(t("activity:ip-address"))}</td>
<td>{row.ipAddress}</td>
</tr>}
row.payload?.map(
(action) =>
action.secretVersions.length > 0 && (
<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 +121,61 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
* @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();
return (
<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="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">
<thead className="text-bunker-300">
<tr className='text-sm'>
<th className="text-left pl-6 pt-2.5 pb-3"></th>
<th className="text-left font-semibold pt-2.5 pb-3">{String(t("common:event")).toUpperCase()}</th>
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{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></th>
<tr className="text-sm">
<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 pl-6 pt-2.5 pb-3">
{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>
</thead>
<tbody>
{data?.map((row, index) => {
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
})}
{data?.map((row, index) => (
<ActivityLogsRow
key={`activity.${index + 1}.${row._id}`}
row={row}
toggleSidebar={toggleSidebar}
/>
))}
</tbody>
</table>
</div>
{isLoading && <div className='w-full flex justify-center mb-8 mt-4'><Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="loading animation"
></Image></div>}
{isLoading && (
<div className="w-full flex justify-center mb-8 mt-4">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="loading animation"
/>
</div>
)}
</div>
);
};

View File

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

View File

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

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 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)
* @returns
* @returns
*/
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
/**
* findUnique finds all unique values in arr[lo..hi], inclusive. This
* function is used in preparation for determining the longest common
* subsequence. Specifically, it first reduces the array range in question
* to unique values.
* @param chars - an array of characters
* @param lo
* @param hi
* @param lo
* @param hi
* @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) {
const characterMap = new Map();
function findUnique(chars: string[], lo: number, hi: number) {
const characterMap = new Map();
for (let i=lo; i<=hi; 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 });
}
}
for (let i = lo; i <= hi; i += 1) {
const character = chars[i];
characterMap.forEach((val, key, map) => {
if (val.count !== 1) {
map.delete(key);
} else {
map.set(key, val.index);
}
});
if (characterMap.has(character)) {
characterMap.get(character).count += 1;
characterMap.get(character).index = i;
} else {
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 aLo
* @param aHi
* @param bArray
* @param bLo
* @param bHi
* @param aArray
* @param aLo
* @param aHi
* @param bArray
* @param bLo
* @param bHi
* @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
* the matching unique lines.
*
*
*/
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
const ma = findUnique(aArray, aLo, aHi);
const mb = findUnique(bArray, bLo, bHi);
function uniqueCommon(
aArray: string[],
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) => {
if (mb.has(key)) {
map.set(key, {
indexA: val,
indexB: mb.get(key)
});
} else {
map.delete(key);
}
});
ma.forEach((val, key, map) => {
if (mb.has(key)) {
map.set(key, {
indexA: val,
indexB: mb.get(key)
});
} else {
map.delete(key);
}
});
return ma;
}
return ma;
}
/**
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
* and determines the Longest Common Subsequence (LCS).
* @param abMap
* @param abMap
* @returns an ordered array of objects containing the array indexes of the
* matching lines for a LCS.
*/
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
const ja: any = [];
function longestCommonSubsequence(
abMap: Map<number, { indexA: number; indexB: number; prev?: number }>
) {
const ja: any = [];
// First, walk the list creating the jagged array.
abMap.forEach((val, key, map) => {
let i = 0;
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
i++;
}
// First, walk the list creating the jagged array.
abMap.forEach((val, key, map) => {
let i = 0;
if (!ja[i]) {
ja[i] = [];
}
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
i += 1;
}
if (0 < i) {
val.prev = ja[i-1][ja[i - 1].length - 1];
}
ja[i].push(val);
});
if (!ja[i]) {
ja[i] = [];
}
// Now, pull out the longest common subsequence.
let lcs: any[] = [];
if (0 < ja.length) {
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);
}
}
if (i > 0) {
val.prev = ja[i - 1][ja[i - 1].length - 1];
}
ja[i].push(val);
});
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
// lines that are shared between textOld and textNew, and the textNew that were
// inserted.
const result: any[] = [];
let deleted = 0;
let inserted = 0;
if (ja.length > 0) {
const n = ja.length - 1;
lcs = [ja[n][ja[n].length - 1]];
// 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[] = [];
while (lcs[lcs.length - 1].prev) {
lcs.push(lcs[lcs.length - 1].prev);
}
}
return lcs.reverse();
}
// "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
* array captures the diff of the line, aIndex, and bIndex from the textOld
* and textNew array.
* @param aIndex
* @param bIndex
* @param aIndex
* @param bIndex
*/
function addToResult(aIndex: number, bIndex: number) {
if (bIndex < 0) {
aMove.push(textOld[aIndex]);
aMoveIndex.push(result.length);
deleted++;
} else if (aIndex < 0) {
bMove.push(textNew[bIndex]);
bMoveIndex.push(result.length);
inserted++;
}
function addToResult(aIndex: number, bIndex: number) {
if (bIndex < 0) {
aMove.push(textOld[aIndex]);
aMoveIndex.push(result.length);
deleted += 1;
} else if (aIndex < 0) {
bMove.push(textNew[bIndex]);
bMoveIndex.push(result.length);
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,
* this function might recursively call recurseLCS to further match the lines
* between textOld and textNew.
* @param aLo
* @param aHi
* @param bLo
* @param bHi
* @param aLo
* @param aHi
* @param bLo
* @param bHi
*/
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
// Match any lines at the beginning of textOld and textNew.
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
addToResult(aLo++, bLo++);
}
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
// Match any lines at the beginning of textOld and textNew.
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
addToResult(aLo++, bLo++);
}
// 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
// the beginning and the end need to be analyzed first.
const aHiTemp = aHi;
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
aHi--;
bHi--;
}
// 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
// the beginning and the end need to be analyzed first.
// Now, check to determine with the remaining lines in the subsequence
// whether there are any unique common lines between textOld and textNew.
//
// If not, add the subsequence to the result (all textOld having been
// 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);
}
const aHiTemp = aHi;
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
aHi -= 1;
bHi -= 1;
}
while (bLo <= bHi) {
addToResult(-1, bLo++);
}
} else {
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
}
// Now, check to determine with the remaining lines in the subsequence
// whether there are any unique common lines between textOld and textNew.
//
// If not, add the subsequence to the result (all textOld having been
// 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
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
* recursively performs another LCS search (via addSubMatch), until there are
* none found, at which point the subsequence is dumped to the result.
* @param aLo
* @param aHi
* @param bLo
* @param bHi
* @param uniqueCommonMap
* @param aLo
* @param aHi
* @param bLo
* @param bHi
* @param uniqueCommonMap
*/
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
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);
}
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
const x = longestCommonSubsequence(
uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi)
);
let i;
for (i = 0; i < x.length - 1; i++) {
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
}
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);
}
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
}
}
}
let i;
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) {
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0,
aMove: aMove,
aMoveIndex: aMoveIndex,
bMove: bMove,
bMoveIndex: bMoveIndex,
};
}
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
return {
lines: result,
lineCountDeleted: deleted,
lineCountInserted: inserted,
lineCountMoved: 0,
};
if (diffPlusFlag) {
return {
lines: result,
lineCountDeleted: deleted,
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[]) {
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;
let aMoveIndexNext = difference.aMoveIndex;
let bMoveNext = difference.bMove;
let bMoveIndexNext = difference.bMoveIndex;
delete difference.aMove;
delete difference.aMoveIndex;
delete difference.bMove;
delete difference.bMoveIndex;
delete difference.aMove;
delete difference.aMoveIndex;
delete difference.bMove;
delete difference.bMoveIndex;
let lastLineCountMoved;
let lastLineCountMoved;
do {
const aMove = aMoveNext;
const aMoveIndex = aMoveIndexNext;
const bMove = bMoveNext;
const bMoveIndex = bMoveIndexNext;
do {
const aMove = aMoveNext;
const aMoveIndex = aMoveIndexNext;
const bMove = bMoveNext;
const bMoveIndex = bMoveIndexNext;
aMoveNext = [];
aMoveIndexNext = [];
bMoveNext = [];
bMoveIndexNext = [];
aMoveNext = [];
aMoveIndexNext = [];
bMoveNext = [];
bMoveIndexNext = [];
const subDiff = patienceDiff(aMove!, bMove!);
lastLineCountMoved = difference.lineCountMoved;
const subDiff = patienceDiff(aMove!, bMove!);
subDiff.lines.forEach((v, i) => {
lastLineCountMoved = difference.lineCountMoved;
if (0 <= v.aIndex && 0 <= v.bIndex) {
difference.lines[aMoveIndex![v.aIndex]].moved = true;
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
difference.lines[bMoveIndex![v.bIndex]].moved = true;
difference.lineCountInserted--;
difference.lineCountDeleted--;
difference.lineCountMoved++;
} else if (v.bIndex < 0) {
aMoveNext!.push(aMove![v.aIndex]);
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
} else {
bMoveNext!.push(bMove![v.bIndex]);
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
}
});
} while (0 < difference.lineCountMoved - lastLineCountMoved);
return difference;
subDiff.lines.forEach((v, i) => {
if (v.aIndex >= 0 && v.bIndex >= 0) {
difference.lines[aMoveIndex![v.aIndex]].moved = true;
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
difference.lines[bMoveIndex![v.bIndex]].moved = true;
difference.lineCountInserted -= 1;
difference.lineCountDeleted -= 1;
difference.lineCountMoved += 1;
} else if (v.bIndex < 0) {
aMoveNext!.push(aMove![v.aIndex]);
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
} else {
bMoveNext!.push(bMove![v.bIndex]);
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
}
});
} while (difference.lineCountMoved - lastLineCountMoved > 0);
return difference;
}
export default patienceDiff;

View File

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

View File

@ -1,6 +1,6 @@
import { useCallback, useState } from 'react';
interface usePopUpProps {
interface UsePopUpProps {
name: Readonly<string>;
isOpen: boolean;
}
@ -10,18 +10,18 @@ interface usePopUpProps {
* 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)
*/
type usePopUpState<T extends Readonly<string[]> | usePopUpProps[]> = {
[P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: {
type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
[P in T extends UsePopUpProps[] ? T[number]['name'] : T[number]]: {
isOpen: boolean;
data?: unknown;
};
};
interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
popUp: usePopUpState<T>;
handlePopUpOpen: (popUpName: keyof usePopUpState<T>, data?: unknown) => void;
handlePopUpClose: (popUpName: keyof usePopUpState<T>) => void;
handlePopUpToggle: (popUpName: keyof usePopUpState<T>) => void;
interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
popUp: UsePopUpState<T>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
handlePopUpClose: (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
* @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
): usePopUpReturn<T> => {
const [popUp, setPopUp] = useState<usePopUpState<T>>(
): UsePopUpReturn<T> => {
const [popUp, setPopUp] = useState<UsePopUpState<T>>(
Object.fromEntries(
popUpNames.map((popUpName) =>
typeof popUpName === 'string'
? [popUpName, { isOpen: false }]
: [popUpName.name, { isOpen: popUpName.isOpen }]
) // 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(
(popUpName: keyof usePopUpState<T>, data?: unknown) => {
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } }));
},
[]
);
const handlePopUpClose = useCallback((popUpName: keyof usePopUpState<T>) => {
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } }));
const handlePopUpOpen = useCallback((popUpName: keyof UsePopUpState<T>, data?: unknown) => {
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: true, data } }));
}, []);
const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => {
setPopUp((popUp) => ({
...popUp,
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
const handlePopUpClose = useCallback((popUpName: keyof UsePopUpState<T>) => {
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: false } }));
}, []);
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,
handlePopUpOpen,
handlePopUpClose,
handlePopUpToggle,
handlePopUpToggle
};
};

View File

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

View File

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

View File

@ -1,18 +1,17 @@
import React, { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useTranslation } from "next-i18next";
import ActivitySideBar from '@app/ee/components/ActivitySideBar';
import { useTranslation } from 'next-i18next';
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 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 getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
import ActivityTable from '../../ee/components/ActivityTable';
interface logData {
interface LogData {
_id: string;
channel: string;
createdAt: string;
@ -25,17 +24,17 @@ interface logData {
name: string;
payload: {
secretVersions: string[];
}
}[]
};
}[];
}
interface PayloadProps {
_id: string;
name: string;
name: string;
secretVersions: string[];
}
interface logDataPoint {
interface LogDataPoint {
_id: string;
channel: string;
createdAt: string;
@ -50,11 +49,11 @@ interface logDataPoint {
export default function Activity() {
const router = useRouter();
const [eventChosen, setEventChosen] = useState('');
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 10;
const [currentSidebarAction, toggleSidebar] = useState<string>()
const [currentSidebarAction, toggleSidebar] = useState<string>();
const { t } = useTranslation();
// this use effect updates the data in case of a new filter being added
@ -62,25 +61,29 @@ export default function Activity() {
setCurrentOffset(0);
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen })
setLogsData(tempLogsData.map((log: logData) => {
return {
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log.user.email,
payload: log.actions.map(action => {
return {
_id: action._id,
name: action.name,
secretVersions: action.payload.secretVersions
}
})
}
}))
const tempLogsData = await getProjectLogs({
workspaceId: String(router.query.id),
offset: 0,
limit: currentLimit,
userId: '',
actionNames: eventChosen
});
setLogsData(
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,
name: action.name,
secretVersions: action.payload.secretVersions
}))
}))
);
setIsLoading(false);
}
};
getLogData();
}, [eventChosen]);
@ -88,58 +91,64 @@ export default function Activity() {
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen })
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
return {
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log.user.email,
payload: log.actions.map(action => {
return {
const tempLogsData = await getProjectLogs({
workspaceId: String(router.query.id),
offset: currentOffset,
limit: currentLimit,
userId: '',
actionNames: eventChosen
});
setLogsData(
logsData.concat(
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,
name: action.name,
secretVersions: action.payload.secretVersions
}
})
}
})))
}))
}))
)
);
setIsLoading(false);
}
};
getLogData();
}, [currentLimit, currentOffset]);
const loadMoreLogs = () => {
setCurrentOffset(currentOffset + currentLimit);
}
};
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
<NavHeader pageName="Project Activity" isProjectRelated={true} />
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
<NavHeader pageName="Project Activity" isProjectRelated />
{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-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>
<p className="mr-4 text-base text-gray-400">
{t("activity:subtitle")}
</p>
<p className="mr-4 text-base text-gray-400">{t('activity:subtitle')}</p>
</div>
<div className="px-6 h-8 mt-2">
<EventFilter
selected={eventChosen}
select={setEventChosen}
/>
<EventFilter selected={eventChosen} select={setEventChosen} />
</div>
<ActivityTable
data={logsData}
toggleSidebar={toggleSidebar}
isLoading={isLoading}
/>
<div className='flex justify-center w-full mb-6'>
<div className='items-center w-60'>
<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"/>
<ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
<div className="flex justify-center w-full mb-6">
<div className="items-center w-60">
<Button
text={String(t('common:view-more'))}
textDisabled={String(t('common:end-of-history'))}
active={logsData.length % 10 === 0}
onButtonPressed={loadMoreLogs}
size="md"
color="mineshaft"
/>
</div>
</div>
</div>
@ -148,4 +157,4 @@ export default function Activity() {
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 {
name: string;
@ -12,11 +12,8 @@ interface Props {
* @param {string} obj.expiresIn - how soon the API key expires in ms
* @returns
*/
const addAPIKey = ({
name,
expiresIn,
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/', {
const addAPIKey = ({ name, expiresIn }: Props) =>
SecurityClient.fetchCall('/api/v2/api-key/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@ -26,12 +23,11 @@ const addAPIKey = ({
expiresIn
})
}).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to add API key');
if (res && res.status === 200) {
return res.json();
}
console.log('Failed to add API key');
return undefined;
});
};
export default addAPIKey;

View File

@ -1,4 +1,4 @@
import SecurityClient from '~/utilities/SecurityClient';
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
apiKeyId: string;
@ -10,21 +10,18 @@ interface Props {
* @param {string} obj.apiKeyId - id of the API key to delete
* @returns
*/
const deleteAPIKey = ({
apiKeyId
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/' + apiKeyId, {
const deleteAPIKey = ({ apiKeyId }: Props) =>
SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
method: 'DELETE',
headers: {
'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;

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
* @param {*} param0
* @returns
*/
const getAPIKeys = () => {
return SecurityClient.fetchCall(
'/api/v2/api-key',
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
const getAPIKeys = () =>
SecurityClient.fetchCall('/api/v2/api-key', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).apiKeyData;
} else {
console.log('Failed to get API keys');
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).apiKeyData;
}
console.log('Failed to get API keys');
return undefined;
});
};
export default getAPIKeys;

View File

@ -1,4 +1,4 @@
import SecurityClient from '~/utilities/SecurityClient';
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
encryptedPrivateKey: string;
@ -14,34 +14,26 @@ interface Props {
* @param {*} clientPublicKey
* @returns
*/
const changePassword2 = ({
encryptedPrivateKey,
iv,
tag,
salt,
verifier,
clientProof
}: Props) => {
return SecurityClient.fetchCall('/api/v1/password/change-password', {
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
SecurityClient.fetchCall('/api/v1/password/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
clientProof: clientProof,
encryptedPrivateKey: encryptedPrivateKey,
iv: iv,
tag: tag,
salt: salt,
verifier: verifier
clientProof,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
})
}).then(async (res) => {
if (res && res.status == 200) {
if (res && res.status === 200) {
return res;
} else {
console.log('Failed to change the password');
}
console.log('Failed to change the password');
return undefined;
});
};
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.
* To do that, we get their tokens from cookies, and verify if they are good.
*/
const checkAuth = async () => {
return SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
const checkAuth = async () =>
SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}).then((res) => res);
};
export default checkAuth;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,24 +13,24 @@ interface Login2Response {
* @returns
*/
const login2 = async (email: string, clientProof: string) => {
const response = await fetch("/api/v1/auth/login2", {
method: "POST",
const response = await fetch('/api/v1/auth/login2', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email,
clientProof,
email,
clientProof
}),
credentials: "include",
credentials: 'include'
});
// 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;
return data;
}
throw new Error("Password verification failed");
throw new Error('Password verification failed');
};
export default login2;

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