mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 15:38:03 -05:00
chore(frontend): fixed all eslint errors
This commit is contained in:
@@ -1,49 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"next",
|
|
||||||
"next/core-web-vitals"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["simple-import-sort", "@typescript-eslint"],
|
|
||||||
"rules": {
|
|
||||||
"react-hooks/exhaustive-deps": "off",
|
|
||||||
"no-unused-vars": "warn",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "warn",
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
"simple-import-sort/exports": "warn",
|
|
||||||
"simple-import-sort/imports": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"groups": [
|
|
||||||
// Node.js builtins. You could also generate this regex if you use a `.js` config.
|
|
||||||
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
|
|
||||||
// Note that if you use the `node:` prefix for Node.js builtins,
|
|
||||||
// you can avoid this complexity: You can simply use "^node:".
|
|
||||||
[
|
|
||||||
"^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)"
|
|
||||||
],
|
|
||||||
// Packages `react` related packages
|
|
||||||
["^react", "^next", "^@?\\w"],
|
|
||||||
// Internal packages.
|
|
||||||
["^~(/.*|$)"],
|
|
||||||
// Relative imports
|
|
||||||
[
|
|
||||||
"^\\.\\.(?!/?$)",
|
|
||||||
"^\\.\\./?$",
|
|
||||||
"^\\./(?=.*/)(?!/?$)",
|
|
||||||
"^\\.(?!/?$)",
|
|
||||||
"^\\./?$"
|
|
||||||
],
|
|
||||||
// Style imports.
|
|
||||||
["^.+\\.?(css|scss)$"]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
72
frontend/.eslintrc.js
Normal file
72
frontend/.eslintrc.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true
|
||||||
|
},
|
||||||
|
extends: ['airbnb', 'airbnb-typescript', 'airbnb/hooks', 'plugin:react/recommended', 'prettier'],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
project: './tsconfig.json',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true
|
||||||
|
},
|
||||||
|
tsconfigRootDir: __dirname
|
||||||
|
},
|
||||||
|
plugins: ['react', 'prettier', 'simple-import-sort', 'import'],
|
||||||
|
rules: {
|
||||||
|
'react/react-in-jsx-scope': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'react-hooks/exhaustive-deps': 'off',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
|
// TODO: This rule will be switched ON after complete revamp of frontend
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'arrow-body-style': 'off',
|
||||||
|
'no-underscore-dangle': ['error', { allow: ['_id'] }],
|
||||||
|
'jsx-a11y/anchor-is-valid': 'off', // all those <a> tags must be converted to label or a p component
|
||||||
|
//
|
||||||
|
'react/require-default-props': 'off',
|
||||||
|
'react/jsx-filename-extension': [1, { extensions: ['.tsx', '.ts'] }],
|
||||||
|
// TODO: turn this rule ON after migration. everything should use arrow functions
|
||||||
|
'react/function-component-definition': [
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
namedComponents: 'arrow-function'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'react/no-unknown-property': ['error', { ignore: ['jsx'] }],
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'simple-import-sort/exports': 'warn',
|
||||||
|
'simple-import-sort/imports': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
groups: [
|
||||||
|
// Node.js builtins. You could also generate this regex if you use a `.js` config.
|
||||||
|
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
|
||||||
|
// Note that if you use the `node:` prefix for Node.js builtins,
|
||||||
|
// you can avoid this complexity: You can simply use "^node:".
|
||||||
|
[
|
||||||
|
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
|
||||||
|
],
|
||||||
|
// Packages `react` related packages
|
||||||
|
['^react', '^next', '^@?\\w'],
|
||||||
|
// Internal packages.
|
||||||
|
['^~(/.*|$)'],
|
||||||
|
// Relative imports
|
||||||
|
['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||||
|
// Style imports.
|
||||||
|
['^.+\\.?(css|scss)$']
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
project: ['./tsconfig.json']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
7
frontend/.prettierrc
Normal file
7
frontend/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
1789
frontend/package-lock.json
generated
1789
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"start:docker": "next build && next start",
|
"start:docker": "next build && next start",
|
||||||
"lint": "next lint",
|
"lint": "eslint --fix --ext js,ts,tsx ./src",
|
||||||
"type-check": "tsc --project tsconfig.json"
|
"type-check": "tsc --project tsconfig.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -62,14 +62,23 @@
|
|||||||
"@types/jsrp": "^0.2.4",
|
"@types/jsrp": "^0.2.4",
|
||||||
"@types/node": "18.11.9",
|
"@types/node": "18.11.9",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||||
"@typescript-eslint/parser": "^5.45.0",
|
"@typescript-eslint/parser": "^5.45.0",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
"eslint": "^8.29.0",
|
"eslint": "^8.32.0",
|
||||||
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
"eslint-config-next": "^13.0.5",
|
"eslint-config-next": "^13.0.5",
|
||||||
|
"eslint-config-prettier": "^8.6.0",
|
||||||
"eslint-import-resolver-typescript": "^3.5.2",
|
"eslint-import-resolver-typescript": "^3.5.2",
|
||||||
|
"eslint-plugin-import": "^2.27.4",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-react": "^7.32.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"postcss": "^8.4.14",
|
"postcss": "^8.4.14",
|
||||||
|
"prettier": "^2.8.3",
|
||||||
"tailwindcss": "^3.1.4",
|
"tailwindcss": "^3.1.4",
|
||||||
"typescript": "^4.9.3"
|
"typescript": "^4.9.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ReactNode, useEffect, useState } from 'react';
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { publicPaths } from '@app/const';
|
||||||
import { publicPaths } from '~/const';
|
import checkAuth from '@app/pages/api/auth/CheckAuth';
|
||||||
import checkAuth from '~/pages/api/auth/CheckAuth';
|
|
||||||
|
|
||||||
// #TODO: finish spinner only when the data loads fully
|
// #TODO: finish spinner only when the data loads fully
|
||||||
// #TODO: Redirect somewhere if the page does not exist
|
// #TODO: Redirect somewhere if the page does not exist
|
||||||
@@ -13,8 +12,36 @@ type Prop = {
|
|||||||
|
|
||||||
export default function RouteGuard({ children }: Prop): JSX.Element {
|
export default function RouteGuard({ children }: Prop): JSX.Element {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [authorized, setAuthorized] = useState(false);
|
const [authorized, setAuthorized] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* redirect to login page if accessing a private page and not logged in
|
||||||
|
*/
|
||||||
|
async function authCheck(url: string) {
|
||||||
|
// Make sure that we don't redirect when the user is on the following pages.
|
||||||
|
const path = `/${url.split('?')[0].split('/')[1]}`;
|
||||||
|
|
||||||
|
// Check if the user is authenticated
|
||||||
|
const response = await checkAuth();
|
||||||
|
// #TODO: figure our why sometimes it doesn't output a response
|
||||||
|
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
|
||||||
|
if (!publicPaths.includes(path)) {
|
||||||
|
try {
|
||||||
|
if (response.status !== 200) {
|
||||||
|
router.push('/login');
|
||||||
|
console.log('Unauthorized to access.');
|
||||||
|
setAuthorized(false);
|
||||||
|
} else {
|
||||||
|
setAuthorized(true);
|
||||||
|
console.log('Authorized to access.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error (probably the authCheck route is stuck again...):', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// on initial load - run auth check
|
// on initial load - run auth check
|
||||||
(async () => {
|
(async () => {
|
||||||
@@ -40,35 +67,5 @@ export default function RouteGuard({ children }: Prop): JSX.Element {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
|
||||||
* redirect to login page if accessing a private page and not logged in
|
|
||||||
*/
|
|
||||||
async function authCheck(url: string) {
|
|
||||||
// Make sure that we don't redirect when the user is on the following pages.
|
|
||||||
const path = '/' + url.split('?')[0].split('/')[1];
|
|
||||||
|
|
||||||
// Check if the user is authenticated
|
|
||||||
const response = await checkAuth();
|
|
||||||
// #TODO: figure our why sometimes it doesn't output a response
|
|
||||||
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
|
|
||||||
if (!publicPaths.includes(path)) {
|
|
||||||
try {
|
|
||||||
if (response.status !== 200) {
|
|
||||||
router.push('/login');
|
|
||||||
console.log('Unauthorized to access.');
|
|
||||||
setAuthorized(false);
|
|
||||||
} else {
|
|
||||||
setAuthorized(true);
|
|
||||||
console.log('Authorized to access.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
'Error (probably the authCheck route is stuck again...):',
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return children as JSX.Element;
|
return children as JSX.Element;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const initPostHog = () => {
|
|||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||||
posthog.init(POSTHOG_API_KEY, {
|
posthog.init(POSTHOG_API_KEY, {
|
||||||
api_host: POSTHOG_HOST
|
api_host: POSTHOG_HOST
|
||||||
});
|
});
|
||||||
@@ -19,4 +19,6 @@ export const initPostHog = () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("posthog err", e)
|
console.log("posthog err", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
export default function Error({ text }: { text: string }): JSX.Element {
|
const Error = ({ text }: { text: string }): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className='relative flex flex-row justify-center m-auto items-center w-fit rounded-full'>
|
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red mt-1.5 mb-2 mx-2" />
|
||||||
icon={faExclamationTriangle}
|
{text && <p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>}
|
||||||
className='text-red mt-1.5 mb-2 mx-2'
|
|
||||||
/>
|
|
||||||
{text && (
|
|
||||||
<p className='relative top-0 text-red mr-2 text-sm py-1'>{text}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Error;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Fragment } from 'react';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import {
|
import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faEye,
|
faEye,
|
||||||
@@ -42,27 +41,20 @@ const eventOptions = [
|
|||||||
* @param {string} obj.selected - the event that is currently selected
|
* @param {string} obj.selected - the event that is currently selected
|
||||||
* @param {function} obj.select - an action that happens when an item is selected
|
* @param {function} obj.select - an action that happens when an item is selected
|
||||||
*/
|
*/
|
||||||
export default function EventFilter({
|
const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
|
||||||
selected,
|
|
||||||
select
|
|
||||||
}: ListBoxProps): JSX.Element {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Listbox value={t("activity:event." + selected)} onChange={select}>
|
<Listbox value={t(`activity:event.${selected}`)} onChange={select}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
|
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
|
||||||
{selected != '' ? (
|
{selected !== '' ? (
|
||||||
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
|
<p className="select-none text-bunker-100">{t(`activity:event.${selected}`)}</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="select-none">{String(t("common:select-event"))}</p>
|
<p className="select-none">{String(t('common:select-event'))}</p>
|
||||||
)}
|
)}
|
||||||
{selected != '' ? (
|
{selected !== '' ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faX} className="pl-2 w-2 p-2" onClick={() => select('')} />
|
||||||
icon={faX}
|
|
||||||
className="pl-2 w-2 p-2"
|
|
||||||
onClick={() => select('')}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
|
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
|
||||||
)}
|
)}
|
||||||
@@ -74,33 +66,29 @@ export default function EventFilter({
|
|||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
{eventOptions.map((event, id) => {
|
{eventOptions.map((event, id) => (
|
||||||
return (
|
<Listbox.Option
|
||||||
<Listbox.Option
|
key={`${event.name}.${id + 1}`}
|
||||||
key={id}
|
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
|
||||||
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
|
selected === t(`activity:event.${event.name}`) && 'bg-mineshaft-700'
|
||||||
selected == t("activity:event." + event.name) && 'bg-mineshaft-700'
|
}`}
|
||||||
}`}
|
value={event.name}
|
||||||
value={event.name}
|
>
|
||||||
>
|
{({ selected: isSelected }) => (
|
||||||
{({ selected }) => (
|
<span
|
||||||
<>
|
className={`block truncate ${isSelected ? 'font-semibold' : 'font-normal'}`}
|
||||||
<span
|
>
|
||||||
className={`block truncate ${
|
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
|
||||||
selected ? 'font-semibold' : 'font-normal'
|
{t(`activity:event.${event.name}`)}
|
||||||
}`}
|
</span>
|
||||||
>
|
)}
|
||||||
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
|
</Listbox.Option>
|
||||||
{t("activity:event." + event.name)}
|
))}
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Listbox.Option>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default EventFilter;
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||||||
import guidGenerator from '../utilities/randomId';
|
import guidGenerator from '../utilities/randomId';
|
||||||
|
|
||||||
interface InputFieldProps {
|
interface InputFieldProps {
|
||||||
static?: boolean;
|
isStatic?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
type: string;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isRequired: boolean;
|
isRequired: boolean;
|
||||||
disabled?: boolean;
|
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
text?: string;
|
text?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -20,46 +19,54 @@ interface InputFieldProps {
|
|||||||
onChangeHandler: (value: string) => void;
|
onChangeHandler: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InputField = (
|
const InputField = ({
|
||||||
props: InputFieldProps &
|
isRequired,
|
||||||
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>
|
label,
|
||||||
) => {
|
onChangeHandler,
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
autoComplete,
|
||||||
|
blurred,
|
||||||
|
error,
|
||||||
|
errorText,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
placeholder,
|
||||||
|
isStatic,
|
||||||
|
text
|
||||||
|
}: InputFieldProps & Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>) => {
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||||
|
|
||||||
if (props.static === true) {
|
if (isStatic === true) {
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col my-2 md:my-4 justify-center w-full max-w-md'>
|
<div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md">
|
||||||
<p className='text-sm font-semibold text-gray-400 mb-0.5'>
|
<p className="text-sm font-semibold text-gray-400 mb-0.5">{label}</p>
|
||||||
{props.label}
|
{text && <p className="text-xs text-gray-400 mb-2">{text}</p>}
|
||||||
</p>
|
|
||||||
{props.text && (
|
|
||||||
<p className='text-xs text-gray-400 mb-2'>{props.text}</p>
|
|
||||||
)}
|
|
||||||
<input
|
<input
|
||||||
onChange={(e) => props.onChangeHandler(e.target.value)}
|
onChange={(e) => onChangeHandler(e.target.value)}
|
||||||
type={props.type}
|
type={type}
|
||||||
placeholder={props.placeholder}
|
placeholder={placeholder}
|
||||||
value={props.value}
|
value={value}
|
||||||
required={props.isRequired}
|
required={isRequired}
|
||||||
className='bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none'
|
className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none"
|
||||||
name={props.name}
|
name={name}
|
||||||
readOnly
|
readOnly
|
||||||
autoComplete={props.autoComplete}
|
autoComplete={autoComplete}
|
||||||
id={props.id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
return (
|
return (
|
||||||
<div className='flex-col w-full'>
|
<div className="flex-col w-full">
|
||||||
<div className='flex flex-row text-mineshaft-300 items-center mb-0.5'>
|
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
|
||||||
<p className='text-sm font-semibold mr-1'>{props.label}</p>
|
<p className="text-sm font-semibold mr-1">{label}</p>
|
||||||
{/* {props.label == "Password" && router.asPath != "/login" && (
|
{/* {label === "Password" && router.asPath !== "/login" && (
|
||||||
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCircleExclamation}
|
icon={faCircleExclamation}
|
||||||
className={`text-sm peer ${
|
className={`text-sm peer ${
|
||||||
props.error && "text-red"
|
error && "text-red"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="absolute hidden peer-hover:block duration-200 w-60 -left-28 -top-2 -translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-200 text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
<span className="absolute hidden peer-hover:block duration-200 w-60 -left-28 -top-2 -translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-200 text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||||
@@ -70,74 +77,69 @@ const InputField = (
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
||||||
props.error ? 'border-red' : 'border-mineshaft-500'
|
error ? 'border-red' : 'border-mineshaft-500'
|
||||||
} rounded-md`}
|
} rounded-md`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
onChange={(e) => props.onChangeHandler(e.target.value)}
|
onChange={(e) => onChangeHandler(e.target.value)}
|
||||||
type={passwordVisible === false ? props.type : 'text'}
|
type={passwordVisible === false ? type : 'text'}
|
||||||
placeholder={props.placeholder}
|
placeholder={placeholder}
|
||||||
value={props.value}
|
value={value}
|
||||||
required={props.isRequired}
|
required={isRequired}
|
||||||
className={`${
|
className={`${
|
||||||
props.blurred
|
blurred
|
||||||
? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400'
|
? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400'
|
||||||
: ''
|
: ''
|
||||||
} ${
|
} ${
|
||||||
props.error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
|
error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
|
||||||
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
|
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
|
||||||
name={props.name}
|
name={name}
|
||||||
spellCheck='false'
|
spellCheck="false"
|
||||||
autoComplete={props.autoComplete}
|
autoComplete={autoComplete}
|
||||||
id={props.id}
|
id={id}
|
||||||
/>
|
/>
|
||||||
{props.label?.includes('Password') && (
|
{label?.includes('Password') && (
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPasswordVisible(!passwordVisible);
|
setPasswordVisible(!passwordVisible);
|
||||||
}}
|
}}
|
||||||
className='absolute self-end mr-3 text-gray-400 cursor-pointer'
|
className="absolute self-end mr-3 text-gray-400 cursor-pointer"
|
||||||
>
|
>
|
||||||
{passwordVisible ? (
|
{passwordVisible ? (
|
||||||
<FontAwesomeIcon icon={faEyeSlash} />
|
<FontAwesomeIcon icon={faEyeSlash} />
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon icon={faEye} />
|
<FontAwesomeIcon icon={faEye} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{props.blurred && (
|
{blurred && (
|
||||||
<div className='peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden'>
|
<div className="peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden">
|
||||||
<p className='ml-2'></p>
|
<p className="ml-2" />
|
||||||
{props.value
|
{value
|
||||||
.split('')
|
.split('')
|
||||||
.slice(0, 54)
|
.slice(0, 54)
|
||||||
.map(() => (
|
.map(() => (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
key={guidGenerator()}
|
key={guidGenerator()}
|
||||||
className='text-xxs mx-0.5'
|
className="text-xxs mx-0.5"
|
||||||
icon={faCircle}
|
icon={faCircle}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* {props.error && (
|
{/* {error && (
|
||||||
<div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end">
|
<div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end">
|
||||||
<Error />
|
<Error />
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
</div>
|
|
||||||
{props.error && (
|
|
||||||
<p className='text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs'>
|
|
||||||
{props.errorText}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
{error && <p className="text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs">{errorText}</p>}
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(InputField);
|
export default memo(InputField);
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
/* eslint-disable no-unexpected-multiline */
|
/* eslint-disable no-unexpected-multiline */
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import crypto from 'crypto';
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from "next-i18next";
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||||
|
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||||
|
import getOrganizationUsers from '@app/pages/api/organization/GetOrgUsers';
|
||||||
|
import getUser from '@app/pages/api/user/getUser';
|
||||||
|
import addUserToWorkspace from '@app/pages/api/workspace/addUserToWorkspace';
|
||||||
|
import createWorkspace from '@app/pages/api/workspace/createWorkspace';
|
||||||
|
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
|
||||||
|
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||||
import {
|
import {
|
||||||
faBookOpen,
|
faBookOpen,
|
||||||
faFileLines,
|
faFileLines,
|
||||||
@@ -11,56 +22,42 @@ import {
|
|||||||
faKey,
|
faKey,
|
||||||
faMobile,
|
faMobile,
|
||||||
faPlug,
|
faPlug,
|
||||||
faUser,
|
faPlus,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
faUser
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import getOrganizations from "~/pages/api/organization/getOrgs";
|
import NavBarDashboard from '../navigation/NavBarDashboard';
|
||||||
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
import onboardingCheck from '../utilities/checks/OnboardingCheck';
|
||||||
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
|
||||||
import getUser from "~/pages/api/user/getUser";
|
import { decryptAssymmetric, encryptAssymmetric } from '../utilities/cryptography/crypto';
|
||||||
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
import Button from './buttons/Button';
|
||||||
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
import AddWorkspaceDialog from './dialog/AddWorkspaceDialog';
|
||||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
import Listbox from './Listbox';
|
||||||
import uploadKeys from "~/pages/api/workspace/uploadKeys";
|
|
||||||
|
|
||||||
import NavBarDashboard from "../navigation/NavBarDashboard";
|
|
||||||
import onboardingCheck from "../utilities/checks/OnboardingCheck";
|
|
||||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptAssymmetric,
|
|
||||||
} from "../utilities/cryptography/crypto";
|
|
||||||
import Button from "./buttons/Button";
|
|
||||||
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
|
|
||||||
import Listbox from "./Listbox";
|
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
const crypto = require("crypto");
|
|
||||||
|
|
||||||
export default function Layout({ children }: LayoutProps) {
|
const Layout = ({ children }: LayoutProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
|
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
|
||||||
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
|
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
|
||||||
const [newWorkspaceName, setNewWorkspaceName] = useState("");
|
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] =
|
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
|
||||||
useState(0);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
function closeModal() {
|
const closeModal = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
function openModal() {
|
const openModal = () => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO: what to do about the fact that 2ids can have the same name
|
// TODO: what to do about the fact that 2ids can have the same name
|
||||||
|
|
||||||
@@ -68,7 +65,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
* When a user creates a new workspace, redirect them to the page of the new workspace.
|
* When a user creates a new workspace, redirect them to the page of the new workspace.
|
||||||
* @param {*} workspaceName
|
* @param {*} workspaceName
|
||||||
*/
|
*/
|
||||||
async function submitModal(workspaceName: string, addAllUsers: boolean) {
|
const submitModal = async (workspaceName: string, addAllUsers: boolean) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// timeout code.
|
// timeout code.
|
||||||
setTimeout(() => setLoading(false), 1500);
|
setTimeout(() => setLoading(false), 1500);
|
||||||
@@ -79,62 +76,49 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
if (!currentWorkspaces.includes(workspaceName)) {
|
if (!currentWorkspaces.includes(workspaceName)) {
|
||||||
const newWorkspace = await createWorkspace({
|
const newWorkspace = await createWorkspace({
|
||||||
workspaceName,
|
workspaceName,
|
||||||
organizationId: tempLocalStorage("orgData.id"),
|
organizationId: tempLocalStorage('orgData.id')
|
||||||
});
|
});
|
||||||
const newWorkspaceId = newWorkspace._id;
|
const newWorkspaceId = newWorkspace._id;
|
||||||
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
|
||||||
|
|
||||||
const myUser = await getUser();
|
const myUser = await getUser();
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
plaintext: randomBytes,
|
plaintext: randomBytes,
|
||||||
publicKey: myUser.publicKey,
|
publicKey: myUser.publicKey,
|
||||||
privateKey: PRIVATE_KEY,
|
privateKey: PRIVATE_KEY
|
||||||
}) as { ciphertext: string; nonce: string };
|
});
|
||||||
|
|
||||||
await uploadKeys(
|
await uploadKeys(newWorkspaceId, myUser._id, ciphertext, nonce);
|
||||||
newWorkspaceId,
|
|
||||||
myUser._id,
|
|
||||||
ciphertext,
|
|
||||||
nonce
|
|
||||||
);
|
|
||||||
|
|
||||||
if (addAllUsers) {
|
if (addAllUsers) {
|
||||||
console.log('adding other users')
|
console.log('adding other users');
|
||||||
const orgUsers = await getOrganizationUsers({
|
const orgUsers = await getOrganizationUsers({
|
||||||
orgId: tempLocalStorage("orgData.id"),
|
orgId: tempLocalStorage('orgData.id')
|
||||||
});
|
});
|
||||||
orgUsers.map(async (user: any) => {
|
orgUsers.map(async (user: any) => {
|
||||||
if (user.status == "accepted" && user.email != myUser.email) {
|
if (user.status === 'accepted' && user.email !== myUser.email) {
|
||||||
const result = await addUserToWorkspace(
|
const result = await addUserToWorkspace(user.user.email, newWorkspaceId);
|
||||||
user.user.email,
|
|
||||||
newWorkspaceId
|
|
||||||
);
|
|
||||||
if (result?.invitee && result?.latestKey) {
|
if (result?.invitee && result?.latestKey) {
|
||||||
const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY");
|
const TEMP_PRIVATE_KEY = tempLocalStorage('PRIVATE_KEY');
|
||||||
|
|
||||||
// assymmetrically decrypt symmetric key with local private key
|
// assymmetrically decrypt symmetric key with local private key
|
||||||
const key = decryptAssymmetric({
|
const key = decryptAssymmetric({
|
||||||
ciphertext: result.latestKey.encryptedKey,
|
ciphertext: result.latestKey.encryptedKey,
|
||||||
nonce: result.latestKey.nonce,
|
nonce: result.latestKey.nonce,
|
||||||
publicKey: result.latestKey.sender.publicKey,
|
publicKey: result.latestKey.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY,
|
privateKey: TEMP_PRIVATE_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
|
||||||
plaintext: key,
|
plaintext: key,
|
||||||
publicKey: result.invitee.publicKey,
|
publicKey: result.invitee.publicKey,
|
||||||
privateKey: PRIVATE_KEY,
|
privateKey: PRIVATE_KEY
|
||||||
}) as { ciphertext: string; nonce: string };
|
});
|
||||||
|
|
||||||
uploadKeys(
|
uploadKeys(newWorkspaceId, result.invitee._id, inviteeCipherText, inviteeNonce);
|
||||||
newWorkspaceId,
|
|
||||||
result.invitee._id,
|
|
||||||
ciphertext,
|
|
||||||
nonce
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -142,12 +126,12 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
setWorkspaceMapping((prevState) => ({
|
setWorkspaceMapping((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
[workspaceName]: newWorkspaceId
|
[workspaceName]: newWorkspaceId
|
||||||
}))
|
}));
|
||||||
setWorkspaceSelected(workspaceName);
|
setWorkspaceSelected(workspaceName);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setNewWorkspaceName("");
|
setNewWorkspaceName('');
|
||||||
} else {
|
} else {
|
||||||
console.error("A project with this name already exists.");
|
console.error('A project with this name already exists.');
|
||||||
setError(true);
|
setError(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -156,37 +140,35 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
setError(true);
|
setError(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const menuItems = useMemo(
|
const menuItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
href:
|
href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
|
||||||
"/dashboard/" +
|
title: t('nav:menu.secrets'),
|
||||||
workspaceMapping[workspaceSelected as any],
|
emoji: <FontAwesomeIcon icon={faKey} />
|
||||||
title: t("nav:menu.secrets"),
|
|
||||||
emoji: <FontAwesomeIcon icon={faKey} />,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/users/" + workspaceMapping[workspaceSelected as any],
|
href: `/users/${workspaceMapping[workspaceSelected as any]}`,
|
||||||
title: t("nav:menu.members"),
|
title: t('nav:menu.members'),
|
||||||
emoji: <FontAwesomeIcon icon={faUser} />,
|
emoji: <FontAwesomeIcon icon={faUser} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/integrations/" + workspaceMapping[workspaceSelected as any],
|
href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
|
||||||
title: t("nav:menu.integrations"),
|
title: t('nav:menu.integrations'),
|
||||||
emoji: <FontAwesomeIcon icon={faPlug} />,
|
emoji: <FontAwesomeIcon icon={faPlug} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/activity/' + workspaceMapping[workspaceSelected as any],
|
href: `/activity/${workspaceMapping[workspaceSelected as any]}`,
|
||||||
title: 'Activity Logs',
|
title: 'Activity Logs',
|
||||||
emoji: <FontAwesomeIcon icon={faFileLines} />
|
emoji: <FontAwesomeIcon icon={faFileLines} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
|
href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
|
||||||
title: t("nav:menu.project-settings"),
|
title: t('nav:menu.project-settings'),
|
||||||
emoji: <FontAwesomeIcon icon={faGear} />,
|
emoji: <FontAwesomeIcon icon={faGear} />
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
[t, workspaceMapping, workspaceSelected]
|
[t, workspaceMapping, workspaceSelected]
|
||||||
);
|
);
|
||||||
@@ -194,61 +176,49 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Put a user in a workspace if they're not in one yet
|
// Put a user in a workspace if they're not in one yet
|
||||||
const putUserInWorkSpace = async () => {
|
const putUserInWorkSpace = async () => {
|
||||||
if (tempLocalStorage("orgData.id") === "") {
|
if (tempLocalStorage('orgData.id') === '') {
|
||||||
const userOrgs = await getOrganizations();
|
const userOrgs = await getOrganizations();
|
||||||
localStorage.setItem("orgData.id", userOrgs[0]._id);
|
localStorage.setItem('orgData.id', userOrgs[0]._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orgUserProjects = await getOrganizationUserProjects({
|
const orgUserProjects = await getOrganizationUserProjects({
|
||||||
orgId: tempLocalStorage("orgData.id"),
|
orgId: tempLocalStorage('orgData.id')
|
||||||
});
|
});
|
||||||
const userWorkspaces = orgUserProjects;
|
const userWorkspaces = orgUserProjects;
|
||||||
if (
|
if (
|
||||||
userWorkspaces.length == 0 &&
|
userWorkspaces.length === 0 &&
|
||||||
router.asPath != "/noprojects" &&
|
router.asPath !== '/noprojects' &&
|
||||||
!router.asPath.includes("home")&&
|
!router.asPath.includes('home') &&
|
||||||
!router.asPath.includes("settings")
|
!router.asPath.includes('settings')
|
||||||
) {
|
) {
|
||||||
router.push("/noprojects");
|
router.push('/noprojects');
|
||||||
} else if (router.asPath != "/noprojects") {
|
} else if (router.asPath !== '/noprojects') {
|
||||||
const intendedWorkspaceId = router.asPath
|
const intendedWorkspaceId = router.asPath
|
||||||
.split("/")
|
.split('/')
|
||||||
[router.asPath.split("/").length - 1].split("?")[0];
|
[router.asPath.split('/').length - 1].split('?')[0];
|
||||||
|
|
||||||
if (
|
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
|
||||||
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId)
|
localStorage.setItem('projectData.id', intendedWorkspaceId);
|
||||||
) {
|
|
||||||
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||||
if (
|
if (
|
||||||
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId) &&
|
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
|
||||||
!userWorkspaces
|
!userWorkspaces
|
||||||
.map((workspace: { _id: string }) => workspace._id)
|
.map((workspace: { _id: string }) => workspace._id)
|
||||||
.includes(intendedWorkspaceId)
|
.includes(intendedWorkspaceId)
|
||||||
) {
|
) {
|
||||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||||
} else {
|
} else {
|
||||||
setWorkspaceMapping(
|
setWorkspaceMapping(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
userWorkspaces.map((workspace: any) => [
|
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
|
||||||
workspace.name,
|
|
||||||
workspace._id,
|
|
||||||
])
|
|
||||||
) as any
|
) as any
|
||||||
);
|
);
|
||||||
setWorkspaceSelected(
|
setWorkspaceSelected(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
userWorkspaces.map((workspace: any) => [
|
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
|
||||||
workspace._id,
|
)[router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]]
|
||||||
workspace.name,
|
|
||||||
])
|
|
||||||
)[
|
|
||||||
router.asPath
|
|
||||||
.split("/")
|
|
||||||
[router.asPath.split("/").length - 1].split("?")[0]
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,31 +232,20 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
if (
|
if (
|
||||||
workspaceMapping[workspaceSelected as any] &&
|
workspaceMapping[workspaceSelected as any] &&
|
||||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||||
router.asPath
|
router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]
|
||||||
.split("/")
|
|
||||||
[router.asPath.split("/").length - 1].split("?")[0]
|
|
||||||
) {
|
) {
|
||||||
localStorage.setItem(
|
localStorage.setItem('projectData.id', `${workspaceMapping[workspaceSelected as any]}`);
|
||||||
"projectData.id",
|
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
|
||||||
`${workspaceMapping[workspaceSelected as any]}`
|
|
||||||
);
|
|
||||||
router.push(
|
|
||||||
"/dashboard/" +
|
|
||||||
workspaceMapping[workspaceSelected as any]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.log(error);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}, [workspaceSelected]);
|
}, [workspaceSelected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="fixed w-full hidden md:block flex flex-col h-screen">
|
<div className="fixed w-full md:block flex flex-col h-screen">
|
||||||
<script
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js" defer />
|
||||||
src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js"
|
|
||||||
defer
|
|
||||||
></script>
|
|
||||||
<NavBarDashboard />
|
<NavBarDashboard />
|
||||||
<div className="flex flex-col md:flex-row flex-1">
|
<div className="flex flex-col md:flex-row flex-1">
|
||||||
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
|
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
|
||||||
@@ -295,7 +254,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
||||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||||
{t("nav:menu.project")}
|
{t('nav:menu.project')}
|
||||||
</div>
|
</div>
|
||||||
{Object.keys(workspaceMapping).length > 0 ? (
|
{Object.keys(workspaceMapping).length > 0 ? (
|
||||||
<Listbox
|
<Listbox
|
||||||
@@ -319,35 +278,27 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
{Object.keys(workspaceMapping).length > 0 &&
|
{Object.keys(workspaceMapping).length > 0 &&
|
||||||
menuItems.map(({ href, title, emoji }) => (
|
menuItems.map(({ href, title, emoji }) => (
|
||||||
<li className="mt-0.5 mx-2" key={title}>
|
<li className="mt-0.5 mx-2" key={title}>
|
||||||
{router.asPath.split("/")[1] === href.split("/")[1] &&
|
{router.asPath.split('/')[1] === href.split('/')[1] &&
|
||||||
(["project", "billing", "org", "personal"].includes(
|
(['project', 'billing', 'org', 'personal'].includes(
|
||||||
router.asPath.split("/")[2]
|
router.asPath.split('/')[2]
|
||||||
)
|
)
|
||||||
? router.asPath.split("/")[2] === href.split("/")[2]
|
? router.asPath.split('/')[2] === href.split('/')[2]
|
||||||
: true) ? (
|
: true) ? (
|
||||||
<div
|
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||||
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||||
>
|
|
||||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
|
|
||||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||||
{emoji}
|
{emoji}
|
||||||
</p>
|
</p>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
) : router.asPath == "/noprojects" ? (
|
) : router.asPath === '/noprojects' ? (
|
||||||
<div
|
<div className="flex p-2.5 text-white text-sm rounded">
|
||||||
className={`flex p-2.5 text-white text-sm rounded`}
|
<p className="w-10 flex items-center justify-center text-lg">{emoji}</p>
|
||||||
>
|
|
||||||
<p className="w-10 flex items-center justify-center text-lg">
|
|
||||||
{emoji}
|
|
||||||
</p>
|
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link href={href}>
|
<Link href={href}>
|
||||||
<div
|
<div className="flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5">
|
||||||
className={`flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5`}
|
|
||||||
>
|
|
||||||
<p className="w-10 flex items-center justify-center text-lg">
|
<p className="w-10 flex items-center justify-center text-lg">
|
||||||
{emoji}
|
{emoji}
|
||||||
</p>
|
</p>
|
||||||
@@ -360,53 +311,47 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full mt-40 mb-4 px-2">
|
<div className="w-full mt-40 mb-4 px-2">
|
||||||
{router.asPath.split("/")[1] === "home" ? (
|
{router.asPath.split('/')[1] === 'home' ? (
|
||||||
<div
|
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||||
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||||
>
|
|
||||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
|
|
||||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||||
<FontAwesomeIcon icon={faBookOpen} />
|
<FontAwesomeIcon icon={faBookOpen} />
|
||||||
</p>
|
</p>
|
||||||
Infisical Guide
|
Infisical Guide
|
||||||
<img
|
<img
|
||||||
src={`/images/progress-${
|
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
|
||||||
totalOnboardingActionsDone == 0 ? "0" : ""
|
totalOnboardingActionsDone === 1 ? '14' : ''
|
||||||
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
|
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
|
||||||
totalOnboardingActionsDone == 2 ? "28" : ""
|
totalOnboardingActionsDone === 3 ? '43' : ''
|
||||||
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
|
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
|
||||||
totalOnboardingActionsDone == 4 ? "57" : ""
|
totalOnboardingActionsDone === 5 ? '71' : ''
|
||||||
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
|
}.svg`}
|
||||||
height={58}
|
height={58}
|
||||||
width={58}
|
width={58}
|
||||||
alt="progress bar"
|
alt="progress bar"
|
||||||
className="absolute right-2 -top-2"
|
className="absolute right-2 -top-2"
|
||||||
></img>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
|
||||||
href={`/home/` + workspaceMapping[workspaceSelected as any]}
|
<div className="relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max">
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max`}
|
|
||||||
>
|
|
||||||
<p className="w-10 flex items-center justify-center text-lg">
|
<p className="w-10 flex items-center justify-center text-lg">
|
||||||
<FontAwesomeIcon icon={faBookOpen} />
|
<FontAwesomeIcon icon={faBookOpen} />
|
||||||
</p>
|
</p>
|
||||||
Infisical Guide
|
Infisical Guide
|
||||||
<img
|
<img
|
||||||
src={`/images/progress-${
|
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
|
||||||
totalOnboardingActionsDone == 0 ? "0" : ""
|
totalOnboardingActionsDone === 1 ? '14' : ''
|
||||||
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
|
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
|
||||||
totalOnboardingActionsDone == 2 ? "28" : ""
|
totalOnboardingActionsDone === 3 ? '43' : ''
|
||||||
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
|
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
|
||||||
totalOnboardingActionsDone == 4 ? "57" : ""
|
totalOnboardingActionsDone === 5 ? '71' : ''
|
||||||
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
|
}.svg`}
|
||||||
height={58}
|
height={58}
|
||||||
width={58}
|
width={58}
|
||||||
alt="progress bar"
|
alt="progress bar"
|
||||||
className="absolute right-2 -top-2"
|
className="absolute right-2 -top-2"
|
||||||
></img>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -426,14 +371,13 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
|
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faMobile} className="text-gray-300 text-7xl mb-8" />
|
||||||
icon={faMobile}
|
|
||||||
className="text-gray-300 text-7xl mb-8"
|
|
||||||
/>
|
|
||||||
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
||||||
{` ${t("common:no-mobile")} `}
|
{` ${t('common:no-mobile')} `}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Layout;
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import React from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Fragment } from 'react';
|
import { faAngleDown, faCheck, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
import {
|
|
||||||
faAngleDown,
|
|
||||||
faCheck,
|
|
||||||
faPlus,
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Listbox, Transition } from '@headlessui/react';
|
import { Listbox, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
@@ -27,72 +22,69 @@ interface ListBoxProps {
|
|||||||
* @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button
|
* @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function ListBox({
|
const ListBox = ({
|
||||||
selected,
|
selected,
|
||||||
onChange,
|
onChange,
|
||||||
data,
|
data,
|
||||||
text,
|
text,
|
||||||
buttonAction,
|
buttonAction,
|
||||||
isFull,
|
isFull
|
||||||
}: ListBoxProps): JSX.Element {
|
}: ListBoxProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Listbox value={selected} onChange={onChange}>
|
<Listbox value={selected} onChange={onChange}>
|
||||||
<div className='relative'>
|
<div className="relative">
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={`text-gray-400 relative ${
|
className={`text-gray-400 relative ${
|
||||||
isFull ? 'w-full' : 'w-52'
|
isFull ? 'w-full' : 'w-52'
|
||||||
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
|
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
|
||||||
>
|
>
|
||||||
<div className='flex flex-row'>
|
<div className="flex flex-row">
|
||||||
{text}
|
{text}
|
||||||
<span className='ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize'>
|
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize">
|
||||||
{' '}
|
{' '}
|
||||||
{selected}
|
{selected}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{data && (
|
{data && (
|
||||||
<div className='cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2'>
|
<div className="cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
<FontAwesomeIcon icon={faAngleDown} className='text-md mr-1.5' />
|
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Listbox.Button>
|
</Listbox.Button>
|
||||||
{data && (
|
{data && (
|
||||||
<Transition
|
<Transition
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
leave='transition ease-in duration-100'
|
leave="transition ease-in duration-100"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className='border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm'>
|
<Listbox.Options className="border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||||
{data.map((person, personIdx) => (
|
{data.map((person, personIdx) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={personIdx}
|
key={`${person}.${personIdx + 1}`}
|
||||||
className={({ active, selected }) =>
|
className={({ active, selected: isSelected }) =>
|
||||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
|
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
|
||||||
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
isSelected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
||||||
} ${
|
} ${
|
||||||
active && !selected
|
active && !isSelected
|
||||||
? 'bg-white/5 text-mineshaft-200 cursor-pointer'
|
? 'bg-white/5 text-mineshaft-200 cursor-pointer'
|
||||||
: 'text-gray-400'
|
: 'text-gray-400'
|
||||||
} `
|
} `
|
||||||
}
|
}
|
||||||
value={person}
|
value={person}
|
||||||
>
|
>
|
||||||
{({ selected }) => (
|
{({ selected: isSelected }) => (
|
||||||
<>
|
<>
|
||||||
<span
|
<span
|
||||||
className={`block truncate text-primary${
|
className={`block truncate text-primary${
|
||||||
selected ? 'font-medium' : 'font-normal'
|
isSelected ? 'font-medium' : 'font-normal'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{person}
|
{person}
|
||||||
</span>
|
</span>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<span className='text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3'>
|
<span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
||||||
icon={faCheck}
|
|
||||||
className='text-md ml-1'
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
@@ -100,13 +92,10 @@ export default function ListBox({
|
|||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
{buttonAction && (
|
{buttonAction && (
|
||||||
<button
|
<button type="button" onClick={buttonAction} className="cursor-pointer w-full">
|
||||||
onClick={buttonAction}
|
<div className="my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2">
|
||||||
className='cursor-pointer w-full'
|
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||||
>
|
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
||||||
<div className='my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2'>
|
|
||||||
<span className='rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4'>
|
|
||||||
<FontAwesomeIcon icon={faPlus} className='text-lg' />
|
|
||||||
</span>
|
</span>
|
||||||
Add Project
|
Add Project
|
||||||
</div>
|
</div>
|
||||||
@@ -118,4 +107,6 @@ export default function ListBox({
|
|||||||
</div>
|
</div>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default ListBox;
|
||||||
|
|||||||
@@ -16,17 +16,12 @@ interface ToggleProps {
|
|||||||
* @param {number} obj.pos - position of a certain secret
|
* @param {number} obj.pos - position of a certain secret
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function Toggle({
|
const Toggle = ({ enabled, setEnabled, addOverride, pos }: ToggleProps): JSX.Element => {
|
||||||
enabled,
|
|
||||||
setEnabled,
|
|
||||||
addOverride,
|
|
||||||
pos,
|
|
||||||
}: ToggleProps): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
if (enabled == false) {
|
if (enabled === false) {
|
||||||
addOverride('', pos);
|
addOverride('', pos);
|
||||||
} else {
|
} else {
|
||||||
addOverride(undefined, pos);
|
addOverride(undefined, pos);
|
||||||
@@ -37,7 +32,7 @@ export default function Toggle({
|
|||||||
enabled ? 'bg-primary' : 'bg-bunker-400'
|
enabled ? 'bg-primary' : 'bg-bunker-400'
|
||||||
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||||
>
|
>
|
||||||
<span className='sr-only'>Enable notifications</span>
|
<span className="sr-only">Enable notifications</span>
|
||||||
<span
|
<span
|
||||||
className={`${
|
className={`${
|
||||||
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
||||||
@@ -45,4 +40,6 @@ export default function Toggle({
|
|||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Toggle;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import React, { ButtonHTMLAttributes } from "react";
|
/* eslint-disable react/button-has-type */
|
||||||
import Image from "next/image";
|
import React, { ButtonHTMLAttributes } from 'react';
|
||||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
import Image from 'next/image';
|
||||||
import {
|
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
FontAwesomeIcon,
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
} from "@fortawesome/react-fontawesome";
|
|
||||||
|
|
||||||
const classNames = require("classnames");
|
const classNames = require('classnames');
|
||||||
|
|
||||||
type ButtonProps = {
|
type ButtonProps = {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -17,7 +16,7 @@ type ButtonProps = {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
iconDisabled?: IconProp;
|
iconDisabled?: IconProp;
|
||||||
textDisabled?: string;
|
textDisabled?: string;
|
||||||
type?: ButtonHTMLAttributes<any>['type'];
|
type?: ButtonHTMLAttributes<any>["type"];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,72 +33,76 @@ type ButtonProps = {
|
|||||||
* @param {string} props.textDisable - text inside the button when it is disabled
|
* @param {string} props.textDisable - text inside the button when it is disabled
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function Button(props: ButtonProps): JSX.Element {
|
const Button = ({
|
||||||
|
active,
|
||||||
|
text,
|
||||||
|
textDisabled,
|
||||||
|
color,
|
||||||
|
size,
|
||||||
|
onButtonPressed,
|
||||||
|
loading,
|
||||||
|
icon,
|
||||||
|
iconDisabled,
|
||||||
|
type = "button",
|
||||||
|
}: ButtonProps): JSX.Element => {
|
||||||
// Check if the button show always be 'active' - then true;
|
// Check if the button show always be 'active' - then true;
|
||||||
// or if it should switch between 'active' and 'disabled' - then give the status
|
// or if it should switch between 'active' and 'disabled' - then give the status
|
||||||
const activityStatus =
|
const activityStatus = active || (text !== '' && textDisabled === undefined);
|
||||||
props.active || (props.text != "" && props.textDisabled == undefined);
|
|
||||||
|
|
||||||
const styleButton = classNames(
|
const styleButton = classNames(
|
||||||
"group m-auto md:m-0 inline-block rounded-md duration-200",
|
'group m-auto md:m-0 inline-block rounded-md duration-200',
|
||||||
|
|
||||||
// Setting background colors and hover modes
|
// Setting background colors and hover modes
|
||||||
props.color == "mineshaft" &&
|
color === 'mineshaft' && activityStatus && 'bg-mineshaft-700 hover:bg-primary',
|
||||||
activityStatus &&
|
color === 'mineshaft' && !activityStatus && 'bg-mineshaft',
|
||||||
"bg-mineshaft-700 hover:bg-primary",
|
(color === 'primary' || !color) && activityStatus && 'bg-primary hover:opacity-80',
|
||||||
props.color == "mineshaft" && !activityStatus && "bg-mineshaft",
|
(color === 'primary' || !color) && !activityStatus && 'bg-primary',
|
||||||
(props.color == "primary" || !props.color) &&
|
color === 'red' && 'bg-red',
|
||||||
activityStatus &&
|
|
||||||
"bg-primary hover:opacity-80",
|
|
||||||
(props.color == "primary" || !props.color) &&
|
|
||||||
!activityStatus &&
|
|
||||||
"bg-primary",
|
|
||||||
props.color == "red" && "bg-red",
|
|
||||||
|
|
||||||
// Changing the opacity when active vs when not
|
// Changing the opacity when active vs when not
|
||||||
activityStatus ? "opacity-100 cursor-pointer" : "opacity-40",
|
activityStatus ? 'opacity-100 cursor-pointer' : 'opacity-40',
|
||||||
|
|
||||||
// Setting the button sizes
|
// Setting the button sizes
|
||||||
props.size == "md" && "h-10 w-full px-2 md:px-4",
|
size === 'md' && 'h-10 w-full px-2 md:px-4',
|
||||||
props.size == "lg" && "h-12 w-full px-2 md:px-8",
|
size === 'lg' && 'h-12 w-full px-2 md:px-8',
|
||||||
!props.size && "md:py-1 px-3 md:px-8",
|
!size && 'md:py-1 px-3 md:px-8',
|
||||||
props.size == "icon-md" && "h-10 w-10 flex items-center justify-center",
|
size === 'icon-md' && 'h-10 w-10 flex items-center justify-center',
|
||||||
props.size == "icon-sm" && "h-9 w-9 flex items-center justify-center"
|
size === 'icon-sm' && 'h-9 w-9 flex items-center justify-center'
|
||||||
);
|
);
|
||||||
|
|
||||||
const styleMainDiv = classNames(
|
const styleMainDiv = classNames(
|
||||||
"relative font-medium flex items-center",
|
'relative font-medium flex items-center',
|
||||||
|
|
||||||
// Setting the text color for the text and icon
|
// Setting the text color for the text and icon
|
||||||
props.color == "mineshaft" && "text-gray-400",
|
color === 'mineshaft' && 'text-gray-400',
|
||||||
props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black",
|
color !== 'mineshaft' && color !== 'red' && color !== 'none' && 'text-black',
|
||||||
props.color == "red" && "text-gray-200",
|
color === 'red' && 'text-gray-200',
|
||||||
props.color == "none" && "text-gray-200 text-xl",
|
color === 'none' && 'text-gray-200 text-xl',
|
||||||
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "",
|
activityStatus && color !== 'red' && color !== 'none' ? 'group-hover:text-black' : '',
|
||||||
|
|
||||||
props.size == "icon" && "flex items-center justify-center"
|
size === 'icon' && 'flex items-center justify-center'
|
||||||
);
|
);
|
||||||
|
|
||||||
const textStyle = classNames(
|
const textStyle = classNames(
|
||||||
"relative duration-200 text-center w-full",
|
'relative duration-200 text-center w-full',
|
||||||
|
|
||||||
// Show the loading sign if the loading indicator is on
|
// Show the loading sign if the loading indicator is on
|
||||||
props.loading ? "opacity-0" : "opacity-100",
|
loading ? 'opacity-0' : 'opacity-100',
|
||||||
props.size == "md" && "text-sm",
|
size === 'md' && 'text-sm',
|
||||||
props.size == "lg" && "text-lg"
|
size === 'lg' && 'text-lg'
|
||||||
);
|
);
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<button
|
<button
|
||||||
disabled={!activityStatus}
|
disabled={!activityStatus}
|
||||||
type={props.type}
|
type={type}
|
||||||
onClick={props.onButtonPressed}
|
onClick={onButtonPressed}
|
||||||
className={styleButton}
|
className={styleButton}
|
||||||
>
|
>
|
||||||
<div className={styleMainDiv}>
|
<div className={styleMainDiv}>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
props.loading == true ? "opacity-100" : "opacity-0"
|
loading === true ? 'opacity-100' : 'opacity-0'
|
||||||
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -107,33 +110,33 @@ export default function Button(props: ButtonProps): JSX.Element {
|
|||||||
height={25}
|
height={25}
|
||||||
width={42}
|
width={42}
|
||||||
alt="loading animation"
|
alt="loading animation"
|
||||||
className={`rounded-xl`}
|
className="rounded-xl"
|
||||||
></Image>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{props.icon && (
|
{icon && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={props.icon}
|
icon={icon}
|
||||||
className={`flex my-auto font-extrabold ${
|
className={`flex my-auto font-extrabold ${size === 'icon-sm' ? 'text-sm' : 'text-sm'} ${
|
||||||
props.size == "icon-sm" ? "text-sm" : "text-sm"
|
(text || textDisabled) && 'mr-2'
|
||||||
} ${(props.text || props.textDisabled) && "mr-2"}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{props.iconDisabled && (
|
{iconDisabled && (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={props.iconDisabled as IconProp}
|
icon={iconDisabled as IconProp}
|
||||||
className={`flex my-auto font-extrabold ${
|
className={`flex my-auto font-extrabold ${size === 'icon-sm' ? 'text-sm' : 'text-md'} ${
|
||||||
props.size == "icon-sm" ? "text-sm" : "text-md"
|
(text || textDisabled) && 'mr-2'
|
||||||
} ${(props.text || props.textDisabled) && "mr-2"}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(props.text || props.textDisabled) && (
|
{(text || textDisabled) && (
|
||||||
<p className={textStyle}>
|
<p className={textStyle}>{activityStatus ? text : textDisabled}</p>
|
||||||
{activityStatus ? props.text : props.textDisabled}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Button;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const ActivateBotDialog = ({
|
|||||||
closeModal,
|
closeModal,
|
||||||
selectedIntegrationOption,
|
selectedIntegrationOption,
|
||||||
handleBotActivate,
|
handleBotActivate,
|
||||||
handleIntegrationOption,
|
handleIntegrationOption
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ const ActivateBotDialog = ({
|
|||||||
if (!selectedIntegrationOption) return;
|
if (!selectedIntegrationOption) return;
|
||||||
// 2. start integration
|
// 2. start integration
|
||||||
await handleIntegrationOption({
|
await handleIntegrationOption({
|
||||||
integrationOption: selectedIntegrationOption,
|
integrationOption: selectedIntegrationOption
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@@ -42,47 +42,44 @@ const ActivateBotDialog = ({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative z-10' onClose={closeModal}>
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as='h3'
|
|
||||||
className='text-lg font-medium leading-6 text-gray-400'
|
|
||||||
>
|
|
||||||
{t('integrations:grant-access-to-secrets')}
|
{t('integrations:grant-access-to-secrets')}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className='mt-2 mb-2'>
|
<div className="mt-2 mb-2">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">
|
||||||
{t('integrations:why-infisical-needs-access')}
|
{t('integrations:why-infisical-needs-access')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-6 max-w-max'>
|
<div className="mt-6 max-w-max">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={submit}
|
onButtonPressed={submit}
|
||||||
color='mineshaft'
|
color="mineshaft"
|
||||||
text={t('integrations:grant-access-button') as string}
|
text={t('integrations:grant-access-button') as string}
|
||||||
size='md'
|
size="md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import addAPIKey from '@app/pages/api/apiKey/addAPIKey';
|
||||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
import addAPIKey from '~/pages/api/apiKey/addAPIKey';
|
|
||||||
|
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
import InputField from '../InputField';
|
import InputField from '../InputField';
|
||||||
import ListBox from '../Listbox';
|
import ListBox from '../Listbox';
|
||||||
@@ -99,7 +98,7 @@ const AddApiKeyDialog = ({
|
|||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom='opacity-100 scale-100'
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo='opacity-0 scale-95'
|
||||||
>
|
>
|
||||||
{apiKey == '' ? (
|
{apiKey === '' ? (
|
||||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as='h3'
|
as='h3'
|
||||||
@@ -135,7 +134,7 @@ const AddApiKeyDialog = ({
|
|||||||
'6 months',
|
'6 months',
|
||||||
'12 months',
|
'12 months',
|
||||||
]}
|
]}
|
||||||
isFull={true}
|
isFull
|
||||||
text={`${t('common:expired-in')}: `}
|
text={`${t('common:expired-in')}: `}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +148,7 @@ const AddApiKeyDialog = ({
|
|||||||
t('section-api-key:add-dialog.add') as string
|
t('section-api-key:add-dialog.add') as string
|
||||||
}
|
}
|
||||||
size='md'
|
size='md'
|
||||||
active={apiKeyName == '' ? false : true}
|
active={apiKeyName !== ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,15 +175,16 @@ const AddApiKeyDialog = ({
|
|||||||
<input
|
<input
|
||||||
type='text'
|
type='text'
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
disabled={true}
|
disabled
|
||||||
id='apiKey'
|
id='apiKey'
|
||||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||||
></input>
|
/>
|
||||||
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||||
{apiKey}
|
{apiKey}
|
||||||
</div>
|
</div>
|
||||||
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import addIncidentContact from '@app/pages/api/organization/addIncidentContact';
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
import addIncidentContact from '~/pages/api/organization/addIncidentContact';
|
|
||||||
|
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
import InputField from '../InputField';
|
import InputField from '../InputField';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
workspaceId: string;
|
|
||||||
incidentContacts: string[];
|
incidentContacts: string[];
|
||||||
setIncidentContacts: (arg: string[]) => void;
|
setIncidentContacts: (arg: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,122 +27,105 @@ const AddProjectMemberDialog = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='z-50'>
|
<div className="z-50">
|
||||||
<Transition appear show={isOpen} as={Fragment}>
|
<Transition appear show={isOpen} as={Fragment}>
|
||||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0'
|
enterFrom="opacity-0"
|
||||||
enterTo='opacity-100'
|
enterTo="opacity-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100'
|
leaveFrom="opacity-100"
|
||||||
leaveTo='opacity-0'
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className='fixed inset-0 overflow-y-auto'>
|
<div className="fixed inset-0 overflow-y-auto">
|
||||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='ease-out duration-300'
|
enter="ease-out duration-300"
|
||||||
enterFrom='opacity-0 scale-95'
|
enterFrom="opacity-0 scale-95"
|
||||||
enterTo='opacity-100 scale-100'
|
enterTo="opacity-100 scale-100"
|
||||||
leave='ease-in duration-200'
|
leave="ease-in duration-200"
|
||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom="opacity-100 scale-100"
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
{data?.length > 0 ? (
|
{data?.length > 0 ? (
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as='h3'
|
as="h3"
|
||||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||||
>
|
>
|
||||||
{t('section-members:add-dialog.add-member-to-project')}
|
{t('section-members:add-dialog.add-member-to-project')}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
) : (
|
) : (
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as='h3'
|
as="h3"
|
||||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||||
>
|
>
|
||||||
{t('section-members:add-dialog.already-all-invited')}
|
{t('section-members:add-dialog.already-all-invited')}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
)}
|
)}
|
||||||
<div className='mt-2 mb-4'>
|
<div className="mt-2 mb-4">
|
||||||
{data?.length > 0 ? (
|
{data?.length > 0 ? (
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">
|
||||||
{t('section-members:add-dialog.user-will-email')}
|
{t('section-members:add-dialog.user-will-email')}
|
||||||
</p>
|
</p>
|
||||||
<div className=''>
|
<div className="">
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey='section-members:add-dialog.looking-add'
|
i18nKey="section-members:add-dialog.looking-add"
|
||||||
components={[
|
components={[
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={() =>
|
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||||
router.push(
|
aria-label="add member"
|
||||||
'/settings/org/' + router.query.id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>,
|
/>,
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
<button
|
<button
|
||||||
type='button'
|
type="button"
|
||||||
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push(
|
router.push(`/settings/org/${router.query.id}?invite`)
|
||||||
'/settings/org/' +
|
|
||||||
router.query.id +
|
|
||||||
'?invite'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>,
|
aria-label="add member"
|
||||||
|
/>
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className='text-sm text-gray-500'>
|
<p className="text-sm text-gray-500">
|
||||||
{t('section-members:add-dialog.add-user-org-first')}
|
{t('section-members:add-dialog.add-user-org-first')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='max-h-28'>
|
<div className="max-h-28">
|
||||||
{data?.length > 0 && (
|
{data?.length > 0 && (
|
||||||
<ListBox
|
<ListBox selected={email || data[0]} onChange={setEmail} data={data} isFull />
|
||||||
selected={email ? email : data[0]}
|
|
||||||
onChange={setEmail}
|
|
||||||
data={data}
|
|
||||||
isFull={true}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='max-w-max'>
|
<div className="max-w-max">
|
||||||
{data?.length > 0 ? (
|
{data?.length > 0 ? (
|
||||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
<div className="mt-6 flex flex-col justify-start w-max">
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={submitModal}
|
onButtonPressed={submitModal}
|
||||||
color='mineshaft'
|
color="mineshaft"
|
||||||
text={t('section-members:add-member') as string}
|
text={t('section-members:add-member') as string}
|
||||||
size='md'
|
size="md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onButtonPressed={() =>
|
onButtonPressed={() => router.push(`/settings/org/${router.query.id}`)}
|
||||||
router.push('/settings/org/' + router.query.id)
|
color="mineshaft"
|
||||||
}
|
text={t('section-members:add-dialog.add-user-to-org') as string}
|
||||||
color='mineshaft'
|
size="md"
|
||||||
text={
|
|
||||||
t(
|
|
||||||
'section-members:add-dialog.add-user-to-org'
|
|
||||||
) as string
|
|
||||||
}
|
|
||||||
size='md'
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ import crypto from 'crypto';
|
|||||||
|
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import addServiceToken from '@app/pages/api/serviceToken/addServiceToken';
|
||||||
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
|
|
||||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
@@ -86,7 +85,7 @@ const AddServiceTokenDialog = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||||
setServiceToken(newServiceToken.serviceToken + '.' + randomBytes);
|
setServiceToken(`${newServiceToken.serviceToken }.${ randomBytes}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
function copyToClipboard() {
|
function copyToClipboard() {
|
||||||
@@ -141,7 +140,7 @@ const AddServiceTokenDialog = ({
|
|||||||
leaveFrom='opacity-100 scale-100'
|
leaveFrom='opacity-100 scale-100'
|
||||||
leaveTo='opacity-0 scale-95'
|
leaveTo='opacity-0 scale-95'
|
||||||
>
|
>
|
||||||
{serviceToken == '' ? (
|
{serviceToken === '' ? (
|
||||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
as='h3'
|
as='h3'
|
||||||
@@ -186,7 +185,7 @@ const AddServiceTokenDialog = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isFull={true}
|
isFull
|
||||||
text={`${t('common:environment')}: `}
|
text={`${t('common:environment')}: `}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +200,7 @@ const AddServiceTokenDialog = ({
|
|||||||
'6 months',
|
'6 months',
|
||||||
'12 months',
|
'12 months',
|
||||||
]}
|
]}
|
||||||
isFull={true}
|
isFull
|
||||||
text={`${t('common:expired-in')}: `}
|
text={`${t('common:expired-in')}: `}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,7 +214,7 @@ const AddServiceTokenDialog = ({
|
|||||||
t('section-token:add-dialog.add') as string
|
t('section-token:add-dialog.add') as string
|
||||||
}
|
}
|
||||||
size='md'
|
size='md'
|
||||||
active={serviceTokenName == '' ? false : true}
|
active={serviceTokenName !== ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,13 +243,14 @@ const AddServiceTokenDialog = ({
|
|||||||
value={serviceToken}
|
value={serviceToken}
|
||||||
id='serviceToken'
|
id='serviceToken'
|
||||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||||
></input>
|
/>
|
||||||
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||||
{serviceToken}
|
{serviceToken}
|
||||||
</div>
|
</div>
|
||||||
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||||
<button
|
<button
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
|
type="button"
|
||||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||||
>
|
>
|
||||||
{serviceTokenCopied ? (
|
{serviceTokenCopied ? (
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ type Props = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
submitModal: (email: string) => void;
|
submitModal: (email: string) => void;
|
||||||
workspaceId: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
setEmail: (email: string) => void;
|
setEmail: (email: string) => void;
|
||||||
currentPlan: string;
|
currentPlan: string;
|
||||||
@@ -82,13 +81,13 @@ const AddUserDialog = ({
|
|||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{currentPlan == STRIPE_PRODUCT_STARTER && (
|
{currentPlan === STRIPE_PRODUCT_STARTER && (
|
||||||
<div className='flex flex-row'>
|
<div className='flex flex-row'>
|
||||||
<button
|
<button
|
||||||
type='button'
|
type='button'
|
||||||
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push('/settings/billing/' + router.query.id)
|
router.push(`/settings/billing/${ router.query.id}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
You can add up to 5 members on a Free tier.
|
You can add up to 5 members on a Free tier.
|
||||||
@@ -97,7 +96,7 @@ const AddUserDialog = ({
|
|||||||
type='button'
|
type='button'
|
||||||
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
router.push('/settings/billing/' + router.query.id)
|
router.push(`/settings/billing/${ router.query.id}`)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Upgrade now.
|
Upgrade now.
|
||||||
|
|||||||
@@ -26,7 +26,14 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} />
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-70"
|
||||||
|
onClick={onClose}
|
||||||
|
onKeyDown={onClose}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label="Close"
|
||||||
|
/>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
@@ -39,10 +46,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
|||||||
leaveTo="opacity-0 scale-95"
|
leaveTo="opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-gray-400"
|
|
||||||
>
|
|
||||||
{t('dashboard:sidebar.delete-key-dialog.title')}
|
{t('dashboard:sidebar.delete-key-dialog.title')}
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import InputField from "../InputField";
|
|||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
submitModal: (userIdToBeDeleted: string) => void;
|
|
||||||
selectedIntegrationOption: string;
|
selectedIntegrationOption: string;
|
||||||
handleBotActivate: () => void;
|
handleBotActivate: () => void;
|
||||||
handleIntegrationOption: (arg:{integrationOption:string})=>void;
|
handleIntegrationOption: (arg:{integrationOption:string})=>void;
|
||||||
@@ -20,6 +19,7 @@ const IntegrationAccessTokenDialog = ({
|
|||||||
handleIntegrationOption
|
handleIntegrationOption
|
||||||
}:Props) => {
|
}:Props) => {
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
// 1. activate bot
|
// 1. activate bot
|
||||||
@@ -85,7 +85,7 @@ const IntegrationAccessTokenDialog = ({
|
|||||||
label="Access token"
|
label="Access token"
|
||||||
onChangeHandler={() => {}}
|
onChangeHandler={() => {}}
|
||||||
type="varName"
|
type="varName"
|
||||||
value={"Hello"}
|
value="Hello"
|
||||||
placeholder=""
|
placeholder=""
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,50 +23,47 @@ interface PopupProps {
|
|||||||
* @param {string} org.setCheckDocsPopUpVisible - the functions that closes the popup
|
* @param {string} org.setCheckDocsPopUpVisible - the functions that closes the popup
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function BottonRightPopup({
|
const BottonRightPopup = ({
|
||||||
buttonText,
|
buttonText,
|
||||||
buttonLink,
|
buttonLink,
|
||||||
titleText,
|
titleText,
|
||||||
emoji,
|
emoji,
|
||||||
textLine1,
|
textLine1,
|
||||||
textLine2,
|
textLine2,
|
||||||
setCheckDocsPopUpVisible,
|
setCheckDocsPopUpVisible
|
||||||
}: PopupProps): JSX.Element {
|
}: PopupProps): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6'
|
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
||||||
role='alert'
|
role="alert"
|
||||||
>
|
>
|
||||||
<div className='flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6'>
|
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||||
<div className='font-bold text-xl mr-2 mt-0.5 flex flex-row'>
|
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
||||||
<div>{titleText}</div>
|
<div>{titleText}</div>
|
||||||
<div className='ml-2.5'>{emoji}</div>
|
<div className="ml-2.5">{emoji}</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
|
||||||
className='mt-1'
|
|
||||||
onClick={() => setCheckDocsPopUpVisible(false)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faXmark}
|
icon={faXmark}
|
||||||
className='text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer'
|
className="text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className='block sm:inline px-6 mt-4 mb-0.5 text-gray-300'>
|
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">{textLine1}</div>
|
||||||
{textLine1}
|
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||||
</div>
|
<div className="flex flex-row px-6 w-full">
|
||||||
<div className='block sm:inline mb-4 px-6'>{textLine2}</div>
|
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||||
<div className='flex flex-row px-6 w-full'>
|
|
||||||
{/*eslint-disable-next-line react/jsx-no-target-blank */}
|
|
||||||
<a
|
<a
|
||||||
className='font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center'
|
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||||
href={buttonLink}
|
href={buttonLink}
|
||||||
target='_blank'
|
target="_blank"
|
||||||
rel='noopener'
|
rel="noopener"
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default BottonRightPopup;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
import deleteAPIKey from '../../../pages/api/apiKey/deleteAPIKey';
|
||||||
|
|
||||||
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
|
|
||||||
import guidGenerator from '../../utilities/randomId';
|
import guidGenerator from '../../utilities/randomId';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
|
|
||||||
@@ -28,49 +27,47 @@ const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
|
|||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
return (
|
return (
|
||||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
|
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
||||||
<table className="w-full my-1">
|
<table className="w-full my-1">
|
||||||
<thead className="text-bunker-300 text-sm font-light">
|
<thead className="text-bunker-300 text-sm font-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
|
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
|
<th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
|
||||||
<th></th>
|
<th aria-label="button" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data?.length > 0 ? (
|
{data?.length > 0 ? (
|
||||||
data?.map((row) => {
|
data?.map((row) => (
|
||||||
return (
|
<tr
|
||||||
<tr
|
key={guidGenerator()}
|
||||||
key={guidGenerator()}
|
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
||||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
>
|
||||||
>
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
{row.name}
|
||||||
{row.name}
|
</td>
|
||||||
</td>
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
{new Date(row.expiresAt).toUTCString()}
|
||||||
{new Date(row.expiresAt).toUTCString()}
|
</td>
|
||||||
</td>
|
<td className="py-2 border-mineshaft-700 border-t">
|
||||||
<td className="py-2 border-mineshaft-700 border-t">
|
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
<Button
|
||||||
<Button
|
onButtonPressed={() => {
|
||||||
onButtonPressed={() => {
|
deleteAPIKey({ apiKeyId: row._id });
|
||||||
deleteAPIKey({ apiKeyId: row._id} );
|
setApiKeys(data.filter((token) => token._id !== row._id));
|
||||||
setApiKeys(data.filter(token => token._id != row._id));
|
createNotification({
|
||||||
createNotification({
|
text: `'${row.name}' API key has been revoked.`,
|
||||||
text: `'${row.name}' API key has been revoked.`,
|
type: 'error'
|
||||||
type: 'error'
|
});
|
||||||
});
|
}}
|
||||||
}}
|
color="red"
|
||||||
color="red"
|
size="icon-sm"
|
||||||
size="icon-sm"
|
icon={faX}
|
||||||
icon={faX}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
))
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
||||||
|
|||||||
@@ -1,31 +1,32 @@
|
|||||||
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
type Props = {
|
type Props = {
|
||||||
addAllUsers: boolean;
|
addAllUsers: boolean;
|
||||||
setAddAllUsers: (arg: boolean) => void;
|
setAddAllUsers: (arg: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => {
|
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
|
||||||
return (
|
<div className="flex flex-row items-center">
|
||||||
<>
|
{addAllUsers === true ? (
|
||||||
<div className='flex flex-row items-center'>
|
<input
|
||||||
{addAllUsers == true ? (
|
type="checkbox"
|
||||||
<input
|
className="accent-primary h-4 w-4"
|
||||||
type='checkbox'
|
checked
|
||||||
className='accent-primary h-4 w-4'
|
readOnly
|
||||||
checked
|
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||||
readOnly
|
/>
|
||||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
) : (
|
||||||
/>
|
<div
|
||||||
) : (
|
onKeyDown={() => setAddAllUsers(!addAllUsers)}
|
||||||
<div
|
role="button"
|
||||||
className='h-4 w-4 bg-bunker border border-gray-600 rounded-sm'
|
tabIndex={0}
|
||||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
aria-label="add all users"
|
||||||
></div>
|
className="h-4 w-4 bg-bunker border border-gray-600 rounded-sm"
|
||||||
)}
|
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<label className='ml-2 text-gray-500 text-sm'>
|
<label className="ml-2 text-gray-500 text-sm">
|
||||||
Add all members of my organization to this project.
|
Add all members of my organization to this project.
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ type Props = {
|
|||||||
onDeleteEnv: (slug: string) => Promise<void>;
|
onDeleteEnv: (slug: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvironmentTable = ({
|
const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: Props) => {
|
||||||
data = [],
|
|
||||||
onCreateEnv,
|
|
||||||
onDeleteEnv,
|
|
||||||
onUpdateEnv,
|
|
||||||
}: Props) => {
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
'createUpdateEnv',
|
'createUpdateEnv',
|
||||||
'deleteEnv',
|
'deleteEnv'
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onEnvCreateCB = async (env: Env) => {
|
const onEnvCreateCB = async (env: Env) => {
|
||||||
@@ -36,10 +31,7 @@ const EnvironmentTable = ({
|
|||||||
|
|
||||||
const onEnvUpdateCB = async (env: Env) => {
|
const onEnvUpdateCB = async (env: Env) => {
|
||||||
try {
|
try {
|
||||||
await onUpdateEnv(
|
await onUpdateEnv((popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug, env);
|
||||||
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
|
|
||||||
env
|
|
||||||
);
|
|
||||||
handlePopUpClose('createUpdateEnv');
|
handlePopUpClose('createUpdateEnv');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -48,9 +40,7 @@ const EnvironmentTable = ({
|
|||||||
|
|
||||||
const onEnvDeleteCB = async () => {
|
const onEnvDeleteCB = async () => {
|
||||||
try {
|
try {
|
||||||
await onDeleteEnv(
|
await onDeleteEnv((popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug);
|
||||||
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
|
|
||||||
);
|
|
||||||
handlePopUpClose('deleteEnv');
|
handlePopUpClose('deleteEnv');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -59,83 +49,68 @@ const EnvironmentTable = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='flex flex-row justify-between w-full'>
|
<div className="flex flex-row justify-between w-full">
|
||||||
<div className='flex flex-col w-full'>
|
<div className="flex flex-col w-full">
|
||||||
<p className='text-xl font-semibold mb-3'>Project Environments</p>
|
<p className="text-xl font-semibold mb-3">Project Environments</p>
|
||||||
<p className='text-base text-gray-400 mb-4'>
|
<p className="text-base text-gray-400 mb-4">
|
||||||
Choose which environments will show up in your dashboard like
|
Choose which environments will show up in your dashboard like development, staging,
|
||||||
development, staging, production
|
production
|
||||||
</p>
|
</p>
|
||||||
<p className='text-sm mr-1 text-gray-500 self-start'>
|
<p className="text-sm mr-1 text-gray-500 self-start">
|
||||||
Note: the text in slugs shows how these environmant should be
|
Note: the text in slugs shows how these environmant should be accessed in CLI.
|
||||||
accessed in CLI.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-48'>
|
<div className="w-48">
|
||||||
<Button
|
<Button
|
||||||
text='Add New Env'
|
text="Add New Env"
|
||||||
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
|
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
|
||||||
color='mineshaft'
|
color="mineshaft"
|
||||||
icon={faPlus}
|
icon={faPlus}
|
||||||
size='md'
|
size="md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1'>
|
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||||
<div className='absolute rounded-t-md w-full h-12 bg-white/5'></div>
|
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
||||||
<table className='w-full my-1'>
|
<table className="w-full my-1">
|
||||||
<thead className='text-bunker-300'>
|
<thead className="text-bunker-300">
|
||||||
<tr>
|
<tr>
|
||||||
<th className='text-left pl-6 pt-2.5 pb-2'>Name</th>
|
<th className="text-left pl-6 pt-2.5 pb-2">Name</th>
|
||||||
<th className='text-left pl-6 pt-2.5 pb-2'>Slug</th>
|
<th className="text-left pl-6 pt-2.5 pb-2">Slug</th>
|
||||||
<th></th>
|
<th aria-label="buttons" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data?.length > 0 ? (
|
{data?.length > 0 ? (
|
||||||
data.map(({ name, slug }) => {
|
data.map(({ name, slug }) => (
|
||||||
return (
|
<tr key={name} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100">
|
||||||
<tr
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize">
|
||||||
key={name}
|
{name}
|
||||||
className='bg-bunker-800 hover:bg-bunker-800/5 duration-100'
|
</td>
|
||||||
>
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">{slug}</td>
|
||||||
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize'>
|
<td className="py-2 border-mineshaft-700 border-t flex">
|
||||||
{name}
|
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-8">
|
||||||
</td>
|
<Button
|
||||||
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300'>
|
onButtonPressed={() => handlePopUpOpen('createUpdateEnv', { name, slug })}
|
||||||
{slug}
|
color="red"
|
||||||
</td>
|
size="icon-sm"
|
||||||
<td className='py-2 border-mineshaft-700 border-t flex'>
|
icon={faPencil}
|
||||||
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
|
/>
|
||||||
<Button
|
</div>
|
||||||
onButtonPressed={() =>
|
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||||
handlePopUpOpen('createUpdateEnv', { name, slug })
|
<Button
|
||||||
}
|
onButtonPressed={() => handlePopUpOpen('deleteEnv', { name, slug })}
|
||||||
color='red'
|
color="red"
|
||||||
size='icon-sm'
|
size="icon-sm"
|
||||||
icon={faPencil}
|
icon={faX}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center'>
|
</td>
|
||||||
<Button
|
</tr>
|
||||||
onButtonPressed={() =>
|
))
|
||||||
handlePopUpOpen('deleteEnv', { name, slug })
|
|
||||||
}
|
|
||||||
color='red'
|
|
||||||
size='icon-sm'
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td colSpan={4} className="text-center pt-7 pb-4 text-bunker-400">
|
||||||
colSpan={4}
|
|
||||||
className='text-center pt-7 pb-4 text-bunker-400'
|
|
||||||
>
|
|
||||||
No environmants found
|
No environmants found
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -143,7 +118,7 @@ const EnvironmentTable = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp['deleteEnv'].isOpen}
|
isOpen={popUp.deleteEnv.isOpen}
|
||||||
title={`Are you sure want to delete ${
|
title={`Are you sure want to delete ${
|
||||||
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
|
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
|
||||||
}?`}
|
}?`}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
import deleteServiceToken from '../../../pages/api/serviceToken/deleteServiceToken';
|
||||||
|
|
||||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
|
||||||
import guidGenerator from '../../utilities/randomId';
|
import guidGenerator from '../../utilities/randomId';
|
||||||
import Button from '../buttons/Button';
|
import Button from '../buttons/Button';
|
||||||
|
|
||||||
@@ -33,7 +32,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
|
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
||||||
<table className="w-full my-1">
|
<table className="w-full my-1">
|
||||||
<thead className="text-bunker-300 text-sm font-light">
|
<thead className="text-bunker-300 text-sm font-light">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -41,49 +40,47 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
|||||||
<th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
|
<th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
|
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
|
||||||
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
|
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
|
||||||
<th></th>
|
<th aria-label="button" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data?.length > 0 ? (
|
{data?.length > 0 ? (
|
||||||
data?.map((row) => {
|
data?.map((row) => (
|
||||||
return (
|
<tr
|
||||||
<tr
|
key={guidGenerator()}
|
||||||
key={guidGenerator()}
|
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
||||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
>
|
||||||
>
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
{row.name}
|
||||||
{row.name}
|
</td>
|
||||||
</td>
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
{workspaceName}
|
||||||
{workspaceName}
|
</td>
|
||||||
</td>
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
{row.environment}
|
||||||
{row.environment}
|
</td>
|
||||||
</td>
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
{new Date(row.expiresAt).toUTCString()}
|
||||||
{new Date(row.expiresAt).toUTCString()}
|
</td>
|
||||||
</td>
|
<td className="py-2 border-mineshaft-700 border-t">
|
||||||
<td className="py-2 border-mineshaft-700 border-t">
|
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
<Button
|
||||||
<Button
|
onButtonPressed={() => {
|
||||||
onButtonPressed={() => {
|
deleteServiceToken({ serviceTokenId: row._id });
|
||||||
deleteServiceToken({ serviceTokenId: row._id} );
|
setServiceTokens(data.filter((token) => token._id !== row._id));
|
||||||
setServiceTokens(data.filter(token => token._id != row._id));
|
createNotification({
|
||||||
createNotification({
|
text: `'${row.name}' token has been revoked.`,
|
||||||
text: `'${row.name}' token has been revoked.`,
|
type: 'error'
|
||||||
type: 'error'
|
});
|
||||||
});
|
}}
|
||||||
}}
|
color="red"
|
||||||
color="red"
|
size="icon-sm"
|
||||||
size="icon-sm"
|
icon={faX}
|
||||||
icon={faX}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
))
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
||||||
|
|||||||
@@ -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;
|
|
||||||
218
frontend/src/components/basic/table/UserTable.tsx
Normal file
218
frontend/src/components/basic/table/UserTable.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
|
||||||
|
import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace';
|
||||||
|
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
||||||
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
|
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||||
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
import { decryptAssymmetric, encryptAssymmetric } from '../../utilities/cryptography/crypto';
|
||||||
|
import guidGenerator from '../../utilities/randomId';
|
||||||
|
import Button from '../buttons/Button';
|
||||||
|
import Listbox from '../Listbox';
|
||||||
|
|
||||||
|
// const roles = ['admin', 'user'];
|
||||||
|
// TODO: Set type for this
|
||||||
|
type Props = {
|
||||||
|
userData: any[];
|
||||||
|
changeData: (users: any[]) => void;
|
||||||
|
myUser: string;
|
||||||
|
filter: string;
|
||||||
|
resendInvite: (email: string) => void;
|
||||||
|
isOrg: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too.
|
||||||
|
* #TODO: add the possibility of choosing and doing operations on multiple users.
|
||||||
|
* @param {*} props
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }: Props) => {
|
||||||
|
const [roleSelected, setRoleSelected] = useState(
|
||||||
|
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||||
|
);
|
||||||
|
const router = useRouter();
|
||||||
|
const [myRole, setMyRole] = useState('member');
|
||||||
|
|
||||||
|
const workspaceId = router.query.id as string;
|
||||||
|
// Delete the row in the table (e.g. a user)
|
||||||
|
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
|
||||||
|
const handleDelete = (membershipId: string, index: number) => {
|
||||||
|
// setUserIdToBeDeleted(userId);
|
||||||
|
// onClick();
|
||||||
|
if (isOrg) {
|
||||||
|
deleteUserFromOrganization(membershipId);
|
||||||
|
} else {
|
||||||
|
deleteUserFromWorkspace(membershipId);
|
||||||
|
}
|
||||||
|
changeData(userData.filter((v, i) => i !== index));
|
||||||
|
setRoleSelected([
|
||||||
|
...roleSelected.slice(0, index),
|
||||||
|
...roleSelected.slice(index + 1, userData?.length)
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the rold of a certain user
|
||||||
|
const handleRoleUpdate = (index: number, e: string) => {
|
||||||
|
changeUserRoleInWorkspace(userData[index].membershipId, e);
|
||||||
|
changeData([
|
||||||
|
...userData.slice(0, index),
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
key: userData[index].key,
|
||||||
|
firstName: userData[index].firstName,
|
||||||
|
lastName: userData[index].lastName,
|
||||||
|
email: userData[index].email,
|
||||||
|
role: e,
|
||||||
|
status: userData[index].status,
|
||||||
|
userId: userData[index].userId,
|
||||||
|
membershipId: userData[index].membershipId,
|
||||||
|
publicKey: userData[index].publicKey
|
||||||
|
}
|
||||||
|
],
|
||||||
|
...userData.slice(index + 1, userData?.length)
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
|
||||||
|
}, [userData, myUser]);
|
||||||
|
|
||||||
|
const grantAccess = async (id: string, publicKey: string) => {
|
||||||
|
const result = await getLatestFileKey({ workspaceId });
|
||||||
|
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
|
|
||||||
|
// assymmetrically decrypt symmetric key with local private key
|
||||||
|
const key = decryptAssymmetric({
|
||||||
|
ciphertext: result.latestKey.encryptedKey,
|
||||||
|
nonce: result.latestKey.nonce,
|
||||||
|
publicKey: result.latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: key,
|
||||||
|
publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadKeys(workspaceId, id, ciphertext, nonce);
|
||||||
|
router.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMembershipAndResendInvite = (email: string) => {
|
||||||
|
// deleteUserFromWorkspace(membershipId);
|
||||||
|
resendInvite(email);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||||
|
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
||||||
|
<table className="w-full my-0.5">
|
||||||
|
<thead className="text-gray-400 text-sm font-light">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
|
||||||
|
<th className="text-left pl-6 py-3.5">LAST NAME</th>
|
||||||
|
<th className="text-left pl-6 py-3.5">EMAIL</th>
|
||||||
|
<th aria-label="buttons" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{userData?.filter(
|
||||||
|
(user) =>
|
||||||
|
user.firstName?.toLowerCase().includes(filter) ||
|
||||||
|
user.lastName?.toLowerCase().includes(filter) ||
|
||||||
|
user.email?.toLowerCase().includes(filter)
|
||||||
|
).length > 0 &&
|
||||||
|
userData
|
||||||
|
?.filter(
|
||||||
|
(user) =>
|
||||||
|
user.firstName?.toLowerCase().includes(filter) ||
|
||||||
|
user.lastName?.toLowerCase().includes(filter) ||
|
||||||
|
user.email?.toLowerCase().includes(filter)
|
||||||
|
)
|
||||||
|
.map((row, index) => (
|
||||||
|
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5">
|
||||||
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
|
{row.firstName}
|
||||||
|
</td>
|
||||||
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
|
{row.lastName}
|
||||||
|
</td>
|
||||||
|
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||||
|
{row.email}
|
||||||
|
</td>
|
||||||
|
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||||
|
<div className="justify-end mr-6 mx-2 w-full h-full flex flex-row items-center">
|
||||||
|
{row.status === 'granted' &&
|
||||||
|
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
|
||||||
|
myUser !== row.email ? (
|
||||||
|
<Listbox
|
||||||
|
selected={row.role}
|
||||||
|
onChange={(e) => handleRoleUpdate(index, e)}
|
||||||
|
data={
|
||||||
|
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
|
||||||
|
}
|
||||||
|
text="Role: "
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
row.status !== 'invited' &&
|
||||||
|
row.status !== 'verified' && (
|
||||||
|
<Listbox
|
||||||
|
selected={row.role}
|
||||||
|
text="Role: "
|
||||||
|
onChange={() => {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}}
|
||||||
|
data={null}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{(row.status === 'invited' || row.status === 'verified') && (
|
||||||
|
<div className="w-full pl-9">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
|
||||||
|
color="mineshaft"
|
||||||
|
text="Resend Invite"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{row.status === 'completed' && myUser !== row.email && (
|
||||||
|
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
||||||
|
color="mineshaft"
|
||||||
|
text="Grant Access"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{myUser !== row.email &&
|
||||||
|
// row.role !== "admin" &&
|
||||||
|
myRole !== 'member' ? (
|
||||||
|
<div className="opacity-50 hover:opacity-100 flex items-center">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||||
|
color="red"
|
||||||
|
size="icon-sm"
|
||||||
|
icon={faX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-9 h-9" />
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserTable;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import StripeRedirect from '@app/pages/api/organization/StripeRedirect';
|
||||||
import StripeRedirect from '~/pages/api/organization/StripeRedirect';
|
|
||||||
|
|
||||||
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
|
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
|
||||||
|
|
||||||
@@ -21,54 +20,43 @@ export default function Plan({ plan }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative flex flex-col justify-between border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
className={`relative flex flex-col justify-between border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
||||||
plan.name != 'Starter' && plan.current == true
|
plan.name !== 'Starter' && plan.current === true ? 'border-primary' : 'border-chicago-700'
|
||||||
? 'border-primary'
|
|
||||||
: 'border-chicago-700'
|
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<div className='flex flex-row justify-between items-center relative z-10'>
|
<div className="flex flex-row justify-between items-center relative z-10">
|
||||||
<p className={`px-6 py-4 text-3xl font-semibold text-gray-400`}>
|
<p className="px-6 py-4 text-3xl font-semibold text-gray-400">{plan.name}</p>
|
||||||
{plan.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flwx-row items-end justify-start mb-4'>
|
<div className="flex flwx-row items-end justify-start mb-4">
|
||||||
<p className='pl-6 text-3xl font-semibold text-primary'>
|
<p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
|
||||||
{plan.price}
|
<p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
|
||||||
</p>
|
|
||||||
<p className='pl-3 mb-1 text-lg text-gray-400'>
|
|
||||||
{plan.priceExplanation}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'>
|
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
|
||||||
{plan.text}
|
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
|
||||||
</p>
|
|
||||||
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'>
|
|
||||||
{plan.subtext}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row items-center'>
|
<div className="flex flex-row items-center">
|
||||||
{plan.current == false ? (
|
{plan.current === false ? (
|
||||||
<>
|
<>
|
||||||
{plan.buttonTextMain == 'Schedule a Demo' ? (
|
{plan.buttonTextMain === 'Schedule a Demo' ? (
|
||||||
<a href='/scheduledemo' target='_blank rel="noopener"'>
|
<a href="/scheduledemo" target='_blank rel="noopener"'>
|
||||||
<div className='relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max'>
|
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
|
||||||
{plan.buttonTextMain}
|
{plan.buttonTextMain}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${
|
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${
|
||||||
plan.buttonTextMain == 'Downgrade'
|
plan.buttonTextMain === 'Downgrade'
|
||||||
? 'hover:bg-red hover:text-white hover:border-red'
|
? 'hover:bg-red hover:text-white hover:border-red'
|
||||||
: 'hover:bg-primary hover:text-black hover:border-primary'
|
: 'hover:bg-primary hover:text-black hover:border-primary'
|
||||||
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
|
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
StripeRedirect({
|
StripeRedirect({
|
||||||
orgId: tempLocalStorage('orgData.id'),
|
orgId: tempLocalStorage('orgData.id')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -76,11 +64,8 @@ export default function Plan({ plan }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<a
|
<a href="https://infisical.com/pricing" target='_blank rel="noopener"'>
|
||||||
href='https://infisical.com/pricing'
|
<div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
|
||||||
target='_blank rel="noopener"'
|
|
||||||
>
|
|
||||||
<div className='relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5'>
|
|
||||||
{plan.buttonTextSecondary}
|
{plan.buttonTextSecondary}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -88,12 +73,10 @@ export default function Plan({ plan }: Props) {
|
|||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
|
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
|
||||||
plan.name != 'Starter' && plan.current == true
|
plan.name !== 'Starter' && plan.current === true ? 'bg-primary' : 'bg-chicago-700'
|
||||||
? 'bg-primary'
|
|
||||||
: 'bg-chicago-700'
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<p className='text-xs text-black font-semibold'>CURRENT PLAN</p>
|
<p className="text-xs text-black font-semibold">CURRENT PLAN</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ import { useEffect, useRef } from 'react';
|
|||||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import { Notification as NotificationType } from './NotificationProvider';
|
type NotificationType = 'success' | 'error' | 'info';
|
||||||
|
|
||||||
|
export type TNotification = {
|
||||||
|
text: string;
|
||||||
|
type?: NotificationType;
|
||||||
|
timeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
interface NotificationProps {
|
interface NotificationProps {
|
||||||
notification: Required<NotificationType>;
|
notification: Required<TNotification>;
|
||||||
clearNotification: (text: string) => void;
|
clearNotification: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Notification = ({
|
const Notification = ({ notification, clearNotification }: NotificationProps) => {
|
||||||
notification,
|
|
||||||
clearNotification
|
|
||||||
}: NotificationProps) => {
|
|
||||||
const timeout = useRef<number>();
|
const timeout = useRef<number>();
|
||||||
|
|
||||||
const handleClearNotification = () => clearNotification(notification.text);
|
const handleClearNotification = () => clearNotification(notification.text);
|
||||||
|
|
||||||
const setNotifTimeout = () => {
|
const setNotifTimeout = () => {
|
||||||
timeout.current = window.setTimeout(
|
timeout.current = window.setTimeout(handleClearNotification, notification.timeoutMs);
|
||||||
handleClearNotification,
|
|
||||||
notification.timeoutMs
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelNotifTimeout = () => {
|
const cancelNotifTimeout = () => {
|
||||||
@@ -40,25 +40,21 @@ const Notification = ({
|
|||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
{notification.type === 'error' && (
|
{notification.type === 'error' && (
|
||||||
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md"></div>
|
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md" />
|
||||||
)}
|
)}
|
||||||
{notification.type === 'success' && (
|
{notification.type === 'success' && (
|
||||||
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md"></div>
|
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md" />
|
||||||
)}
|
)}
|
||||||
{notification.type === 'info' && (
|
{notification.type === 'info' && (
|
||||||
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md"></div>
|
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md" />
|
||||||
)}
|
)}
|
||||||
<p className="text-bunker-200 text-sm font-semibold mt-0.5">
|
<p className="text-bunker-200 text-sm font-semibold mt-0.5">{notification.text}</p>
|
||||||
{notification.text}
|
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
onClick={() => clearNotification(notification.text)}
|
onClick={() => clearNotification(notification.text)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon className="text-white pl-2 w-4 h-3 hover:text-red" icon={faX} />
|
||||||
className="text-white pl-2 w-4 h-3 hover:text-red"
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { TNotification } from './Notification';
|
||||||
import Notifications from './Notifications';
|
import Notifications from './Notifications';
|
||||||
|
|
||||||
type NotificationType = 'success' | 'error' | 'info';
|
|
||||||
|
|
||||||
export type Notification = {
|
|
||||||
text: string;
|
|
||||||
type?: NotificationType;
|
|
||||||
timeoutMs?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NotificationContextState = {
|
type NotificationContextState = {
|
||||||
createNotification: (newNotification: Notification) => void;
|
createNotification: (newNotification: TNotification) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotificationContext = createContext<NotificationContextState>({
|
const NotificationContext = createContext<NotificationContextState>({
|
||||||
@@ -24,43 +17,33 @@ interface NotificationProviderProps {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migration to radix toast
|
||||||
const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
||||||
const [notifications, setNotifications] = useState<Required<Notification>[]>(
|
const [notifications, setNotifications] = useState<Required<TNotification>[]>([]);
|
||||||
[]
|
|
||||||
|
const clearNotification = (text: string) =>
|
||||||
|
setNotifications((state) => state.filter((notif) => notif.text !== text));
|
||||||
|
|
||||||
|
const createNotification = useCallback(
|
||||||
|
({ text, type = 'success', timeoutMs = 4000 }: TNotification) => {
|
||||||
|
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
||||||
|
|
||||||
|
if (doesNotifExist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNotification: Required<TNotification> = { text, type, timeoutMs };
|
||||||
|
|
||||||
|
setNotifications((state) => [...state, newNotification]);
|
||||||
|
},
|
||||||
|
[notifications]
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearNotification = (text: string) => {
|
const value = useMemo(() => ({ createNotification }), [createNotification]);
|
||||||
return setNotifications((state) =>
|
|
||||||
state.filter((notif) => notif.text !== text)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createNotification = ({
|
|
||||||
text,
|
|
||||||
type = 'success',
|
|
||||||
timeoutMs = 4000
|
|
||||||
}: Notification) => {
|
|
||||||
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
|
||||||
|
|
||||||
if (doesNotifExist) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNotification: Required<Notification> = { text, type, timeoutMs };
|
|
||||||
|
|
||||||
return setNotifications((state) => [...state, newNotification]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationContext.Provider
|
<NotificationContext.Provider value={value}>
|
||||||
value={{
|
<Notifications notifications={notifications} clearNotification={clearNotification} />
|
||||||
createNotification
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Notifications
|
|
||||||
notifications={notifications}
|
|
||||||
clearNotification={clearNotification}
|
|
||||||
/>
|
|
||||||
{children}
|
{children}
|
||||||
</NotificationContext.Provider>
|
</NotificationContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
import Notification from './Notification';
|
import Notification, { TNotification } from './Notification';
|
||||||
import { Notification as NotificationType } from './NotificationProvider';
|
|
||||||
|
|
||||||
interface NoticationsProps {
|
interface NoticationsProps {
|
||||||
notifications: Required<NotificationType>[];
|
notifications: Required<TNotification>[];
|
||||||
clearNotification: (text: string) => void;
|
clearNotification: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Notifications = ({
|
const Notifications = ({ notifications, clearNotification }: NoticationsProps) => {
|
||||||
notifications,
|
|
||||||
clearNotification
|
|
||||||
}: NoticationsProps) => {
|
|
||||||
if (!notifications.length) {
|
if (!notifications.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="hidden fixed z-50 md:flex md:flex-col-reverse bottom-1 gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
|
<div className="hidden fixed z-50 md:flex md:flex-col-reverse gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
|
||||||
{notifications.map((notif) => (
|
{notifications.map((notif) => (
|
||||||
<Notification
|
<Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
|
||||||
key={notif.text}
|
|
||||||
notification={notif}
|
|
||||||
clearNotification={clearNotification}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,30 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the text field where people can add comments to particular secrets.
|
* This is the text field where people can add comments to particular secrets.
|
||||||
*/
|
*/
|
||||||
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
|
const CommentField = ({
|
||||||
|
comment,
|
||||||
|
modifyComment,
|
||||||
|
position
|
||||||
|
}: {
|
||||||
|
comment: string;
|
||||||
|
modifyComment: (value: string, posistion: number) => void;
|
||||||
|
position: number;
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <div className={`relative mt-4 px-4 pt-6`}>
|
return (
|
||||||
<p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p>
|
<div className="relative mt-4 px-4 pt-6">
|
||||||
<textarea
|
<p className="text-sm text-bunker-300 pl-0.5">{t('dashboard:sidebar.comments')}</p>
|
||||||
className="bg-bunker-800 placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
<textarea
|
||||||
value={comment}
|
className="placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||||
onChange={(e) => modifyComment(e.target.value, position)}
|
value={comment}
|
||||||
placeholder="Leave any comments here..."
|
onChange={(e) => modifyComment(e.target.value, position)}
|
||||||
/>
|
placeholder="Leave any comments here..."
|
||||||
</div>
|
/>
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default CommentField;
|
export default CommentField;
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ const DashboardInputField = ({
|
|||||||
}: DashboardInputFieldProps) => {
|
}: DashboardInputFieldProps) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||||
if (ref.current == null) return;
|
if (ref.current === null) return;
|
||||||
|
|
||||||
ref.current.scrollTop = e.currentTarget.scrollTop;
|
ref.current.scrollTop = e.currentTarget.scrollTop;
|
||||||
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === 'varName') {
|
if (type === 'varName') {
|
||||||
const startsWithNumber = !isNaN(Number(value?.charAt(0))) && value != '';
|
const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== '';
|
||||||
const error = startsWithNumber || isDuplicate;
|
const error = startsWithNumber || isDuplicate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -58,9 +58,7 @@ const DashboardInputField = ({
|
|||||||
} rounded-md`}
|
} rounded-md`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
onChange={(e) =>
|
onChange={(e) => onChangeHandler(e.target.value.toUpperCase(), position)}
|
||||||
onChangeHandler(e.target.value.toUpperCase(), position)
|
|
||||||
}
|
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${
|
className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${
|
||||||
@@ -81,13 +79,16 @@ const DashboardInputField = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (type === 'value') {
|
}
|
||||||
|
if (type === 'value') {
|
||||||
return (
|
return (
|
||||||
<div className="flex-col w-full">
|
<div className="flex-col w-full">
|
||||||
<div
|
<div className="group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md">
|
||||||
className={`group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md`}
|
{override === true && (
|
||||||
>
|
<div className="bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md">
|
||||||
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
|
Override enabled
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<input
|
<input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||||
@@ -105,20 +106,18 @@ const DashboardInputField = ({
|
|||||||
blurred && !override
|
blurred && !override
|
||||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
||||||
: ''
|
: ''
|
||||||
} ${
|
} ${override ? 'text-primary-300' : 'text-gray-400'}
|
||||||
override ? 'text-primary-300' : 'text-gray-400'
|
|
||||||
}
|
|
||||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||||
>
|
>
|
||||||
{value?.split(REGEX).map((word, id) => {
|
{value?.split(REGEX).map((word, id) => {
|
||||||
if (word.match(REGEX) !== null) {
|
if (word.match(REGEX) !== null) {
|
||||||
return (
|
return (
|
||||||
<span className="ph-no-capture text-yellow" key={id}>
|
<span className="ph-no-capture text-yellow" key={`${word}.${id + 1}`}>
|
||||||
{word.slice(0, 2)}
|
{word.slice(0, 2)}
|
||||||
<span className="ph-no-capture text-yellow-200/80">
|
<span className="ph-no-capture text-yellow-200/80">
|
||||||
{word.slice(2, word.length - 1)}
|
{word.slice(2, word.length - 1)}
|
||||||
</span>
|
</span>
|
||||||
{word.slice(word.length - 1, word.length) == '}' ? (
|
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||||
<span className="ph-no-capture text-yellow">
|
<span className="ph-no-capture text-yellow">
|
||||||
{word.slice(word.length - 1, word.length)}
|
{word.slice(word.length - 1, word.length)}
|
||||||
</span>
|
</span>
|
||||||
@@ -129,13 +128,12 @@ const DashboardInputField = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<span key={id} className="ph-no-capture">
|
|
||||||
{word}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<span key={`${word}_${id + 1}`} className="ph-no-capture">
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{blurred && (
|
{blurred && (
|
||||||
@@ -160,7 +158,14 @@ const DashboardInputField = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||||
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
|
return (
|
||||||
|
prev.value === next.value &&
|
||||||
|
prev.type === next.type &&
|
||||||
|
prev.position === next.position &&
|
||||||
|
prev.blurred === next.blurred &&
|
||||||
|
prev.override === next.override &&
|
||||||
|
prev.isDuplicate === next.isDuplicate
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(DashboardInputField, inputPropsAreEqual);
|
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Props = {
|
|||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DeleteActionButton({ onSubmit }: Props) {
|
export const DeleteActionButton = ({ onSubmit }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Menu, Transition } from '@headlessui/react';
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||||
@@ -10,57 +9,49 @@ import downloadYaml from '../utilities/secrets/downloadYaml';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
|
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
|
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
|
||||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||||
*/
|
*/
|
||||||
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||||
const { t } = useTranslation();
|
return (
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
return <Menu
|
<Menu.Button
|
||||||
as="div"
|
as="div"
|
||||||
className="relative inline-block text-left"
|
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||||
>
|
>
|
||||||
<Menu.Button
|
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
|
||||||
as="div"
|
</Menu.Button>
|
||||||
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
<Transition
|
||||||
>
|
as={Fragment}
|
||||||
<Button
|
enter="transition ease-out duration-100"
|
||||||
color="mineshaft"
|
enterFrom="transform opacity-0 scale-95"
|
||||||
size="icon-md"
|
enterTo="transform opacity-100 scale-100"
|
||||||
icon={faDownload}
|
leave="transition ease-in duration-75"
|
||||||
onButtonPressed={() => {}}
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
/>
|
leaveTo="transform opacity-0 scale-95"
|
||||||
</Menu.Button>
|
>
|
||||||
<Transition
|
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
||||||
as={Fragment}
|
<Menu.Item>
|
||||||
enter="transition ease-out duration-100"
|
<Button
|
||||||
enterFrom="transform opacity-0 scale-95"
|
color="mineshaft"
|
||||||
enterTo="transform opacity-100 scale-100"
|
onButtonPressed={() => downloadDotEnv({ data, env })}
|
||||||
leave="transition ease-in duration-75"
|
size="md"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
text="Download as .env"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
/>
|
||||||
>
|
</Menu.Item>
|
||||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
<Menu.Item>
|
||||||
<Menu.Item>
|
<Button
|
||||||
<Button
|
color="mineshaft"
|
||||||
color="mineshaft"
|
onButtonPressed={() => downloadYaml({ data, env })}
|
||||||
onButtonPressed={() => downloadDotEnv({ data, env })}
|
size="md"
|
||||||
size="md"
|
text="Download as .yml"
|
||||||
text="Download as .env"
|
/>
|
||||||
/>
|
</Menu.Item>
|
||||||
</Menu.Item>
|
</Menu.Items>
|
||||||
<Menu.Item>
|
</Transition>
|
||||||
<Button
|
</Menu>
|
||||||
color="mineshaft"
|
);
|
||||||
onButtonPressed={() => downloadYaml({ data, env })}
|
};
|
||||||
size="md"
|
|
||||||
text="Download as .yml"
|
|
||||||
/>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DownloadSecretMenu;
|
export default DownloadSecretMenu;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { type ChangeEvent, type DragEvent, useState } from "react";
|
/* eslint-disable no-nested-ternary */
|
||||||
import Image from "next/image";
|
import { type ChangeEvent, type DragEvent, useState } from 'react';
|
||||||
import { useTranslation } from "next-i18next";
|
import Image from 'next/image';
|
||||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { parseDocument, Scalar, YAMLMap } from 'yaml';
|
import { parseDocument, Scalar, YAMLMap } from 'yaml';
|
||||||
|
|
||||||
import Button from "../basic/buttons/Button";
|
import Button from '../basic/buttons/Button';
|
||||||
import Error from "../basic/Error";
|
import Error from '../basic/Error';
|
||||||
import { parseDotEnv } from '../utilities/parseDotEnv';
|
import { parseDotEnv } from '../utilities/parseDotEnv';
|
||||||
import guidGenerator from "../utilities/randomId";
|
import guidGenerator from '../utilities/randomId';
|
||||||
|
|
||||||
interface DropZoneProps {
|
interface DropZoneProps {
|
||||||
// TODO: change Data type from any
|
// TODO: change Data type from any
|
||||||
@@ -28,7 +29,7 @@ const DropZone = ({
|
|||||||
errorDragAndDrop,
|
errorDragAndDrop,
|
||||||
setButtonReady,
|
setButtonReady,
|
||||||
keysExist,
|
keysExist,
|
||||||
numCurrentRows,
|
numCurrentRows
|
||||||
}: DropZoneProps) => {
|
}: DropZoneProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ const DropZone = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// set dropEffect to copy i.e copy of the source item
|
// set dropEffect to copy i.e copy of the source item
|
||||||
e.dataTransfer.dropEffect = "copy";
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
};
|
};
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -57,16 +58,14 @@ const DropZone = ({
|
|||||||
switch (fileType) {
|
switch (fileType) {
|
||||||
case 'env': {
|
case 'env': {
|
||||||
const keyPairs = parseDotEnv(file);
|
const keyPairs = parseDotEnv(file);
|
||||||
secrets = Object.keys(keyPairs).map((key, index) => {
|
secrets = Object.keys(keyPairs).map((key, index) => ({
|
||||||
return {
|
id: guidGenerator(),
|
||||||
id: guidGenerator(),
|
pos: numCurrentRows + index,
|
||||||
pos: numCurrentRows + index,
|
key,
|
||||||
key: key,
|
value: keyPairs[key as keyof typeof keyPairs].value,
|
||||||
value: keyPairs[key as keyof typeof keyPairs].value,
|
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
|
||||||
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
|
type: 'shared'
|
||||||
type: 'shared',
|
}));
|
||||||
};
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'yml': {
|
case 'yml': {
|
||||||
@@ -79,15 +78,15 @@ const DropZone = ({
|
|||||||
fileContent!.items
|
fileContent!.items
|
||||||
.find((item) => item.key.value === key)
|
.find((item) => item.key.value === key)
|
||||||
?.key?.commentBefore?.split('\n')
|
?.key?.commentBefore?.split('\n')
|
||||||
.map((comment) => comment.trim())
|
.map((cmnt) => cmnt.trim())
|
||||||
.join('\n') ?? '';
|
.join('\n') ?? '';
|
||||||
return {
|
return {
|
||||||
id: guidGenerator(),
|
id: guidGenerator(),
|
||||||
pos: numCurrentRows + index,
|
pos: numCurrentRows + index,
|
||||||
key: key,
|
key,
|
||||||
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
|
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
|
||||||
comment,
|
comment,
|
||||||
type: 'shared',
|
type: 'shared'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -105,7 +104,7 @@ const DropZone = ({
|
|||||||
setTimeout(() => setLoading(false), 5000);
|
setTimeout(() => setLoading(false), 5000);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.dataTransfer.dropEffect = "copy";
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
|
|
||||||
const file = e.dataTransfer.files[0];
|
const file = e.dataTransfer.files[0];
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -149,16 +148,11 @@ const DropZone = ({
|
|||||||
|
|
||||||
return loading ? (
|
return loading ? (
|
||||||
<div className="flex items-center justify-center pt-16 mb-16">
|
<div className="flex items-center justify-center pt-16 mb-16">
|
||||||
<Image
|
<Image src="/images/loading/loading.gif" height={70} width={120} alt="google logo" />
|
||||||
src="/images/loading/loading.gif"
|
|
||||||
height={70}
|
|
||||||
width={120}
|
|
||||||
alt="google logo"
|
|
||||||
></Image>
|
|
||||||
</div>
|
</div>
|
||||||
) : keysExist ? (
|
) : keysExist ? (
|
||||||
<div
|
<div
|
||||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
|
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
@@ -171,36 +165,27 @@ const DropZone = ({
|
|||||||
accept=".txt,.env,.yml"
|
accept=".txt,.env,.yml"
|
||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
{errorDragAndDrop ? (
|
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
|
||||||
<div className="my-3 max-w-xl opacity-80"></div>
|
|
||||||
) : (
|
|
||||||
<div className=""></div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faUpload} className="text-bunker-300 text-3xl mr-6" />
|
||||||
icon={faUpload}
|
<p className="text-bunker-300 mt-1">{t('common:drop-zone-keys')}</p>
|
||||||
className="text-bunker-300 text-3xl mr-6"
|
|
||||||
/>
|
|
||||||
<p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{errorDragAndDrop ? (
|
{errorDragAndDrop && (
|
||||||
<div className="mt-8 max-w-xl opacity-80">
|
<div className="mt-8 max-w-xl opacity-80">
|
||||||
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
|
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker outline max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4"
|
className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4"
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
|
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
|
||||||
<p className="">{t("common:drop-zone")}</p>
|
<p className="">{t('common:drop-zone')}</p>
|
||||||
<input
|
<input
|
||||||
id="fileSelect"
|
id="fileSelect"
|
||||||
type="file"
|
type="file"
|
||||||
@@ -209,14 +194,14 @@ const DropZone = ({
|
|||||||
onChange={handleFileSelect}
|
onChange={handleFileSelect}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row w-full items-center justify-center mb-6 mt-5">
|
<div className="flex flex-row w-full items-center justify-center mb-6 mt-5">
|
||||||
<div className="border-t border-gray-700 w-1/5"></div>
|
<div className="border-t border-gray-700 w-1/5" />
|
||||||
<p className="text-gray-400 text-xs mx-4">OR</p>
|
<p className="text-gray-400 text-xs mx-4">OR</p>
|
||||||
<div className="border-t border-gray-700 w-1/5"></div>
|
<div className="border-t border-gray-700 w-1/5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="z-10 mb-6">
|
<div className="z-10 mb-6">
|
||||||
<Button
|
<Button
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
text={String(t("dashboard:add-secret"))}
|
text={String(t('dashboard:add-secret'))}
|
||||||
onButtonPressed={createNewFile}
|
onButtonPressed={createNewFile}
|
||||||
size="md"
|
size="md"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,95 +1,106 @@
|
|||||||
import { Fragment,useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { Menu, Transition } from '@headlessui/react';
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
||||||
* @returns the popup-menu for randomly generating secrets
|
* @returns the popup-menu for randomly generating secrets
|
||||||
*/
|
*/
|
||||||
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
|
const GenerateSecretMenu = ({
|
||||||
|
modifyValue,
|
||||||
|
position
|
||||||
|
}: {
|
||||||
|
modifyValue: (value: string, position: number) => void;
|
||||||
|
position: number;
|
||||||
|
}) => {
|
||||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <Menu as="div" className="relative inline-block text-left">
|
return (
|
||||||
<div>
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
<div>
|
||||||
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
|
<Menu.Button className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||||
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
|
<div className="py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500">
|
||||||
</div>
|
<FontAwesomeIcon icon={faShuffle} className="text-bunker-300" />
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
if (randomStringLength > 32) {
|
|
||||||
setRandomStringLength(32);
|
|
||||||
} else if (randomStringLength < 2) {
|
|
||||||
setRandomStringLength(2);
|
|
||||||
} else {
|
|
||||||
modifyValue(
|
|
||||||
[...Array(randomStringLength)]
|
|
||||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
|
||||||
.join(''),
|
|
||||||
position
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="text-lg pl-1.5 pr-3"
|
|
||||||
icon={faShuffle}
|
|
||||||
/>
|
|
||||||
<div className="text-sm justify-between flex flex-row w-full">
|
|
||||||
<p>{t("dashboard:sidebar.generate-random-hex")}</p>
|
|
||||||
<p>{t("dashboard:sidebar.digits")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Menu.Button>
|
||||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
</div>
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||||
<div
|
<div
|
||||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (randomStringLength > 1) {
|
if (randomStringLength > 32) {
|
||||||
setRandomStringLength(randomStringLength - 1);
|
setRandomStringLength(32);
|
||||||
|
} else if (randomStringLength < 2) {
|
||||||
|
setRandomStringLength(2);
|
||||||
|
} else {
|
||||||
|
modifyValue(
|
||||||
|
[...Array(randomStringLength)]
|
||||||
|
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||||
|
.join(''),
|
||||||
|
position
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||||
>
|
>
|
||||||
-
|
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faShuffle} />
|
||||||
|
<div className="text-sm justify-between flex flex-row w-full">
|
||||||
|
<p>{t('dashboard:sidebar.generate-random-hex')}</p>
|
||||||
|
<p>{t('dashboard:sidebar.digits')}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||||
onChange={(e) =>
|
<div
|
||||||
setRandomStringLength(parseInt(e.target.value))
|
onKeyDown={() => null}
|
||||||
}
|
role="button"
|
||||||
value={randomStringLength}
|
tabIndex={0}
|
||||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||||
spellCheck="false"
|
onClick={() => {
|
||||||
/>
|
if (randomStringLength > 1) {
|
||||||
<div
|
setRandomStringLength(randomStringLength - 1);
|
||||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
}
|
||||||
onClick={() => {
|
}}
|
||||||
if (randomStringLength < 32) {
|
>
|
||||||
setRandomStringLength(randomStringLength + 1);
|
-
|
||||||
}
|
</div>
|
||||||
}}
|
<input
|
||||||
>
|
onChange={(e) => setRandomStringLength(parseInt(e.target.value, 10))}
|
||||||
+
|
value={randomStringLength}
|
||||||
|
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||||
|
spellCheck="false"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||||
|
onClick={() => {
|
||||||
|
if (randomStringLength < 32) {
|
||||||
|
setRandomStringLength(randomStringLength + 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Menu.Items>
|
||||||
</Menu.Items>
|
</Transition>
|
||||||
</Transition>
|
</Menu>
|
||||||
</Menu>
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default GenerateSecretMenu;
|
export default GenerateSecretMenu;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
|
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||||
@@ -7,9 +6,9 @@ import DashboardInputField from './DashboardInputField';
|
|||||||
|
|
||||||
interface KeyPairProps {
|
interface KeyPairProps {
|
||||||
keyPair: SecretDataProps;
|
keyPair: SecretDataProps;
|
||||||
modifyKey: (value: string, position: number) => void;
|
modifyKey: (value: string, position: number) => void;
|
||||||
modifyValue: (value: string, position: number) => void;
|
modifyValue: (value: string, position: number) => void;
|
||||||
modifyValueOverride: (value: string, position: number) => void;
|
modifyValueOverride: (value: string, position: number) => void;
|
||||||
isBlurred: boolean;
|
isBlurred: boolean;
|
||||||
isDuplicate: boolean;
|
isDuplicate: boolean;
|
||||||
toggleSidebar: (id: string) => void;
|
toggleSidebar: (id: string) => void;
|
||||||
@@ -28,7 +27,7 @@ interface KeyPairProps {
|
|||||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||||
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const KeyPair = ({
|
const KeyPair = ({
|
||||||
@@ -41,48 +40,61 @@ const KeyPair = ({
|
|||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
sidebarSecretId,
|
sidebarSecretId,
|
||||||
isSnapshot
|
isSnapshot
|
||||||
}: KeyPairProps) => {
|
}: KeyPairProps) => (
|
||||||
return (
|
<div
|
||||||
<div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && 'pointer-events-none'} ${
|
||||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
keyPair.id === sidebarSecretId && 'bg-mineshaft-500 duration-200'
|
||||||
{keyPair.valueOverride && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
} rounded-md`}
|
||||||
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
|
>
|
||||||
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||||
This secret is overriden
|
{keyPair.valueOverride && (
|
||||||
</span>
|
<div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||||
</div>}
|
<div className="w-1 h-1 rounded-full bg-primary z-40" />
|
||||||
<div className="min-w-xl w-96">
|
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||||
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
This secret is overriden
|
||||||
<DashboardInputField
|
</span>
|
||||||
onChangeHandler={modifyKey}
|
|
||||||
type="varName"
|
|
||||||
position={keyPair.pos}
|
|
||||||
value={keyPair.key}
|
|
||||||
isDuplicate={isDuplicate}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full min-w-xl">
|
)}
|
||||||
<div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
|
<div className="min-w-xl w-96">
|
||||||
<DashboardInputField
|
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||||
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
|
<DashboardInputField
|
||||||
type="value"
|
onChangeHandler={modifyKey}
|
||||||
position={keyPair.pos}
|
type="varName"
|
||||||
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
|
position={keyPair.pos}
|
||||||
blurred={isBlurred}
|
value={keyPair.key}
|
||||||
override={Boolean(keyPair.valueOverride)}
|
isDuplicate={isDuplicate}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isSnapshot && <div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
|
||||||
<FontAwesomeIcon
|
|
||||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
|
||||||
icon={faEllipsis}
|
|
||||||
/>
|
/>
|
||||||
</div>}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-full min-w-xl">
|
||||||
|
<div
|
||||||
|
className={`flex min-w-xl items-center ${
|
||||||
|
!isSnapshot && 'pr-1.5'
|
||||||
|
} rounded-lg mt-4 md:mt-0 max-h-10`}
|
||||||
|
>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
|
||||||
|
type="value"
|
||||||
|
position={keyPair.pos}
|
||||||
|
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
|
||||||
|
blurred={isBlurred}
|
||||||
|
override={Boolean(keyPair.valueOverride)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isSnapshot && (
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => toggleSidebar(keyPair.id)}
|
||||||
|
className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="text-gray-300 px-2.5 text-lg mt-0.5" icon={faEllipsis} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
};
|
);
|
||||||
|
|
||||||
export default KeyPair;
|
export default KeyPair;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
/* eslint-disable react/no-unused-prop-types */
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import SecretVersionList from '@app/ee/components/SecretVersionList';
|
import SecretVersionList from '@app/ee/components/SecretVersionList';
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
@@ -12,7 +13,6 @@ import DashboardInputField from './DashboardInputField';
|
|||||||
import { DeleteActionButton } from './DeleteActionButton';
|
import { DeleteActionButton } from './DeleteActionButton';
|
||||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||||
|
|
||||||
|
|
||||||
interface SecretProps {
|
interface SecretProps {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -22,22 +22,18 @@ interface SecretProps {
|
|||||||
comment: string;
|
comment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OverrideProps {
|
export interface DeleteRowFunctionProps {
|
||||||
id: string;
|
ids: string[];
|
||||||
valueOverride: string;
|
secretName: string;
|
||||||
}
|
|
||||||
export interface DeleteRowFunctionProps {
|
|
||||||
ids: string[];
|
|
||||||
secretName: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SideBarProps {
|
interface SideBarProps {
|
||||||
toggleSidebar: (value: string) => void;
|
toggleSidebar: (value: string) => void;
|
||||||
data: SecretProps[];
|
data: SecretProps[];
|
||||||
modifyKey: (value: string, position: number) => void;
|
modifyKey: (value: string, position: number) => void;
|
||||||
modifyValue: (value: string, position: number) => void;
|
modifyValue: (value: string, position: number) => void;
|
||||||
modifyValueOverride: (value: string | undefined, position: number) => void;
|
modifyValueOverride: (value: string | undefined, position: number) => void;
|
||||||
modifyComment: (value: string, position: number) => void;
|
modifyComment: (value: string, position: number) => void;
|
||||||
buttonReady: boolean;
|
buttonReady: boolean;
|
||||||
savePush: () => void;
|
savePush: () => void;
|
||||||
sharedToHide: string[];
|
sharedToHide: string[];
|
||||||
@@ -57,112 +53,140 @@ interface SideBarProps {
|
|||||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||||
* @returns the sidebar with 'secret's settings'
|
* @returns the sidebar with 'secret's settings'
|
||||||
*/
|
*/
|
||||||
const SideBar = ({
|
const SideBar = ({
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
data,
|
data,
|
||||||
modifyKey,
|
modifyKey,
|
||||||
modifyValue,
|
modifyValue,
|
||||||
modifyValueOverride,
|
modifyValueOverride,
|
||||||
modifyComment,
|
modifyComment,
|
||||||
buttonReady,
|
buttonReady,
|
||||||
savePush,
|
savePush,
|
||||||
deleteRow
|
deleteRow
|
||||||
}: SideBarProps) => {
|
}: SideBarProps) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride != undefined);
|
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between'>
|
return (
|
||||||
{isLoading ? (
|
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between">
|
||||||
<div className="flex items-center justify-center h-full">
|
{isLoading ? (
|
||||||
<Image
|
<div className="flex items-center justify-center h-full">
|
||||||
src="/images/loading/loading.gif"
|
<Image
|
||||||
height={60}
|
src="/images/loading/loading.gif"
|
||||||
width={100}
|
height={60}
|
||||||
alt="infisical loading indicator"
|
width={100}
|
||||||
></Image>
|
alt="infisical loading indicator"
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='h-min overflow-y-auto'>
|
<div className="h-min overflow-y-auto">
|
||||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||||
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.secret")}</p>
|
<p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
|
||||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
<div
|
||||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
onKeyDown={() => null}
|
||||||
</div>
|
role="button"
|
||||||
</div>
|
tabIndex={0}
|
||||||
<div className='mt-4 px-4 pointer-events-none'>
|
className="p-1"
|
||||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
|
onClick={() => toggleSidebar('None')}
|
||||||
<DashboardInputField
|
>
|
||||||
onChangeHandler={modifyKey}
|
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||||
type="varName"
|
|
||||||
position={data[0]?.pos}
|
|
||||||
value={data[0]?.key}
|
|
||||||
isDuplicate={false}
|
|
||||||
blurred={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{data[0]?.value
|
|
||||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
|
||||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
|
|
||||||
<DashboardInputField
|
|
||||||
onChangeHandler={modifyValue}
|
|
||||||
type="value"
|
|
||||||
position={data[0].pos}
|
|
||||||
value={data[0]?.value}
|
|
||||||
isDuplicate={false}
|
|
||||||
blurred={true}
|
|
||||||
/>
|
|
||||||
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
|
||||||
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
|
||||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
|
|
||||||
{t("dashboard:sidebar.personal-explanation")}
|
|
||||||
</div>}
|
|
||||||
<div className='mt-4 px-4'>
|
|
||||||
{data[0]?.value &&
|
|
||||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
|
||||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
|
|
||||||
<Toggle
|
|
||||||
enabled={overrideEnabled}
|
|
||||||
setEnabled={setOverrideEnabled}
|
|
||||||
addOverride={modifyValueOverride}
|
|
||||||
pos={data[0]?.pos}
|
|
||||||
/>
|
|
||||||
</div>}
|
|
||||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
|
||||||
<DashboardInputField
|
|
||||||
onChangeHandler={modifyValueOverride}
|
|
||||||
type="value"
|
|
||||||
position={data[0]?.pos}
|
|
||||||
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
|
|
||||||
isDuplicate={false}
|
|
||||||
blurred={true}
|
|
||||||
/>
|
|
||||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
|
||||||
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mt-4 px-4 pointer-events-none">
|
||||||
|
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.key')}</p>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyKey}
|
||||||
|
type="varName"
|
||||||
|
position={data[0]?.pos}
|
||||||
|
value={data[0]?.key}
|
||||||
|
isDuplicate={false}
|
||||||
|
blurred={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data[0]?.value ? (
|
||||||
|
<div
|
||||||
|
className={`relative mt-2 px-4 ${
|
||||||
|
overrideEnabled && 'opacity-40 pointer-events-none'
|
||||||
|
} duration-200`}
|
||||||
|
>
|
||||||
|
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.value')}</p>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyValue}
|
||||||
|
type="value"
|
||||||
|
position={data[0].pos}
|
||||||
|
value={data[0]?.value}
|
||||||
|
isDuplicate={false}
|
||||||
|
blurred
|
||||||
|
/>
|
||||||
|
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
|
||||||
|
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="px-4 text-sm text-bunker-300 pt-4">
|
||||||
|
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1">
|
||||||
|
{t('common:note')}:
|
||||||
|
</span>
|
||||||
|
{t('dashboard:sidebar.personal-explanation')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-4 px-4">
|
||||||
|
{data[0]?.value && (
|
||||||
|
<div className="flex flex-row items-center justify-between my-2 pl-1 pr-2">
|
||||||
|
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.override')}</p>
|
||||||
|
<Toggle
|
||||||
|
enabled={overrideEnabled}
|
||||||
|
setEnabled={setOverrideEnabled}
|
||||||
|
addOverride={modifyValueOverride}
|
||||||
|
pos={data[0]?.pos}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`relative ${
|
||||||
|
!overrideEnabled && 'opacity-40 pointer-events-none'
|
||||||
|
} duration-200`}
|
||||||
|
>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={modifyValueOverride}
|
||||||
|
type="value"
|
||||||
|
position={data[0]?.pos}
|
||||||
|
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
|
||||||
|
isDuplicate={false}
|
||||||
|
blurred
|
||||||
|
/>
|
||||||
|
<div className="absolute right-[0.57rem] top-[0.3rem] z-50">
|
||||||
|
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SecretVersionList secretId={data[0]?.id} />
|
||||||
|
<CommentField
|
||||||
|
comment={data[0]?.comment}
|
||||||
|
modifyComment={modifyComment}
|
||||||
|
position={data[0]?.pos}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SecretVersionList secretId={data[0]?.id} />
|
)}
|
||||||
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
<div className="flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]">
|
||||||
|
<Button
|
||||||
|
text={String(t('common:save-changes'))}
|
||||||
|
onButtonPressed={savePush}
|
||||||
|
color="primary"
|
||||||
|
size="md"
|
||||||
|
active={buttonReady}
|
||||||
|
textDisabled="Saved"
|
||||||
|
/>
|
||||||
|
<DeleteActionButton
|
||||||
|
onSubmit={() =>
|
||||||
|
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
|
||||||
<Button
|
|
||||||
text={String(t("common:save-changes"))}
|
|
||||||
onButtonPressed={savePush}
|
|
||||||
color="primary"
|
|
||||||
size="md"
|
|
||||||
active={buttonReady}
|
|
||||||
textDisabled="Saved"
|
|
||||||
/>
|
|
||||||
<DeleteActionButton
|
|
||||||
onSubmit={() => deleteRow({ ids: data.map(secret => secret.id), secretName: data[0]?.key })}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SideBar;
|
export default SideBar;
|
||||||
|
|||||||
@@ -21,12 +21,8 @@ interface IntegrationAuth {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cloudIntegrationOption: CloudIntegrationOption;
|
cloudIntegrationOption: CloudIntegrationOption;
|
||||||
setSelectedIntegrationOption: (
|
setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
|
||||||
cloudIntegration: CloudIntegrationOption
|
integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
|
||||||
) => void;
|
|
||||||
integrationOptionPress: (
|
|
||||||
cloudIntegrationOption: CloudIntegrationOption
|
|
||||||
) => void;
|
|
||||||
integrationAuths: IntegrationAuth[];
|
integrationAuths: IntegrationAuth[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,11 +30,14 @@ const CloudIntegration = ({
|
|||||||
cloudIntegrationOption,
|
cloudIntegrationOption,
|
||||||
setSelectedIntegrationOption,
|
setSelectedIntegrationOption,
|
||||||
integrationOptionPress,
|
integrationOptionPress,
|
||||||
integrationAuths,
|
integrationAuths
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return integrationAuths ? (
|
return integrationAuths ? (
|
||||||
<div
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
className={`relative ${
|
className={`relative ${
|
||||||
cloudIntegrationOption.isAvailable
|
cloudIntegrationOption.isAvailable
|
||||||
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
||||||
@@ -55,18 +54,17 @@ const CloudIntegration = ({
|
|||||||
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
||||||
height={70}
|
height={70}
|
||||||
width={70}
|
width={70}
|
||||||
alt='integration logo'
|
alt="integration logo"
|
||||||
/>
|
/>
|
||||||
{cloudIntegrationOption.name.split(' ').length > 2 ? (
|
{cloudIntegrationOption.name.split(' ').length > 2 ? (
|
||||||
<div className='font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs'>
|
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
||||||
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
||||||
<div className='text-base'>
|
<div className="text-base">
|
||||||
{cloudIntegrationOption.name.split(' ')[1]}{' '}
|
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
|
||||||
{cloudIntegrationOption.name.split(' ')[2]}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs'>
|
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
||||||
{cloudIntegrationOption.name}
|
{cloudIntegrationOption.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -74,43 +72,45 @@ const CloudIntegration = ({
|
|||||||
integrationAuths
|
integrationAuths
|
||||||
.map((authorization) => authorization.integration)
|
.map((authorization) => authorization.integration)
|
||||||
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
||||||
<div className='absolute group z-40 top-0 right-0 flex flex-row'>
|
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
||||||
<div
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
deleteIntegrationAuth({
|
deleteIntegrationAuth({
|
||||||
integrationAuthId: integrationAuths
|
integrationAuthId: integrationAuths
|
||||||
.filter(
|
.filter(
|
||||||
(authorization) =>
|
(authorization) =>
|
||||||
authorization.integration ==
|
authorization.integration === cloudIntegrationOption.name.toLowerCase()
|
||||||
cloudIntegrationOption.name.toLowerCase()
|
|
||||||
)
|
)
|
||||||
.map((authorization) => authorization._id)[0],
|
.map((authorization) => authorization._id)[0]
|
||||||
});
|
});
|
||||||
|
|
||||||
router.reload();
|
router.reload();
|
||||||
}}
|
}}
|
||||||
className='cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200'
|
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faX} className='text-xs mr-2 py-px' />
|
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
|
||||||
Revoke
|
Revoke
|
||||||
</div>
|
</div>
|
||||||
<div className='w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200'>
|
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
||||||
<FontAwesomeIcon icon={faCheck} className='text-xs mr-2' />
|
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
|
||||||
Authorized
|
Authorized
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!cloudIntegrationOption.isAvailable && (
|
{!cloudIntegrationOption.isAvailable && (
|
||||||
<div className='absolute group z-50 top-0 right-0 flex flex-row'>
|
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||||
<div className='w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90'>
|
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
||||||
Coming Soon
|
Coming Soon
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const CloudIntegrationSection = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`}
|
className="flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2"
|
||||||
>
|
>
|
||||||
<h1 className='font-semibold text-3xl'>
|
<h1 className='font-semibold text-3xl'>
|
||||||
{t('integrations:cloud-integrations')}
|
{t('integrations:cloud-integrations')}
|
||||||
|
|||||||
@@ -7,33 +7,29 @@ interface Framework {
|
|||||||
docsLink: string;
|
docsLink: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FrameworkIntegration = ({ framework }: { framework: Framework }) => {
|
const FrameworkIntegration = ({ framework }: { framework: Framework }) => (
|
||||||
return (
|
<a
|
||||||
<a
|
href={framework.docsLink}
|
||||||
href={framework.docsLink}
|
rel="noopener"
|
||||||
rel='noopener'
|
className="relative flex flex-row justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer"
|
||||||
className={`relative flex flex-row items-center justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer`}
|
>
|
||||||
|
<div
|
||||||
|
className={`hover:bg-white/10 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
||||||
|
framework?.name?.split(' ').length > 1 ? 'text-sm px-1' : 'text-xl px-2'
|
||||||
|
} text-center w-full max-w-xs`}
|
||||||
>
|
>
|
||||||
<div
|
{framework?.image && (
|
||||||
className={`hover:bg-white/10 duration-200 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
<Image
|
||||||
framework?.name?.split(' ').length > 1
|
src={`/images/integrations/${framework.image}.png`}
|
||||||
? 'text-sm px-1'
|
height={framework?.name ? 60 : 90}
|
||||||
: 'text-xl px-2'
|
width={framework?.name ? 60 : 90}
|
||||||
} text-center w-full max-w-xs`}
|
alt="integration logo"
|
||||||
>
|
/>
|
||||||
{framework?.image && (
|
)}
|
||||||
<Image
|
{framework?.name && framework?.image && <div className="h-2" />}
|
||||||
src={`/images/integrations/${framework.image}.png`}
|
{framework?.name && framework.name}
|
||||||
height={framework?.name ? 60 : 90}
|
</div>
|
||||||
width={framework?.name ? 60 : 90}
|
</a>
|
||||||
alt='integration logo'
|
);
|
||||||
></Image>
|
|
||||||
)}
|
|
||||||
{framework?.name && framework?.image && <div className='h-2'></div>}
|
|
||||||
{framework?.name && framework.name}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FrameworkIntegration;
|
export default FrameworkIntegration;
|
||||||
|
|||||||
@@ -1,33 +1,26 @@
|
|||||||
import { useEffect, useState } from "react";
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import { useRouter } from "next/router";
|
import { useEffect, useState } from 'react';
|
||||||
import {
|
import { useRouter } from 'next/router';
|
||||||
faArrowRight,
|
import Button from '@app/components/basic/buttons/Button';
|
||||||
faRotate,
|
import ListBox from '@app/components/basic/Listbox';
|
||||||
faX,
|
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
// TODO: This needs to be moved from public folder
|
||||||
// TODO: This needs to be moved from public folder
|
import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
|
||||||
import {
|
|
||||||
contextNetlifyMapping,
|
|
||||||
reverseContextNetlifyMapping,
|
|
||||||
} from "public/data/frequentConstants";
|
|
||||||
|
|
||||||
import Button from "~/components/basic/buttons/Button";
|
import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
|
||||||
import ListBox from "~/components/basic/Listbox";
|
import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
|
||||||
|
import updateIntegration from '../../pages/api/integrations/updateIntegration';
|
||||||
|
|
||||||
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
|
interface TIntegration {
|
||||||
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps";
|
_id: string;
|
||||||
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
app?: string;
|
||||||
|
target?: string;
|
||||||
interface Integration {
|
environment: string;
|
||||||
_id: string;
|
integration: string;
|
||||||
app?: string;
|
integrationAuth: string;
|
||||||
target?: string;
|
isActive: boolean;
|
||||||
environment: string;
|
context: string;
|
||||||
integration: string;
|
|
||||||
integrationAuth: string;
|
|
||||||
isActive: boolean;
|
|
||||||
context: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IntegrationApp {
|
interface IntegrationApp {
|
||||||
@@ -36,219 +29,190 @@ interface IntegrationApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
integration: Integration;
|
integration: TIntegration;
|
||||||
environments: Array<{ name: string; slug: string }>;
|
environments: Array<{ name: string; slug: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Integration = ({
|
const Integration = ({ integration, environments = [] }: Props) => {
|
||||||
integration,
|
// set initial environment. This find will only execute when component is mounting
|
||||||
environments = []
|
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||||
}:Props ) => {
|
environments.find(({ slug }) => slug === integration.environment) || {
|
||||||
// set initial environment. This find will only execute when component is mounting
|
name: '',
|
||||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<
|
slug: ''
|
||||||
Props['environments'][0]
|
|
||||||
>(
|
|
||||||
environments.find(({ slug }) => slug === integration.environment) || {
|
|
||||||
name: '',
|
|
||||||
slug: '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [fileState, setFileState] = useState([]);
|
|
||||||
const router = useRouter();
|
|
||||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
|
||||||
const [integrationApp, setIntegrationApp] = useState(""); // integration app name
|
|
||||||
const [integrationTarget, setIntegrationTarget] = useState(""); // vercel-specific integration param
|
|
||||||
const [integrationContext, setIntegrationContext] = useState(""); // netlify-specific integration param
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
const loadIntegration = async () => {
|
|
||||||
interface App {
|
|
||||||
name: string;
|
|
||||||
siteId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
|
||||||
integrationAuthId: integration.integrationAuth,
|
|
||||||
});
|
|
||||||
|
|
||||||
setApps(tempApps);
|
|
||||||
setIntegrationApp(
|
|
||||||
integration.app ? integration.app : tempApps[0].name
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (integration.integration) {
|
|
||||||
case "vercel":
|
|
||||||
setIntegrationTarget(
|
|
||||||
integration?.target
|
|
||||||
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
|
||||||
: "Development"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "netlify":
|
|
||||||
setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadIntegration();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderIntegrationSpecificParams = (integration: Integration) => {
|
|
||||||
try {
|
|
||||||
switch (integration.integration) {
|
|
||||||
case "vercel":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">
|
|
||||||
ENVIRONMENT
|
|
||||||
</div>
|
|
||||||
<ListBox
|
|
||||||
data={!integration.isActive ? [
|
|
||||||
"Development",
|
|
||||||
"Preview",
|
|
||||||
"Production"
|
|
||||||
] : null}
|
|
||||||
selected={integrationTarget}
|
|
||||||
onChange={setIntegrationTarget}
|
|
||||||
isFull={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case "netlify":
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
|
||||||
CONTEXT
|
|
||||||
</div>
|
|
||||||
<ListBox
|
|
||||||
data={!integration.isActive ? [
|
|
||||||
"Production",
|
|
||||||
"Deploy previews",
|
|
||||||
"Branch deploys",
|
|
||||||
"Local development"
|
|
||||||
] : null}
|
|
||||||
selected={integrationContext}
|
|
||||||
onChange={setIntegrationContext}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <div></div>;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
if (!integrationApp || apps.length === 0) return <div></div>
|
const [fileState, setFileState] = useState([]);
|
||||||
|
const router = useRouter();
|
||||||
return (
|
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||||
<div className='max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between'>
|
const [integrationApp, setIntegrationApp] = useState(''); // integration app name
|
||||||
<div className='flex'>
|
const [integrationTarget, setIntegrationTarget] = useState(''); // vercel-specific integration param
|
||||||
<div>
|
const [integrationContext, setIntegrationContext] = useState(''); // netlify-specific integration param
|
||||||
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
|
||||||
ENVIRONMENT
|
useEffect(() => {
|
||||||
</p>
|
const loadIntegration = async () => {
|
||||||
<ListBox
|
interface App {
|
||||||
data={
|
name: string;
|
||||||
!integration.isActive
|
siteId?: string;
|
||||||
? environments.map(({ name }) => name)
|
}
|
||||||
: null
|
|
||||||
}
|
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||||
selected={integrationEnvironment.name}
|
integrationAuthId: integration.integrationAuth
|
||||||
onChange={(envName) =>
|
});
|
||||||
setIntegrationEnvironment(
|
|
||||||
environments.find(({ name }) => envName === name) || {
|
setApps(tempApps);
|
||||||
name: 'unknown',
|
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
||||||
slug: 'unknown',
|
|
||||||
}
|
switch (integration.integration) {
|
||||||
)
|
case 'vercel':
|
||||||
}
|
setIntegrationTarget(
|
||||||
isFull={true}
|
integration?.target
|
||||||
/>
|
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
||||||
</div>
|
: 'Development'
|
||||||
<div className='pt-2'>
|
);
|
||||||
<FontAwesomeIcon
|
break;
|
||||||
icon={faArrowRight}
|
case 'netlify':
|
||||||
className='mx-4 text-gray-400 mt-8'
|
setIntegrationContext(
|
||||||
/>
|
integration?.context ? contextNetlifyMapping[integration.context] : 'Local development'
|
||||||
</div>
|
);
|
||||||
<div className='mr-2'>
|
break;
|
||||||
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
default:
|
||||||
INTEGRATION
|
break;
|
||||||
</p>
|
}
|
||||||
<div className='py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300'>
|
};
|
||||||
{integration.integration.charAt(0).toUpperCase() +
|
|
||||||
integration.integration.slice(1)}
|
loadIntegration();
|
||||||
</div>
|
}, []);
|
||||||
</div>
|
|
||||||
<div className='mr-2'>
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
<div className='text-gray-400 text-xs font-semibold mb-2'>APP</div>
|
const renderIntegrationSpecificParams = (integration: TIntegration) => {
|
||||||
<ListBox
|
try {
|
||||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
switch (integration.integration) {
|
||||||
selected={integrationApp}
|
case 'vercel':
|
||||||
onChange={(app) => {
|
return (
|
||||||
setIntegrationApp(app);
|
<div>
|
||||||
}}
|
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
||||||
/>
|
<ListBox
|
||||||
</div>
|
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
||||||
{renderIntegrationSpecificParams(integration)}
|
selected={integrationTarget}
|
||||||
</div>
|
onChange={setIntegrationTarget}
|
||||||
<div className='flex items-end'>
|
isFull
|
||||||
{integration.isActive ? (
|
|
||||||
<div className='max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4'>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={faRotate}
|
|
||||||
className='text-lg mr-2.5 text-primary animate-spin'
|
|
||||||
/>
|
/>
|
||||||
<div className='text-gray-300 font-semibold'>In Sync</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
);
|
||||||
<Button
|
case 'netlify':
|
||||||
text='Start Integration'
|
return (
|
||||||
onButtonPressed={async () => {
|
<div>
|
||||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
|
||||||
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
<ListBox
|
||||||
|
data={
|
||||||
|
!integration.isActive
|
||||||
|
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
selected={integrationContext}
|
||||||
|
onChange={setIntegrationContext}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
await updateIntegration({
|
return <div />;
|
||||||
integrationId: integration._id,
|
|
||||||
environment: integrationEnvironment.slug,
|
|
||||||
app: integrationApp,
|
|
||||||
isActive: true,
|
|
||||||
target: integrationTarget
|
|
||||||
? integrationTarget.toLowerCase()
|
|
||||||
: null,
|
|
||||||
context: integrationContext
|
|
||||||
? reverseContextNetlifyMapping[integrationContext]
|
|
||||||
: null,
|
|
||||||
siteId,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.reload();
|
|
||||||
}}
|
|
||||||
color='mineshaft'
|
|
||||||
size='md'
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className='opacity-50 hover:opacity-100 duration-200 ml-2'>
|
|
||||||
<Button
|
|
||||||
onButtonPressed={async () => {
|
|
||||||
await deleteIntegration({
|
|
||||||
integrationId: integration._id,
|
|
||||||
});
|
|
||||||
router.reload();
|
|
||||||
}}
|
|
||||||
color='red'
|
|
||||||
size='icon-md'
|
|
||||||
icon={faX}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Integration;
|
if (!integrationApp || apps.length === 0) return <div />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||||
|
<div className="flex">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
||||||
|
<ListBox
|
||||||
|
data={!integration.isActive ? environments.map(({ name }) => name) : null}
|
||||||
|
selected={integrationEnvironment.name}
|
||||||
|
onChange={(envName) =>
|
||||||
|
setIntegrationEnvironment(
|
||||||
|
environments.find(({ name }) => envName === name) || {
|
||||||
|
name: 'unknown',
|
||||||
|
slug: 'unknown'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
isFull
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2">
|
||||||
|
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
|
||||||
|
</div>
|
||||||
|
<div className="mr-2">
|
||||||
|
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
||||||
|
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||||
|
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mr-2">
|
||||||
|
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
|
||||||
|
<ListBox
|
||||||
|
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||||
|
selected={integrationApp}
|
||||||
|
onChange={(app) => {
|
||||||
|
setIntegrationApp(app);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{renderIntegrationSpecificParams(integration)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
{integration.isActive ? (
|
||||||
|
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||||
|
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
|
||||||
|
<div className="text-gray-300 font-semibold">In Sync</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
text="Start Integration"
|
||||||
|
onButtonPressed={async () => {
|
||||||
|
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||||
|
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
||||||
|
|
||||||
|
await updateIntegration({
|
||||||
|
integrationId: integration._id,
|
||||||
|
environment: integrationEnvironment.slug,
|
||||||
|
app: integrationApp,
|
||||||
|
isActive: true,
|
||||||
|
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
||||||
|
context: integrationContext
|
||||||
|
? reverseContextNetlifyMapping[integrationContext]
|
||||||
|
: null,
|
||||||
|
siteId
|
||||||
|
});
|
||||||
|
|
||||||
|
router.reload();
|
||||||
|
}}
|
||||||
|
color="mineshaft"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||||
|
<Button
|
||||||
|
onButtonPressed={async () => {
|
||||||
|
await deleteIntegration({
|
||||||
|
integrationId: integration._id
|
||||||
|
});
|
||||||
|
router.reload();
|
||||||
|
}}
|
||||||
|
color="red"
|
||||||
|
size="icon-md"
|
||||||
|
icon={faX}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Integration;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import guidGenerator from '~/utilities/randomId';
|
import guidGenerator from '@app/components/utilities/randomId';
|
||||||
|
|
||||||
import Integration from './Integration';
|
import Integration from './Integration';
|
||||||
|
|
||||||
@@ -17,29 +17,21 @@ interface IntegrationType {
|
|||||||
context: string;
|
context: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectIntegrationSection = ({
|
const ProjectIntegrationSection = ({ integrations, environments = [] }: Props) =>
|
||||||
integrations,
|
integrations.length > 0 ? (
|
||||||
environments = [],
|
<div className="mb-12">
|
||||||
}: Props) => {
|
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
||||||
return integrations.length > 0 ? (
|
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
||||||
<div className='mb-12'>
|
<p className="text-base text-gray-400">
|
||||||
<div className='flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2'>
|
|
||||||
<h1 className='font-semibold text-3xl'>Current Integrations</h1>
|
|
||||||
<p className='text-base text-gray-400'>
|
|
||||||
Manage your integrations of Infisical with third-party services.
|
Manage your integrations of Infisical with third-party services.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{integrations.map((integration: IntegrationType) => (
|
{integrations.map((integration: IntegrationType) => (
|
||||||
<Integration
|
<Integration key={guidGenerator()} integration={integration} environments={environments} />
|
||||||
key={guidGenerator()}
|
|
||||||
integration={integration}
|
|
||||||
environments={environments}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div></div>
|
<div />
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectIntegrationSection;
|
export default ProjectIntegrationSection;
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||||
/* eslint-disable react/jsx-key */
|
/* eslint-disable react/jsx-key */
|
||||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router';
|
||||||
import { TFunction, useTranslation } from "next-i18next";
|
import { TFunction, useTranslation } from 'next-i18next';
|
||||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
import logout from '@app/pages/api/auth/Logout';
|
||||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
|
||||||
import {
|
import {
|
||||||
faAngleDown,
|
faAngleDown,
|
||||||
faBook,
|
faBook,
|
||||||
@@ -12,40 +14,37 @@ import {
|
|||||||
faEnvelope,
|
faEnvelope,
|
||||||
faGear,
|
faGear,
|
||||||
faPlus,
|
faPlus,
|
||||||
faRightFromBracket,
|
faRightFromBracket
|
||||||
faUpRightFromSquare,
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
|
||||||
|
|
||||||
import logout from "~/pages/api/auth/Logout";
|
import getOrganization from '../../pages/api/organization/GetOrg';
|
||||||
|
import getOrganizations from '../../pages/api/organization/getOrgs';
|
||||||
import getOrganization from "../../pages/api/organization/GetOrg";
|
import getUser from '../../pages/api/user/getUser';
|
||||||
import getOrganizations from "../../pages/api/organization/getOrgs";
|
import guidGenerator from '../utilities/randomId';
|
||||||
import getUser from "../../pages/api/user/getUser";
|
|
||||||
import guidGenerator from "../utilities/randomId";
|
|
||||||
|
|
||||||
const supportOptions = (t: TFunction) => [
|
const supportOptions = (t: TFunction) => [
|
||||||
[
|
[
|
||||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
|
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
|
||||||
t("nav:support.slack"),
|
t('nav:support.slack'),
|
||||||
"https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA",
|
'https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
||||||
t("nav:support.docs"),
|
t('nav:support.docs'),
|
||||||
"https://infisical.com/docs/getting-started/introduction",
|
'https://infisical.com/docs/getting-started/introduction'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
|
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
|
||||||
t("nav:support.issue"),
|
t('nav:support.issue'),
|
||||||
"https://github.com/Infisical/infisical-cli/issues",
|
'https://github.com/Infisical/infisical-cli/issues'
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
||||||
t("nav:support.email"),
|
t('nav:support.email'),
|
||||||
"mailto:support@infisical.com",
|
'mailto:support@infisical.com'
|
||||||
],
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface ICurrentOrg {
|
export interface ICurrentOrg {
|
||||||
@@ -79,17 +78,17 @@ export default function Navbar() {
|
|||||||
setUser(userData);
|
setUser(userData);
|
||||||
const orgsData = await getOrganizations();
|
const orgsData = await getOrganizations();
|
||||||
setOrgs(orgsData);
|
setOrgs(orgsData);
|
||||||
const currentOrg = await getOrganization({
|
const currentUserOrg = await getOrganization({
|
||||||
orgId: String(localStorage.getItem("orgData.id")),
|
orgId: String(localStorage.getItem('orgData.id'))
|
||||||
});
|
});
|
||||||
setCurrentOrg(currentOrg);
|
setCurrentOrg(currentUserOrg);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const closeApp = async () => {
|
const closeApp = async () => {
|
||||||
console.log("Logging out...");
|
console.log('Logging out...');
|
||||||
await logout();
|
await logout();
|
||||||
router.push("/login");
|
router.push('/login');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -97,28 +96,26 @@ export default function Navbar() {
|
|||||||
<div className="m-auto flex justify-start items-center mx-4">
|
<div className="m-auto flex justify-start items-center mx-4">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<div className="flex justify-center py-4">
|
<div className="flex justify-center py-4">
|
||||||
<Image
|
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
|
||||||
src="/images/logotransparent.png"
|
|
||||||
height={23}
|
|
||||||
width={57}
|
|
||||||
alt="logo"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<a className="text-2xl text-white font-semibold mx-2">Infisical</a>
|
<a href="#" className="text-2xl text-white font-semibold mx-2">
|
||||||
|
Infisical
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex justify-start items-center mx-2 z-40">
|
<div className="relative flex justify-start items-center mx-2 z-40">
|
||||||
<a
|
<a
|
||||||
href="https://infisical.com/docs/getting-started/introduction"
|
href="https://infisical.com/docs/getting-started/introduction"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center">
|
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center"
|
||||||
|
>
|
||||||
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
|
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
|
||||||
Docs
|
Docs
|
||||||
</a>
|
</a>
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
<div className="mr-4">
|
<div className="mr-4">
|
||||||
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
<Menu.Button className="inline-flex w-full justify-center px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||||
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
|
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +148,7 @@ export default function Navbar() {
|
|||||||
</Menu>
|
</Menu>
|
||||||
<Menu as="div" className="relative inline-block text-left mr-4">
|
<Menu as="div" className="relative inline-block text-left mr-4">
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button className="inline-flex w-full justify-center rounded-md pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
<Menu.Button className="inline-flex w-full justify-center pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||||
{user?.firstName} {user?.lastName}
|
{user?.firstName} {user?.lastName}
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faAngleDown}
|
icon={faAngleDown}
|
||||||
@@ -171,12 +168,13 @@ export default function Navbar() {
|
|||||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
|
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
|
||||||
<div className="px-1 py-1 ">
|
<div className="px-1 py-1 ">
|
||||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||||
{t("nav:user.signed-in-as")}
|
{t('nav:user.signed-in-as')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onKeyDown={() => null}
|
||||||
router.push("/settings/personal/" + router.query.id)
|
role="button"
|
||||||
}
|
tabIndex={0}
|
||||||
|
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
|
||||||
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
|
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||||
>
|
>
|
||||||
<div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300">
|
<div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300">
|
||||||
@@ -185,13 +183,10 @@ export default function Navbar() {
|
|||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-300 px-2 pt-1 text-sm">
|
<p className="text-gray-300 px-2 pt-1 text-sm">
|
||||||
{" "}
|
{' '}
|
||||||
{user?.firstName} {user?.lastName}
|
{user?.firstName} {user?.lastName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-400 px-2 pb-1 text-xs">
|
<p className="text-gray-400 px-2 pb-1 text-xs"> {user?.email}</p>
|
||||||
{" "}
|
|
||||||
{user?.email}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faGear}
|
icon={faGear}
|
||||||
@@ -202,21 +197,20 @@ export default function Navbar() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="px-2 pt-2">
|
<div className="px-2 pt-2">
|
||||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||||
{t("nav:user.current-organization")}
|
{t('nav:user.current-organization')}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onKeyDown={() => null}
|
||||||
router.push("/settings/org/" + router.query.id)
|
role="button"
|
||||||
}
|
tabIndex={0}
|
||||||
|
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||||
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
|
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||||
>
|
>
|
||||||
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
||||||
{currentOrg?.name?.charAt(0)}
|
{currentOrg?.name?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<p className="text-gray-300 px-2 text-sm">
|
<p className="text-gray-300 px-2 text-sm">{currentOrg?.name}</p>
|
||||||
{currentOrg?.name}
|
|
||||||
</p>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faGear}
|
icon={faGear}
|
||||||
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
|
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
|
||||||
@@ -225,56 +219,57 @@ export default function Navbar() {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
// onClick={buttonAction}
|
// onClick={buttonAction}
|
||||||
|
type="button"
|
||||||
className="cursor-pointer w-full"
|
className="cursor-pointer w-full"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onKeyDown={() => null}
|
||||||
router.push("/settings/billing/" + router.query.id)
|
role="button"
|
||||||
}
|
tabIndex={0}
|
||||||
|
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
|
||||||
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
|
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faCoins} />
|
||||||
className="text-lg pl-1.5 pr-3"
|
<div className="text-sm">{t('nav:user.usage-billing')}</div>
|
||||||
icon={faCoins}
|
|
||||||
/>
|
|
||||||
<div className="text-sm">{t("nav:user.usage-billing")}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
// onClick={buttonAction}
|
// onClick={buttonAction}
|
||||||
className="cursor-pointer w-full mb-2"
|
className="cursor-pointer w-full mb-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onKeyDown={() => null}
|
||||||
router.push(
|
role="button"
|
||||||
"/settings/org/" + router.query.id + "?invite"
|
tabIndex={0}
|
||||||
)
|
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
|
||||||
}
|
|
||||||
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
|
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
|
||||||
>
|
>
|
||||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||||
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
||||||
</span>
|
</span>
|
||||||
<div className="text-sm ml-1">{t("nav:user.invite")}</div>
|
<div className="text-sm ml-1">{t('nav:user.invite')}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{orgs?.length > 1 && (
|
{orgs?.length > 1 && (
|
||||||
<div className="px-1 pt-1">
|
<div className="px-1 pt-1">
|
||||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||||
{t("nav:user.other-organizations")}
|
{t('nav:user.other-organizations')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start px-1 mt-3 mb-2">
|
<div className="flex flex-col items-start px-1 mt-3 mb-2">
|
||||||
{orgs
|
{orgs
|
||||||
.filter(
|
.filter(
|
||||||
(org: { _id: string }) =>
|
(org: { _id: string }) => org._id !== localStorage.getItem('orgData.id')
|
||||||
org._id != localStorage.getItem("orgData.id")
|
|
||||||
)
|
)
|
||||||
.map((org: { _id: string; name: string }) => (
|
.map((org: { _id: string; name: string }) => (
|
||||||
<div
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
key={guidGenerator()}
|
key={guidGenerator()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.setItem("orgData.id", org._id);
|
localStorage.setItem('orgData.id', org._id);
|
||||||
router.reload();
|
router.reload();
|
||||||
}}
|
}}
|
||||||
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
|
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
|
||||||
@@ -283,9 +278,7 @@ export default function Navbar() {
|
|||||||
{org.name.charAt(0)}
|
{org.name.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<p className="text-gray-300 px-2 text-sm">
|
<p className="text-gray-300 px-2 text-sm">{org.name}</p>
|
||||||
{org.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -296,11 +289,10 @@ export default function Navbar() {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
{({ active }) => (
|
{({ active }) => (
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
onClick={closeApp}
|
onClick={closeApp}
|
||||||
className={`${
|
className={`${
|
||||||
active
|
active ? 'bg-red font-semibold text-white' : 'text-gray-400'
|
||||||
? "bg-red font-semibold text-white"
|
|
||||||
: "text-gray-400"
|
|
||||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||||
>
|
>
|
||||||
<div className="relative flex justify-start items-center cursor-pointer select-none">
|
<div className="relative flex justify-start items-center cursor-pointer select-none">
|
||||||
@@ -308,7 +300,7 @@ export default function Navbar() {
|
|||||||
className="text-lg ml-1.5 mr-3"
|
className="text-lg ml-1.5 mr-3"
|
||||||
icon={faRightFromBracket}
|
icon={faRightFromBracket}
|
||||||
/>
|
/>
|
||||||
{t("common:logout")}
|
{t('common:logout')}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import getOrganization from '@app/pages/api/organization/GetOrg';
|
||||||
|
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
|
||||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import getOrganization from '~/pages/api/organization/GetOrg';
|
|
||||||
import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the component at the top of almost every page.
|
* This is the component at the top of almost every page.
|
||||||
* It shows how to navigate to a certain page.
|
* It shows how to navigate to a certain page.
|
||||||
@@ -17,7 +16,7 @@ import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
|
|||||||
*/
|
*/
|
||||||
export default function NavHeader({
|
export default function NavHeader({
|
||||||
pageName,
|
pageName,
|
||||||
isProjectRelated,
|
isProjectRelated
|
||||||
}: {
|
}: {
|
||||||
pageName: string;
|
pageName: string;
|
||||||
isProjectRelated?: boolean;
|
isProjectRelated?: boolean;
|
||||||
@@ -30,12 +29,12 @@ export default function NavHeader({
|
|||||||
(async () => {
|
(async () => {
|
||||||
const orgId = localStorage.getItem('orgData.id');
|
const orgId = localStorage.getItem('orgData.id');
|
||||||
const org = await getOrganization({
|
const org = await getOrganization({
|
||||||
orgId: orgId ? orgId : '',
|
orgId: orgId || ''
|
||||||
});
|
});
|
||||||
setOrgName(org.name);
|
setOrgName(org.name);
|
||||||
|
|
||||||
const workspace = await getProjectInfo({
|
const workspace = await getProjectInfo({
|
||||||
projectId: String(router.query.id),
|
projectId: String(router.query.id)
|
||||||
});
|
});
|
||||||
setWorkspaceName(workspace.name);
|
setWorkspaceName(workspace.name);
|
||||||
})();
|
})();
|
||||||
@@ -43,27 +42,19 @@ export default function NavHeader({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='pt-20 ml-6 flex flex-row items-center'>
|
<div className="pt-20 ml-6 flex flex-row items-center">
|
||||||
<div className='bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2'>
|
<div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2">
|
||||||
{orgName?.charAt(0)}
|
{orgName?.charAt(0)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-primary text-sm font-semibold'>{orgName}</div>
|
<div className="text-primary text-sm font-semibold">{orgName}</div>
|
||||||
{isProjectRelated && (
|
{isProjectRelated && (
|
||||||
<>
|
<>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||||
icon={faAngleRight}
|
<div className="font-semibold text-primary text-sm">{workspaceName}</div>
|
||||||
className='ml-3 text-sm text-gray-400 mr-3'
|
|
||||||
/>
|
|
||||||
<div className='font-semibold text-primary text-sm'>
|
|
||||||
{workspaceName}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||||
icon={faAngleRight}
|
<div className="text-gray-400 text-sm">{pageName}</div>
|
||||||
className='ml-3 text-sm text-gray-400 mr-3'
|
|
||||||
/>
|
|
||||||
<div className='text-gray-400 text-sm'>{pageName}</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,48 @@
|
|||||||
import React, { useState } from "react";
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import ReactCodeInput from "react-code-input";
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from "next-i18next";
|
import ReactCodeInput from 'react-code-input';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
|
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
|
||||||
|
|
||||||
import Button from "../basic/buttons/Button";
|
|
||||||
import Error from "../basic/Error";
|
|
||||||
|
|
||||||
|
import Button from '../basic/buttons/Button';
|
||||||
|
import Error from '../basic/Error';
|
||||||
|
|
||||||
// The style for the verification code input
|
// The style for the verification code input
|
||||||
const props = {
|
const props = {
|
||||||
inputStyle: {
|
inputStyle: {
|
||||||
fontFamily: "monospace",
|
fontFamily: 'monospace',
|
||||||
margin: "4px",
|
margin: '4px',
|
||||||
MozAppearance: "textfield",
|
MozAppearance: 'textfield',
|
||||||
width: "55px",
|
width: '55px',
|
||||||
borderRadius: "5px",
|
borderRadius: '5px',
|
||||||
fontSize: "24px",
|
fontSize: '24px',
|
||||||
height: "55px",
|
height: '55px',
|
||||||
paddingLeft: "7",
|
paddingLeft: '7',
|
||||||
backgroundColor: "#0d1117",
|
backgroundColor: '#0d1117',
|
||||||
color: "white",
|
color: 'white',
|
||||||
border: "1px solid #2d2f33",
|
border: '1px solid #2d2f33',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
outlineColor: "#8ca542",
|
outlineColor: '#8ca542',
|
||||||
borderColor: "#2d2f33"
|
borderColor: '#2d2f33'
|
||||||
},
|
}
|
||||||
} as const;
|
} as const;
|
||||||
const propsPhone = {
|
const propsPhone = {
|
||||||
inputStyle: {
|
inputStyle: {
|
||||||
fontFamily: "monospace",
|
fontFamily: 'monospace',
|
||||||
margin: "4px",
|
margin: '4px',
|
||||||
MozAppearance: "textfield",
|
MozAppearance: 'textfield',
|
||||||
width: "40px",
|
width: '40px',
|
||||||
borderRadius: "5px",
|
borderRadius: '5px',
|
||||||
fontSize: "24px",
|
fontSize: '24px',
|
||||||
height: "40px",
|
height: '40px',
|
||||||
paddingLeft: "7",
|
paddingLeft: '7',
|
||||||
backgroundColor: "#0d1117",
|
backgroundColor: '#0d1117',
|
||||||
color: "white",
|
color: 'white',
|
||||||
border: "1px solid #2d2f33",
|
border: '1px solid #2d2f33',
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
outlineColor: "#8ca542",
|
outlineColor: '#8ca542',
|
||||||
borderColor: "#2d2f33"
|
borderColor: '#2d2f33'
|
||||||
},
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
interface CodeInputStepProps {
|
interface CodeInputStepProps {
|
||||||
@@ -55,17 +54,21 @@ interface CodeInputStepProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the second step of sign up where users need to verify their email
|
* This is the second step of sign up where users need to verify their email
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {string} obj.email - user's email to which we just sent a verification email
|
* @param {string} obj.email - user's email to which we just sent a verification email
|
||||||
* @param {function} obj.incrementStep - goes to the next step of signup
|
* @param {function} obj.incrementStep - goes to the next step of signup
|
||||||
* @param {function} obj.setCode - state updating function that set the current value of the emai verification code
|
* @param {function} obj.setCode - state updating function that set the current value of the emai verification code
|
||||||
* @param {boolean} obj.codeError - whether the code was inputted wrong or now
|
* @param {boolean} obj.codeError - whether the code was inputted wrong or now
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function CodeInputStep({ email, incrementStep, setCode, codeError }: CodeInputStepProps): JSX.Element {
|
export default function CodeInputStep({
|
||||||
|
email,
|
||||||
|
incrementStep,
|
||||||
|
setCode,
|
||||||
|
codeError
|
||||||
|
}: CodeInputStepProps): JSX.Element {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isResendingVerificationEmail, setIsResendingVerificationEmail] =
|
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
|
||||||
useState(false);
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const resendVerificationEmail = async () => {
|
const resendVerificationEmail = async () => {
|
||||||
@@ -80,12 +83,8 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
||||||
<p className="text-l flex justify-center text-bunker-300">
|
<p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
|
||||||
{t("signup:step2-message")}
|
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
|
||||||
</p>
|
|
||||||
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">
|
|
||||||
{email}{" "}
|
|
||||||
</p>
|
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<ReactCodeInput
|
<ReactCodeInput
|
||||||
name=""
|
name=""
|
||||||
@@ -108,28 +107,28 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
|
|||||||
className="mt-2 mb-6"
|
className="mt-2 mb-6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{codeError && <Error text={t("signup:step2-code-error")} />}
|
{codeError && <Error text={t('signup:step2-code-error')} />}
|
||||||
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||||
<Button
|
<Button text={t('signup:verify') ?? ''} onButtonPressed={incrementStep} size="lg" />
|
||||||
text={t("signup:verify") ?? ""}
|
|
||||||
onButtonPressed={incrementStep}
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
||||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||||
<span className="text-bunker-400">
|
<span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
|
||||||
{t("signup:step2-resend-alert")}
|
<u
|
||||||
</span>
|
className={`font-normal ${
|
||||||
<u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}>
|
isResendingVerificationEmail
|
||||||
<button disabled={isLoading} onClick={resendVerificationEmail}>
|
? 'text-bunker-400'
|
||||||
{isResendingVerificationEmail ? t("signup:step2-resend-progress") : t("signup:step2-resend-submit")}
|
: 'text-primary-700 hover:text-primary duration-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
|
||||||
|
{isResendingVerificationEmail
|
||||||
|
? t('signup:step2-resend-progress')
|
||||||
|
: t('signup:step2-resend-submit')}
|
||||||
</button>
|
</button>
|
||||||
</u>
|
</u>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-bunker-400 pb-2">
|
<p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
|
||||||
{t("signup:step2-spam-alert")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,37 +25,37 @@ export default function DonwloadBackupPDFStep({
|
|||||||
incrementStep,
|
incrementStep,
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
name,
|
name
|
||||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl'>
|
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
|
||||||
<p className='text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary'>
|
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||||
{t('signup:step4-message')}
|
{t('signup:step4-message')}
|
||||||
</p>
|
</p>
|
||||||
<div className='flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2'>
|
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
|
||||||
<div>{t('signup:step4-description1')}</div>
|
<div>{t('signup:step4-description1')}</div>
|
||||||
<div className='mt-3'>{t('signup:step4-description2')}</div>
|
<div className="mt-3">{t('signup:step4-description2')}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4'>
|
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
|
||||||
<FontAwesomeIcon icon={faWarning} className='ml-2 mr-4 text-4xl' />
|
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
|
||||||
{t('signup:step4-description3')}
|
{t('signup:step4-description3')}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg'>
|
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
|
||||||
<Button
|
<Button
|
||||||
text='Download PDF'
|
text="Download PDF"
|
||||||
onButtonPressed={async () => {
|
onButtonPressed={async () => {
|
||||||
await issueBackupKey({
|
await issueBackupKey({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
personalName: name,
|
personalName: name,
|
||||||
setBackupKeyError: (value: boolean) => {},
|
setBackupKeyError: () => {},
|
||||||
setBackupKeyIssued: (value: boolean) => {},
|
setBackupKeyIssued: () => {}
|
||||||
});
|
});
|
||||||
incrementStep();
|
incrementStep();
|
||||||
}}
|
}}
|
||||||
size='lg'
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from 'react';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
|
||||||
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
|
|
||||||
|
|
||||||
import Button from "../basic/buttons/Button";
|
|
||||||
import InputField from "../basic/InputField";
|
|
||||||
|
|
||||||
|
import Button from '../basic/buttons/Button';
|
||||||
|
import InputField from '../basic/InputField';
|
||||||
|
|
||||||
interface DownloadBackupPDFStepProps {
|
interface DownloadBackupPDFStepProps {
|
||||||
incrementStep: () => void;
|
incrementStep: () => void;
|
||||||
email: string;
|
email: string;
|
||||||
setEmail: (value: string) => void;
|
setEmail: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the first step of the sign up process - users need to enter their email
|
* This is the first step of the sign up process - users need to enter their email
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {string} obj.email - email of a user signing up
|
* @param {string} obj.email - email of a user signing up
|
||||||
* @param {function} obj.setEmail - funciton that manages the state of the email variable
|
* @param {function} obj.setEmail - funciton that manages the state of the email variable
|
||||||
* @param {function} obj.incrementStep - function to go to the next step of the signup flow
|
* @param {function} obj.incrementStep - function to go to the next step of the signup flow
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function EnterEmailStep({ email, setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element {
|
export default function EnterEmailStep({
|
||||||
|
email,
|
||||||
|
setEmail,
|
||||||
|
incrementStep
|
||||||
|
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||||
const [emailError, setEmailError] = useState(false);
|
const [emailError, setEmailError] = useState(false);
|
||||||
const [emailErrorMessage, setEmailErrorMessage] = useState("");
|
const [emailErrorMessage, setEmailErrorMessage] = useState('');
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,15 +36,11 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
|||||||
let emailCheckBool = false;
|
let emailCheckBool = false;
|
||||||
if (!email) {
|
if (!email) {
|
||||||
setEmailError(true);
|
setEmailError(true);
|
||||||
setEmailErrorMessage("Please enter your email.");
|
setEmailErrorMessage('Please enter your email.');
|
||||||
emailCheckBool = true;
|
emailCheckBool = true;
|
||||||
} else if (
|
} else if (!email.includes('@') || !email.includes('.') || !/[a-z]/.test(email)) {
|
||||||
!email.includes("@") ||
|
|
||||||
!email.includes(".") ||
|
|
||||||
!/[a-z]/.test(email)
|
|
||||||
) {
|
|
||||||
setEmailError(true);
|
setEmailError(true);
|
||||||
setEmailErrorMessage("Please enter a valid email.");
|
setEmailErrorMessage('Please enter a valid email.');
|
||||||
emailCheckBool = true;
|
emailCheckBool = true;
|
||||||
} else {
|
} else {
|
||||||
setEmailError(false);
|
setEmailError(false);
|
||||||
@@ -57,13 +55,13 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
<div className="bg-bunker w-full max-w-md h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||||
{t("signup:step1-start")}
|
{t('signup:step1-start')}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
|
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
|
||||||
<InputField
|
<InputField
|
||||||
label={t("common:email") ?? ""}
|
label={t('common:email') ?? ''}
|
||||||
onChangeHandler={setEmail}
|
onChangeHandler={setEmail}
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
value={email}
|
||||||
@@ -75,11 +73,14 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
|
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
|
||||||
<p className="text-gray-400 mt-2 md:mx-0.5">
|
<p className="text-gray-400 mt-2 md:mx-0.5">{t('signup:step1-privacy')}</p>
|
||||||
{t("signup:step1-privacy")}
|
|
||||||
</p>
|
|
||||||
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
||||||
<Button text={t("signup:step1-submit") ?? ""} type="submit" onButtonPressed={emailCheck} size="lg" />
|
<Button
|
||||||
|
text={t('signup:step1-submit') ?? ''}
|
||||||
|
type="submit"
|
||||||
|
onButtonPressed={emailCheck}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,7 +88,7 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
|||||||
<Link href="/login">
|
<Link href="/login">
|
||||||
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
|
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
|
||||||
<u className="font-normal text-sm text-primary-500">
|
<u className="font-normal text-sm text-primary-500">
|
||||||
{t("signup:already-have-account")}
|
{t('signup:already-have-account')}
|
||||||
</u>
|
</u>
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from 'react';
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
|
||||||
import addUserToOrg from "~/pages/api/organization/addUserToOrg";
|
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
|
||||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
|
||||||
|
|
||||||
import Button from "../basic/buttons/Button";
|
|
||||||
|
|
||||||
|
import Button from '../basic/buttons/Button';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the last step of the signup flow. People can optionally invite their teammates here.
|
* This is the last step of the signup flow. People can optionally invite their teammates here.
|
||||||
*/
|
*/
|
||||||
export default function TeamInviteStep(): JSX.Element {
|
export default function TeamInviteStep(): JSX.Element {
|
||||||
const [emails, setEmails] = useState("");
|
const [emails, setEmails] = useState('');
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -20,34 +18,31 @@ export default function TeamInviteStep(): JSX.Element {
|
|||||||
const redirectToHome = async () => {
|
const redirectToHome = async () => {
|
||||||
const userWorkspaces = await getWorkspaces();
|
const userWorkspaces = await getWorkspaces();
|
||||||
const userWorkspace = userWorkspaces[0]._id;
|
const userWorkspace = userWorkspaces[0]._id;
|
||||||
router.push("/home/" + userWorkspace);
|
router.push(`/home/${userWorkspace}`);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||||
|
inviteEmails
|
||||||
const inviteUsers = async ({ emails }: { emails: string; }) => {
|
|
||||||
emails
|
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(email => email.trim())
|
.map((email) => email.trim())
|
||||||
.map(async (email) => await addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
|
.map(async (email) => addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
|
||||||
|
|
||||||
await redirectToHome();
|
await redirectToHome();
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32">
|
<div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32">
|
||||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||||
{t("signup:step5-invite-team")}
|
{t('signup:step5-invite-team')}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4">
|
<p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4">
|
||||||
{t("signup:step5-subtitle")}
|
{t('signup:step5-subtitle')}
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<div className="overflow-auto bg-bunker-800">
|
<div className="overflow-auto bg-bunker-800">
|
||||||
<div className="whitespace-pre-wrap break-words bg-transparent">
|
<div className="whitespace-pre-wrap break-words bg-transparent" />
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||||
value={emails}
|
value={emails}
|
||||||
onChange={(e) => setEmails(e.target.value)}
|
onChange={(e) => setEmails(e.target.value)}
|
||||||
@@ -55,15 +50,18 @@ export default function TeamInviteStep(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||||
<div
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200"
|
className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200"
|
||||||
onClick={redirectToHome}
|
onClick={redirectToHome}
|
||||||
>
|
>
|
||||||
{t("signup:step5-skip")}
|
{t('signup:step5-skip')}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
text={t("signup:step5-send-invites") ?? ""}
|
text={t('signup:step5-send-invites') ?? ''}
|
||||||
onButtonPressed={() => inviteUsers({ emails})}
|
onButtonPressed={() => inviteUsers({ emails })}
|
||||||
size="lg"
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
||||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import jsrp from 'jsrp';
|
||||||
import completeAccountInformationSignup from '~/pages/api/auth/CompleteAccountInformationSignup';
|
import nacl from 'tweetnacl';
|
||||||
|
import { encodeBase64 } from 'tweetnacl-util';
|
||||||
|
|
||||||
import Button from '../basic/buttons/Button';
|
import Button from '../basic/buttons/Button';
|
||||||
import InputField from '../basic/InputField';
|
import InputField from '../basic/InputField';
|
||||||
@@ -12,9 +14,7 @@ import attemptLogin from '../utilities/attemptLogin';
|
|||||||
import passwordCheck from '../utilities/checks/PasswordCheck';
|
import passwordCheck from '../utilities/checks/PasswordCheck';
|
||||||
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
||||||
|
|
||||||
const nacl = require('tweetnacl');
|
// eslint-disable-next-line new-cap
|
||||||
const jsrp = require('jsrp');
|
|
||||||
nacl.util = require('tweetnacl-util');
|
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
interface UserInfoStepProps {
|
interface UserInfoStepProps {
|
||||||
@@ -51,7 +51,7 @@ export default function UserInfoStep({
|
|||||||
firstName,
|
firstName,
|
||||||
setFirstName,
|
setFirstName,
|
||||||
lastName,
|
lastName,
|
||||||
setLastName,
|
setLastName
|
||||||
}: UserInfoStepProps): JSX.Element {
|
}: UserInfoStepProps): JSX.Element {
|
||||||
const [firstNameError, setFirstNameError] = useState(false);
|
const [firstNameError, setFirstNameError] = useState(false);
|
||||||
const [lastNameError, setLastNameError] = useState(false);
|
const [lastNameError, setLastNameError] = useState(false);
|
||||||
@@ -85,7 +85,7 @@ export default function UserInfoStep({
|
|||||||
setPasswordErrorLength,
|
setPasswordErrorLength,
|
||||||
setPasswordErrorNumber,
|
setPasswordErrorNumber,
|
||||||
setPasswordErrorLowerCase,
|
setPasswordErrorLowerCase,
|
||||||
errorCheck,
|
errorCheck
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!errorCheck) {
|
if (!errorCheck) {
|
||||||
@@ -93,17 +93,14 @@ export default function UserInfoStep({
|
|||||||
const pair = nacl.box.keyPair();
|
const pair = nacl.box.keyPair();
|
||||||
const secretKeyUint8Array = pair.secretKey;
|
const secretKeyUint8Array = pair.secretKey;
|
||||||
const publicKeyUint8Array = pair.publicKey;
|
const publicKeyUint8Array = pair.publicKey;
|
||||||
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
|
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
|
||||||
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
|
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
|
||||||
|
|
||||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||||
text: PRIVATE_KEY,
|
text: PRIVATE_KEY,
|
||||||
secret: password
|
secret: password
|
||||||
.slice(0, 32)
|
.slice(0, 32)
|
||||||
.padStart(
|
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
|
||||||
'0'
|
|
||||||
),
|
|
||||||
}) as { ciphertext: string; iv: string; tag: string };
|
}) as { ciphertext: string; iv: string; tag: string };
|
||||||
|
|
||||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
||||||
@@ -111,50 +108,41 @@ export default function UserInfoStep({
|
|||||||
client.init(
|
client.init(
|
||||||
{
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password: password,
|
password
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
client.createVerifier(
|
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||||
async (err: any, result: { salt: string; verifier: string }) => {
|
const response = await completeAccountInformationSignup({
|
||||||
const response = await completeAccountInformationSignup({
|
email,
|
||||||
email,
|
firstName,
|
||||||
firstName,
|
lastName,
|
||||||
lastName,
|
organizationName: `${firstName}'s organization`,
|
||||||
organizationName: firstName + "'s organization",
|
publicKey: PUBLIC_KEY,
|
||||||
publicKey: PUBLIC_KEY,
|
ciphertext,
|
||||||
ciphertext,
|
iv,
|
||||||
iv,
|
tag,
|
||||||
tag,
|
salt: result.salt,
|
||||||
salt: result.salt,
|
verifier: result.verifier,
|
||||||
verifier: result.verifier,
|
token: verificationToken
|
||||||
token: verificationToken,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
// if everything works, go the main dashboard page.
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
// response = await response.json();
|
// response = await response.json();
|
||||||
|
|
||||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
localStorage.setItem('publicKey', PUBLIC_KEY);
|
||||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||||
localStorage.setItem('iv', iv);
|
localStorage.setItem('iv', iv);
|
||||||
localStorage.setItem('tag', tag);
|
localStorage.setItem('tag', tag);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await attemptLogin(
|
await attemptLogin(email, password, () => {}, router, true, false);
|
||||||
email,
|
incrementStep();
|
||||||
password,
|
} catch (error) {
|
||||||
(value: boolean) => {},
|
setIsLoading(false);
|
||||||
router,
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
incrementStep();
|
|
||||||
} catch (error) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -163,142 +151,108 @@ export default function UserInfoStep({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16'>
|
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
|
||||||
<p className='text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary'>
|
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||||
{t('signup:step3-message')}
|
{t('signup:step3-message')}
|
||||||
</p>
|
</p>
|
||||||
<div className='relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24'>
|
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
|
||||||
<InputField
|
<InputField
|
||||||
label={t('common:first-name')}
|
label={t('common:first-name')}
|
||||||
onChangeHandler={setFirstName}
|
onChangeHandler={setFirstName}
|
||||||
type='name'
|
type="name"
|
||||||
value={firstName}
|
value={firstName}
|
||||||
isRequired
|
isRequired
|
||||||
errorText={
|
errorText={
|
||||||
t('common:validate-required', {
|
t('common:validate-required', {
|
||||||
name: t('common:first-name'),
|
name: t('common:first-name')
|
||||||
}) as string
|
}) as string
|
||||||
}
|
}
|
||||||
error={firstNameError}
|
error={firstNameError}
|
||||||
autoComplete='given-name'
|
autoComplete="given-name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24'>
|
<div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
|
||||||
<InputField
|
<InputField
|
||||||
label={t('common:last-name')}
|
label={t('common:last-name')}
|
||||||
onChangeHandler={setLastName}
|
onChangeHandler={setLastName}
|
||||||
type='name'
|
type="name"
|
||||||
value={lastName}
|
value={lastName}
|
||||||
isRequired
|
isRequired
|
||||||
errorText={
|
errorText={
|
||||||
t('common:validate-required', {
|
t('common:validate-required', {
|
||||||
name: t('common:last-name'),
|
name: t('common:last-name')
|
||||||
}) as string
|
}) as string
|
||||||
}
|
}
|
||||||
error={lastNameError}
|
error={lastNameError}
|
||||||
autoComplete='family-name'
|
autoComplete="family-name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60'>
|
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
|
||||||
<InputField
|
<InputField
|
||||||
label={t('section-password:password')}
|
label={t('section-password:password')}
|
||||||
onChangeHandler={(password: string) => {
|
onChangeHandler={(pass: string) => {
|
||||||
setPassword(password);
|
setPassword(pass);
|
||||||
passwordCheck({
|
passwordCheck({
|
||||||
password,
|
password: pass,
|
||||||
setPasswordErrorLength,
|
setPasswordErrorLength,
|
||||||
setPasswordErrorNumber,
|
setPasswordErrorNumber,
|
||||||
setPasswordErrorLowerCase,
|
setPasswordErrorLowerCase,
|
||||||
errorCheck: false,
|
errorCheck: false
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
type='password'
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
isRequired
|
isRequired
|
||||||
error={
|
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
|
||||||
passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase
|
autoComplete="new-password"
|
||||||
}
|
id="new-password"
|
||||||
autoComplete='new-password'
|
|
||||||
id='new-password'
|
|
||||||
/>
|
/>
|
||||||
{passwordErrorLength ||
|
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
|
||||||
passwordErrorLowerCase ||
|
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
|
||||||
passwordErrorNumber ? (
|
<div className="text-gray-400 text-sm mb-1">{t('section-password:validate-base')}</div>
|
||||||
<div className='w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md'>
|
<div className="flex flex-row justify-start items-center ml-1">
|
||||||
<div className={`text-gray-400 text-sm mb-1`}>
|
|
||||||
{t('section-password:validate-base')}
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-row justify-start items-center ml-1'>
|
|
||||||
{passwordErrorLength ? (
|
{passwordErrorLength ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||||
icon={faX}
|
|
||||||
className='text-md text-red mr-2.5'
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||||
icon={faCheck}
|
|
||||||
className='text-md text-primary mr-2'
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div
|
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
||||||
className={`${
|
|
||||||
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
|
|
||||||
} text-sm`}
|
|
||||||
>
|
|
||||||
{t('section-password:validate-length')}
|
{t('section-password:validate-length')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row justify-start items-center ml-1'>
|
<div className="flex flex-row justify-start items-center ml-1">
|
||||||
{passwordErrorLowerCase ? (
|
{passwordErrorLowerCase ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||||
icon={faX}
|
|
||||||
className='text-md text-red mr-2.5'
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||||
icon={faCheck}
|
|
||||||
className='text-md text-primary mr-2'
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
|
||||||
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
|
|
||||||
} text-sm`}
|
|
||||||
>
|
>
|
||||||
{t('section-password:validate-case')}
|
{t('section-password:validate-case')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row justify-start items-center ml-1'>
|
<div className="flex flex-row justify-start items-center ml-1">
|
||||||
{passwordErrorNumber ? (
|
{passwordErrorNumber ? (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||||
icon={faX}
|
|
||||||
className='text-md text-red mr-2.5'
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||||
icon={faCheck}
|
|
||||||
className='text-md text-primary mr-2'
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<div
|
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
||||||
className={`${
|
|
||||||
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
|
|
||||||
} text-sm`}
|
|
||||||
>
|
|
||||||
{t('section-password:validate-number')}
|
{t('section-password:validate-number')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='py-2'></div>
|
<div className="py-2" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3'>
|
<div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3">
|
||||||
<Button
|
<Button
|
||||||
text={t('signup:signup') ?? ''}
|
text={t('signup:signup') ?? ''}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
onButtonPressed={signupErrorCheck}
|
onButtonPressed={signupErrorCheck}
|
||||||
size='lg'
|
size="lg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,32 +1,27 @@
|
|||||||
import token from '~/pages/api/auth/Token';
|
import token from '@app/pages/api/auth/Token';
|
||||||
|
|
||||||
export default class SecurityClient {
|
export default class SecurityClient {
|
||||||
static #token = '';
|
static #token = '';
|
||||||
|
|
||||||
constructor() {}
|
static setToken(tokenStr: string) {
|
||||||
|
this.#token = tokenStr;
|
||||||
static setToken(token: string) {
|
|
||||||
this.#token = token;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fetchCall(
|
static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
|
||||||
resource: RequestInfo,
|
|
||||||
options?: RequestInit | undefined
|
|
||||||
) {
|
|
||||||
const req = new Request(resource, options);
|
const req = new Request(resource, options);
|
||||||
|
|
||||||
if (this.#token == '') {
|
if (this.#token === '') {
|
||||||
try {
|
try {
|
||||||
// TODO: This should be moved to a context to do it only once when app loads
|
// TODO: This should be moved to a context to do it only once when app loads
|
||||||
// this try catch saves route guard from a stuck state
|
// this try catch saves route guard from a stuck state
|
||||||
this.setToken(await token());
|
this.setToken(await token());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unauthorized access");
|
console.error('Unauthorized access');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#token) {
|
if (this.#token) {
|
||||||
req.headers.set('Authorization', 'Bearer ' + this.#token);
|
req.headers.set('Authorization', `Bearer ${this.#token}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetch(req);
|
return fetch(req);
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
/* eslint-disable prefer-destructuring */
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
|
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||||
import login1 from '~/pages/api/auth/Login1';
|
import login1 from '@app/pages/api/auth/Login1';
|
||||||
import login2 from '~/pages/api/auth/Login2';
|
import login2 from '@app/pages/api/auth/Login2';
|
||||||
import addSecrets from '~/pages/api/files/AddSecrets';
|
import addSecrets from '@app/pages/api/files/AddSecrets';
|
||||||
import getOrganizations from '~/pages/api/organization/getOrgs';
|
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||||
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
|
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||||
import getUser from '~/pages/api/user/getUser';
|
import getUser from '@app/pages/api/user/getUser';
|
||||||
import uploadKeys from '~/pages/api/workspace/uploadKeys';
|
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||||
|
import jsrp from 'jsrp';
|
||||||
|
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||||
|
|
||||||
import { encryptAssymmetric } from './cryptography/crypto';
|
import { encryptAssymmetric } from './cryptography/crypto';
|
||||||
import encryptSecrets from './secrets/encryptSecrets';
|
import encryptSecrets from './secrets/encryptSecrets';
|
||||||
@@ -15,11 +18,7 @@ import Telemetry from './telemetry/Telemetry';
|
|||||||
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||||
import SecurityClient from './SecurityClient';
|
import SecurityClient from './SecurityClient';
|
||||||
|
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
const crypto = require("crypto");
|
|
||||||
const nacl = require('tweetnacl');
|
|
||||||
nacl.util = require('tweetnacl-util');
|
|
||||||
const jsrp = require('jsrp');
|
|
||||||
const client = new jsrp.client();
|
const client = new jsrp.client();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +45,7 @@ const attemptLogin = async (
|
|||||||
client.init(
|
client.init(
|
||||||
{
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password: password
|
password
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const clientPublicKey = client.getPublicKey();
|
const clientPublicKey = client.getPublicKey();
|
||||||
@@ -59,8 +58,10 @@ const attemptLogin = async (
|
|||||||
const clientProof = client.getProof(); // called M1
|
const clientProof = client.getProof(); // called M1
|
||||||
|
|
||||||
// if everything works, go the main dashboard page.
|
// if everything works, go the main dashboard page.
|
||||||
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(
|
||||||
await login2(email, clientProof);
|
email,
|
||||||
|
clientProof
|
||||||
|
);
|
||||||
|
|
||||||
SecurityClient.setToken(token);
|
SecurityClient.setToken(token);
|
||||||
|
|
||||||
@@ -70,10 +71,7 @@ const attemptLogin = async (
|
|||||||
tag,
|
tag,
|
||||||
secret: password
|
secret: password
|
||||||
.slice(0, 32)
|
.slice(0, 32)
|
||||||
.padStart(
|
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
|
||||||
'0'
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
saveTokenToLocalStorage({
|
saveTokenToLocalStorage({
|
||||||
@@ -83,9 +81,9 @@ const attemptLogin = async (
|
|||||||
tag,
|
tag,
|
||||||
privateKey
|
privateKey
|
||||||
});
|
});
|
||||||
|
|
||||||
const userOrgs = await getOrganizations();
|
const userOrgs = await getOrganizations();
|
||||||
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
|
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
|
||||||
|
|
||||||
let orgToLogin;
|
let orgToLogin;
|
||||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||||
@@ -99,11 +97,9 @@ const attemptLogin = async (
|
|||||||
orgId: orgToLogin
|
orgId: orgToLogin
|
||||||
});
|
});
|
||||||
|
|
||||||
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
|
orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
|
||||||
let projectToLogin;
|
let projectToLogin;
|
||||||
if (
|
if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
|
||||||
orgUserProjects.includes(localStorage.getItem('projectData.id'))
|
|
||||||
) {
|
|
||||||
projectToLogin = localStorage.getItem('projectData.id');
|
projectToLogin = localStorage.getItem('projectData.id');
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
@@ -113,90 +109,104 @@ const attemptLogin = async (
|
|||||||
console.log('ERROR: User likely has no projects. ', error);
|
console.log('ERROR: User likely has no projects. ', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
telemetry.identify(email);
|
telemetry.identify(email);
|
||||||
telemetry.capture('User Logged In');
|
telemetry.capture('User Logged In');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSignUp) {
|
if (isSignUp) {
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
|
||||||
|
|
||||||
const myUser = await getUser();
|
const myUser = await getUser();
|
||||||
|
|
||||||
const { ciphertext, nonce } = encryptAssymmetric({
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
plaintext: randomBytes,
|
plaintext: randomBytes,
|
||||||
publicKey: myUser.publicKey,
|
publicKey: myUser.publicKey,
|
||||||
privateKey: PRIVATE_KEY,
|
privateKey: PRIVATE_KEY
|
||||||
}) as { ciphertext: string; nonce: string };
|
}) as { ciphertext: string; nonce: string };
|
||||||
|
|
||||||
await uploadKeys(
|
await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
|
||||||
projectToLogin,
|
|
||||||
myUser._id,
|
|
||||||
ciphertext,
|
|
||||||
nonce
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretsToBeAdded: SecretDataProps[] = [{
|
const secretsToBeAdded: SecretDataProps[] = [
|
||||||
pos: 0,
|
{
|
||||||
key: "DATABASE_URL",
|
pos: 0,
|
||||||
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
|
key: 'DATABASE_URL',
|
||||||
valueOverride: undefined,
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
comment: "This is an example of secret referencing.",
|
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
||||||
id: ''
|
valueOverride: undefined,
|
||||||
}, {
|
comment: 'This is an example of secret referencing.',
|
||||||
pos: 1,
|
id: ''
|
||||||
key: "DB_USERNAME",
|
},
|
||||||
value: "OVERRIDE_THIS",
|
{
|
||||||
valueOverride: undefined,
|
pos: 1,
|
||||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
key: 'DB_USERNAME',
|
||||||
id: ''
|
value: 'OVERRIDE_THIS',
|
||||||
}, {
|
valueOverride: undefined,
|
||||||
pos: 2,
|
comment:
|
||||||
key: "DB_PASSWORD",
|
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
|
||||||
value: "OVERRIDE_THIS",
|
id: ''
|
||||||
valueOverride: undefined,
|
},
|
||||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
{
|
||||||
id: ''
|
pos: 2,
|
||||||
}, {
|
key: 'DB_PASSWORD',
|
||||||
pos: 3,
|
value: 'OVERRIDE_THIS',
|
||||||
key: "DB_USERNAME",
|
valueOverride: undefined,
|
||||||
value: "user1234",
|
comment:
|
||||||
valueOverride: "user1234",
|
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
|
||||||
comment: "",
|
id: ''
|
||||||
id: ''
|
},
|
||||||
}, {
|
{
|
||||||
pos: 4,
|
pos: 3,
|
||||||
key: "DB_PASSWORD",
|
key: 'DB_USERNAME',
|
||||||
value: "example_password",
|
value: 'user1234',
|
||||||
valueOverride: "example_password",
|
valueOverride: 'user1234',
|
||||||
comment: "",
|
comment: '',
|
||||||
id: ''
|
id: ''
|
||||||
}, {
|
},
|
||||||
pos: 5,
|
{
|
||||||
key: "TWILIO_AUTH_TOKEN",
|
pos: 4,
|
||||||
value: "example_twillio_token",
|
key: 'DB_PASSWORD',
|
||||||
valueOverride: undefined,
|
value: 'example_password',
|
||||||
comment: "",
|
valueOverride: 'example_password',
|
||||||
id: ''
|
comment: '',
|
||||||
}, {
|
id: ''
|
||||||
pos: 6,
|
},
|
||||||
key: "WEBSITE_URL",
|
{
|
||||||
value: "http://localhost:3000",
|
pos: 5,
|
||||||
valueOverride: undefined,
|
key: 'TWILIO_AUTH_TOKEN',
|
||||||
comment: "",
|
value: 'example_twillio_token',
|
||||||
id: ''
|
valueOverride: undefined,
|
||||||
}]
|
comment: '',
|
||||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId: String(localStorage.getItem('projectData.id')), env: 'dev' })
|
id: ''
|
||||||
await addSecrets({ secrets: secrets ?? [], env: "dev", workspaceId: String(localStorage.getItem('projectData.id')) });
|
},
|
||||||
|
{
|
||||||
|
pos: 6,
|
||||||
|
key: 'WEBSITE_URL',
|
||||||
|
value: 'http://localhost:3000',
|
||||||
|
valueOverride: undefined,
|
||||||
|
comment: '',
|
||||||
|
id: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const secrets = await encryptSecrets({
|
||||||
|
secretsToEncrypt: secretsToBeAdded,
|
||||||
|
workspaceId: String(localStorage.getItem('projectData.id')),
|
||||||
|
env: 'dev'
|
||||||
|
});
|
||||||
|
await addSecrets({
|
||||||
|
secrets: secrets ?? [],
|
||||||
|
env: 'dev',
|
||||||
|
workspaceId: String(localStorage.getItem('projectData.id'))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLogin) {
|
if (isLogin) {
|
||||||
router.push('/dashboard/' + localStorage.getItem('projectData.id'));
|
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error);
|
||||||
setErrorLogin(true);
|
setErrorLogin(true);
|
||||||
console.log('Login response not available');
|
console.log('Login response not available');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers';
|
import getOrganizationUsers from '@app/pages/api/organization/GetOrgUsers';
|
||||||
import checkUserAction from '~/pages/api/userActions/checkUserAction';
|
import checkUserAction from '@app/pages/api/userActions/checkUserAction';
|
||||||
|
|
||||||
interface OnboardingCheckProps {
|
interface OnboardingCheckProps {
|
||||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||||
@@ -26,46 +26,43 @@ const onboardingCheck = async ({
|
|||||||
action: 'slack_cta_clicked'
|
action: 'slack_cta_clicked'
|
||||||
});
|
});
|
||||||
if (userActionSlack) {
|
if (userActionSlack) {
|
||||||
countActions = countActions + 1;
|
countActions += 1;
|
||||||
}
|
}
|
||||||
setHasUserClickedSlack &&
|
if (setHasUserClickedSlack) setHasUserClickedSlack(!!userActionSlack);
|
||||||
setHasUserClickedSlack(userActionSlack ? true : false);
|
|
||||||
|
|
||||||
const userActionSecrets = await checkUserAction({
|
const userActionSecrets = await checkUserAction({
|
||||||
action: 'first_time_secrets_pushed'
|
action: 'first_time_secrets_pushed'
|
||||||
});
|
});
|
||||||
if (userActionSecrets) {
|
if (userActionSecrets) {
|
||||||
countActions = countActions + 1;
|
countActions += 1;
|
||||||
}
|
}
|
||||||
setHasUserPushedSecrets &&
|
if (setHasUserPushedSecrets) setHasUserPushedSecrets(!!userActionSecrets);
|
||||||
setHasUserPushedSecrets(userActionSecrets ? true : false);
|
|
||||||
|
|
||||||
const userActionIntro = await checkUserAction({
|
const userActionIntro = await checkUserAction({
|
||||||
action: 'intro_cta_clicked'
|
action: 'intro_cta_clicked'
|
||||||
});
|
});
|
||||||
if (userActionIntro) {
|
if (userActionIntro) {
|
||||||
countActions = countActions + 1;
|
countActions += 1;
|
||||||
}
|
}
|
||||||
setHasUserClickedIntro &&
|
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||||
setHasUserClickedIntro(userActionIntro ? true : false);
|
|
||||||
|
|
||||||
const userActionStar = await checkUserAction({
|
const userActionStar = await checkUserAction({
|
||||||
action: 'star_cta_clicked'
|
action: 'star_cta_clicked'
|
||||||
});
|
});
|
||||||
if (userActionStar) {
|
if (userActionStar) {
|
||||||
countActions = countActions + 1;
|
countActions += 1;
|
||||||
}
|
}
|
||||||
setHasUserStarred && setHasUserStarred(userActionStar ? true : false);
|
if (setHasUserStarred) setHasUserStarred(!!userActionStar);
|
||||||
|
|
||||||
const orgId = localStorage.getItem('orgData.id');
|
const orgId = localStorage.getItem('orgData.id');
|
||||||
const orgUsers = await getOrganizationUsers({
|
const orgUsers = await getOrganizationUsers({
|
||||||
orgId: orgId ? orgId : ''
|
orgId: orgId || ''
|
||||||
});
|
});
|
||||||
if (orgUsers.length > 1) {
|
if (orgUsers.length > 1) {
|
||||||
countActions = countActions + 1;
|
countActions += 1;
|
||||||
}
|
}
|
||||||
setUsersInOrg && setUsersInOrg(orgUsers.length > 1);
|
if (setUsersInOrg) setUsersInOrg(orgUsers.length > 1);
|
||||||
setTotalOnboardingActionsDone && setTotalOnboardingActionsDone(countActions);
|
if (setTotalOnboardingActionsDone) setTotalOnboardingActionsDone(countActions);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default onboardingCheck;
|
export default onboardingCheck;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-param-reassign */
|
||||||
interface PasswordCheckProps {
|
interface PasswordCheckProps {
|
||||||
password: string;
|
password: string;
|
||||||
errorCheck: boolean;
|
errorCheck: boolean;
|
||||||
@@ -14,7 +15,7 @@ const passwordCheck = ({
|
|||||||
setPasswordErrorLength,
|
setPasswordErrorLength,
|
||||||
setPasswordErrorNumber,
|
setPasswordErrorNumber,
|
||||||
setPasswordErrorLowerCase,
|
setPasswordErrorLowerCase,
|
||||||
errorCheck,
|
errorCheck
|
||||||
}: PasswordCheckProps) => {
|
}: PasswordCheckProps) => {
|
||||||
if (!password || password.length < 14) {
|
if (!password || password.length < 14) {
|
||||||
setPasswordErrorLength(true);
|
setPasswordErrorLength(true);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class Aes256Gcm {
|
|||||||
/**
|
/**
|
||||||
* No need to run the constructor. The class only has static methods.
|
* No need to run the constructor. The class only has static methods.
|
||||||
*/
|
*/
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypts text with AES 256 GCM.
|
* Encrypts text with AES 256 GCM.
|
||||||
@@ -65,11 +64,7 @@ class Aes256Gcm {
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
|
static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
|
||||||
const decipher = crypto.createDecipheriv(
|
const decipher = crypto.createDecipheriv(ALGORITHM, secret, Buffer.from(iv, 'base64'));
|
||||||
ALGORITHM,
|
|
||||||
secret,
|
|
||||||
Buffer.from(iv, 'base64')
|
|
||||||
);
|
|
||||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||||
|
|
||||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
/* eslint-disable new-cap */
|
||||||
|
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
|
||||||
|
import SRP1 from '@app/pages/api/auth/SRP1';
|
||||||
import jsrp from 'jsrp';
|
import jsrp from 'jsrp';
|
||||||
|
|
||||||
import changePassword2 from '~/pages/api/auth/ChangePassword2';
|
|
||||||
import SRP1 from '~/pages/api/auth/SRP1';
|
|
||||||
|
|
||||||
import Aes256Gcm from './aes-256-gcm';
|
import Aes256Gcm from './aes-256-gcm';
|
||||||
|
|
||||||
const clientOldPassword = new jsrp.client();
|
const clientOldPassword = new jsrp.client();
|
||||||
@@ -33,15 +33,16 @@ const changePassword = async (
|
|||||||
clientOldPassword.init(
|
clientOldPassword.init(
|
||||||
{
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password: currentPassword,
|
password: currentPassword
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||||
|
|
||||||
let serverPublicKey, salt;
|
let serverPublicKey;
|
||||||
|
let salt;
|
||||||
try {
|
try {
|
||||||
const res = await SRP1({
|
const res = await SRP1({
|
||||||
clientPublicKey: clientPublicKey,
|
clientPublicKey
|
||||||
});
|
});
|
||||||
serverPublicKey = res.serverPublicKey;
|
serverPublicKey = res.serverPublicKey;
|
||||||
salt = res.salt;
|
salt = res.salt;
|
||||||
@@ -57,7 +58,7 @@ const changePassword = async (
|
|||||||
clientNewPassword.init(
|
clientNewPassword.init(
|
||||||
{
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password: newPassword,
|
password: newPassword
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
clientNewPassword.createVerifier(async (err, result) => {
|
clientNewPassword.createVerifier(async (err, result) => {
|
||||||
@@ -67,11 +68,9 @@ const changePassword = async (
|
|||||||
secret: newPassword
|
secret: newPassword
|
||||||
.slice(0, 32)
|
.slice(0, 32)
|
||||||
.padStart(
|
.padStart(
|
||||||
32 +
|
32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
|
||||||
(newPassword.slice(0, 32).length -
|
|
||||||
new Blob([newPassword]).size),
|
|
||||||
'0'
|
'0'
|
||||||
),
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ciphertext) {
|
if (ciphertext) {
|
||||||
@@ -87,18 +86,18 @@ const changePassword = async (
|
|||||||
tag,
|
tag,
|
||||||
salt: result.salt,
|
salt: result.salt,
|
||||||
verifier: result.verifier,
|
verifier: result.verifier,
|
||||||
clientProof,
|
clientProof
|
||||||
});
|
});
|
||||||
if (res && res.status == 400) {
|
if (res && res.status === 400) {
|
||||||
setCurrentPasswordError(true);
|
setCurrentPasswordError(true);
|
||||||
} else if (res && res.status == 200) {
|
} else if (res && res.status === 200) {
|
||||||
setPasswordChanged(true);
|
setPasswordChanged(true);
|
||||||
setCurrentPassword('');
|
setCurrentPassword('');
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
setCurrentPasswordError(true);
|
setCurrentPasswordError(true);
|
||||||
console.log(err);
|
console.log(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
const nacl = require('tweetnacl');
|
|
||||||
nacl.util = require('tweetnacl-util');
|
|
||||||
import aes from './aes-256-gcm';
|
import aes from './aes-256-gcm';
|
||||||
|
|
||||||
type encryptAsymmetricProps = {
|
const nacl = require('tweetnacl');
|
||||||
|
nacl.util = require('tweetnacl-util');
|
||||||
|
|
||||||
|
type EncryptAsymmetricProps = {
|
||||||
plaintext: string;
|
plaintext: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: string;
|
privateKey: string;
|
||||||
@@ -22,8 +23,8 @@ type encryptAsymmetricProps = {
|
|||||||
const encryptAssymmetric = ({
|
const encryptAssymmetric = ({
|
||||||
plaintext,
|
plaintext,
|
||||||
publicKey,
|
publicKey,
|
||||||
privateKey,
|
privateKey
|
||||||
}: encryptAsymmetricProps): {
|
}: EncryptAsymmetricProps): {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
} => {
|
} => {
|
||||||
@@ -37,11 +38,11 @@ const encryptAssymmetric = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ciphertext: nacl.util.encodeBase64(ciphertext),
|
ciphertext: nacl.util.encodeBase64(ciphertext),
|
||||||
nonce: nacl.util.encodeBase64(nonce),
|
nonce: nacl.util.encodeBase64(nonce)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type decryptAsymmetricProps = {
|
type DecryptAsymmetricProps = {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
@@ -61,8 +62,8 @@ const decryptAssymmetric = ({
|
|||||||
ciphertext,
|
ciphertext,
|
||||||
nonce,
|
nonce,
|
||||||
publicKey,
|
publicKey,
|
||||||
privateKey,
|
privateKey
|
||||||
}: decryptAsymmetricProps): string => {
|
}: DecryptAsymmetricProps): string => {
|
||||||
const plaintext = nacl.box.open(
|
const plaintext = nacl.box.open(
|
||||||
nacl.util.decodeBase64(ciphertext),
|
nacl.util.decodeBase64(ciphertext),
|
||||||
nacl.util.decodeBase64(nonce),
|
nacl.util.decodeBase64(nonce),
|
||||||
@@ -73,24 +74,23 @@ const decryptAssymmetric = ({
|
|||||||
return nacl.util.encodeUTF8(plaintext);
|
return nacl.util.encodeUTF8(plaintext);
|
||||||
};
|
};
|
||||||
|
|
||||||
type encryptSymmetricProps = {
|
type EncryptSymmetricProps = {
|
||||||
plaintext: string;
|
plaintext: string;
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type encryptSymmetricReturn = {
|
type EncryptSymmetricReturn = {
|
||||||
ciphertext:string;
|
ciphertext: string;
|
||||||
iv:string;
|
iv: string;
|
||||||
tag:string;
|
tag: string;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Return symmetrically encrypted [plaintext] using [key].
|
* Return symmetrically encrypted [plaintext] using [key].
|
||||||
*/
|
*/
|
||||||
const encryptSymmetric = ({
|
const encryptSymmetric = ({ plaintext, key }: EncryptSymmetricProps): EncryptSymmetricReturn => {
|
||||||
plaintext,
|
let ciphertext;
|
||||||
key,
|
let iv;
|
||||||
}: encryptSymmetricProps): encryptSymmetricReturn => {
|
let tag;
|
||||||
let ciphertext, iv, tag;
|
|
||||||
try {
|
try {
|
||||||
const obj = aes.encrypt({ text: plaintext, secret: key });
|
const obj = aes.encrypt({ text: plaintext, secret: key });
|
||||||
ciphertext = obj.ciphertext;
|
ciphertext = obj.ciphertext;
|
||||||
@@ -105,11 +105,11 @@ const encryptSymmetric = ({
|
|||||||
return {
|
return {
|
||||||
ciphertext,
|
ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type decryptSymmetricProps = {
|
type DecryptSymmetricProps = {
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
@@ -126,12 +126,7 @@ type decryptSymmetricProps = {
|
|||||||
* @param {String} obj.key - 32-byte hex key
|
* @param {String} obj.key - 32-byte hex key
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const decryptSymmetric = ({
|
const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
key,
|
|
||||||
}: decryptSymmetricProps): string => {
|
|
||||||
let plaintext;
|
let plaintext;
|
||||||
try {
|
try {
|
||||||
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
||||||
@@ -143,9 +138,4 @@ const decryptSymmetric = ({
|
|||||||
return plaintext;
|
return plaintext;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
|
||||||
decryptAssymmetric,
|
|
||||||
decryptSymmetric,
|
|
||||||
encryptAssymmetric,
|
|
||||||
encryptSymmetric,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey';
|
/* eslint-disable new-cap */
|
||||||
import SRP1 from '~/pages/api/auth/SRP1';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
import issueBackupPrivateKey from '@app/pages/api/auth/IssueBackupPrivateKey';
|
||||||
|
import SRP1 from '@app/pages/api/auth/SRP1';
|
||||||
|
import jsrp from 'jsrp';
|
||||||
|
|
||||||
import generateBackupPDF from '../generateBackupPDF';
|
import generateBackupPDF from '../generateBackupPDF';
|
||||||
import Aes256Gcm from './aes-256-gcm';
|
import Aes256Gcm from './aes-256-gcm';
|
||||||
|
|
||||||
const nacl = require('tweetnacl');
|
|
||||||
nacl.util = require('tweetnacl-util');
|
|
||||||
const jsrp = require('jsrp');
|
|
||||||
const clientPassword = new jsrp.client();
|
const clientPassword = new jsrp.client();
|
||||||
const clientKey = new jsrp.client();
|
const clientKey = new jsrp.client();
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
interface BackupKeyProps {
|
interface BackupKeyProps {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -42,15 +42,16 @@ const issueBackupKey = async ({
|
|||||||
clientPassword.init(
|
clientPassword.init(
|
||||||
{
|
{
|
||||||
username: email,
|
username: email,
|
||||||
password: password
|
password
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
const clientPublicKey = clientPassword.getPublicKey();
|
const clientPublicKey = clientPassword.getPublicKey();
|
||||||
|
|
||||||
let serverPublicKey, salt;
|
let serverPublicKey;
|
||||||
|
let salt;
|
||||||
try {
|
try {
|
||||||
const res = await SRP1({
|
const res = await SRP1({
|
||||||
clientPublicKey: clientPublicKey
|
clientPublicKey
|
||||||
});
|
});
|
||||||
serverPublicKey = res.serverPublicKey;
|
serverPublicKey = res.serverPublicKey;
|
||||||
salt = res.salt;
|
salt = res.salt;
|
||||||
@@ -87,9 +88,9 @@ const issueBackupKey = async ({
|
|||||||
clientProof
|
clientProof
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res?.status == 400) {
|
if (res?.status === 400) {
|
||||||
setBackupKeyError(true);
|
setBackupKeyError(true);
|
||||||
} else if (res?.status == 200) {
|
} else if (res?.status === 200) {
|
||||||
generateBackupPDF({
|
generateBackupPDF({
|
||||||
personalName,
|
personalName,
|
||||||
personalEmail: email,
|
personalEmail: email,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -29,6 +29,7 @@ export function parseDotEnv(src: ArrayBuffer) {
|
|||||||
let match;
|
let match;
|
||||||
let item: [string, string, string[]] | [] = [];
|
let item: [string, string, string[]] | [] = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-cond-assign
|
||||||
while ((match = LINE.exec(line)) !== null) {
|
while ((match = LINE.exec(line)) !== null) {
|
||||||
const key = match[1];
|
const key = match[1];
|
||||||
|
|
||||||
@@ -58,8 +59,8 @@ export function parseDotEnv(src: ArrayBuffer) {
|
|||||||
})
|
})
|
||||||
.filter((line) => line.length > 1)
|
.filter((line) => line.length > 1)
|
||||||
.forEach((line) => {
|
.forEach((line) => {
|
||||||
const [key, value, comments] = line;
|
const [key, value, cmnts] = line;
|
||||||
object[key as string] = { value, comments };
|
object[key as string] = { value, comments: cmnts };
|
||||||
});
|
});
|
||||||
|
|
||||||
return object;
|
return object;
|
||||||
|
|||||||
@@ -3,23 +3,11 @@
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const guidGenerator = () => {
|
const guidGenerator = () => {
|
||||||
const S4 = function () {
|
const S4 = () => {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||||
};
|
};
|
||||||
return (
|
return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
|
||||||
S4() +
|
|
||||||
S4() +
|
|
||||||
'-' +
|
|
||||||
S4() +
|
|
||||||
'-' +
|
|
||||||
S4() +
|
|
||||||
'-' +
|
|
||||||
S4() +
|
|
||||||
'-' +
|
|
||||||
S4() +
|
|
||||||
S4() +
|
|
||||||
S4()
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default guidGenerator;
|
export default guidGenerator;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const saveTokenToLocalStorage = ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Unable to send the tokens in local storage:" + err.message
|
`Unable to send the tokens in local storage:${ err.message}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function downloads the secrets as a .env file
|
* This function downloads the secrets as a .env file
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
|
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
|
const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => {
|
||||||
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
|
let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret));
|
||||||
const overridenSecrets = data!.filter(
|
const overridenSecrets = data!.filter((secret) =>
|
||||||
(secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal'
|
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||||
|
? 'shared'
|
||||||
|
: 'personal'
|
||||||
);
|
);
|
||||||
if (overridenSecrets.length) {
|
if (overridenSecrets.length) {
|
||||||
overridenSecrets.forEach((secret) => {
|
overridenSecrets.forEach((secret) => {
|
||||||
const index = secrets!.findIndex(
|
const index = secrets!.findIndex(
|
||||||
(_secret) => _secret.key === secret.key && (secret.valueOverride == undefined || secret?.value != secret?.valueOverride)
|
(_secret) =>
|
||||||
|
_secret.key === secret.key &&
|
||||||
|
(secret.valueOverride === undefined || secret?.value !== secret?.valueOverride)
|
||||||
);
|
);
|
||||||
secrets![index].value = secret.value;
|
secrets![index].value = secret.value;
|
||||||
});
|
});
|
||||||
secrets = secrets!.filter((secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride));
|
secrets = secrets!.filter(
|
||||||
|
(secret) => secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return secrets;
|
return secrets;
|
||||||
}
|
};
|
||||||
|
|
||||||
export default checkOverrides;
|
export default checkOverrides;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { envMapping } from "public/data/frequentConstants";
|
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
|
||||||
|
|
||||||
import checkOverrides from './checkOverrides';
|
import checkOverrides from './checkOverrides';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function downloads the secrets as a .env file
|
* This function downloads the secrets as a .env file
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||||
*/
|
*/
|
||||||
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
const secrets = await checkOverrides({ data });
|
const secrets = await checkOverrides({ data });
|
||||||
|
|
||||||
@@ -19,21 +17,21 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
|
|||||||
(item: SecretDataProps) =>
|
(item: SecretDataProps) =>
|
||||||
`${
|
`${
|
||||||
item.comment
|
item.comment
|
||||||
? item.comment
|
? `${item.comment
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map((comment) => '# '.concat(comment))
|
.map((comment) => '# '.concat(comment))
|
||||||
.join('\n') + '\n'
|
.join('\n')}\n`
|
||||||
: ''
|
: ''
|
||||||
}` + [item.key, item.value].join('=')
|
}${[item.key, item.value].join('=')}`
|
||||||
)
|
)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
const blob = new Blob([file]);
|
const blob = new Blob([file]);
|
||||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||||
const alink = document.createElement('a');
|
const alink = document.createElement('a');
|
||||||
alink.href = fileDownloadUrl;
|
alink.href = fileDownloadUrl;
|
||||||
alink.download = env + '.env';
|
alink.download = `${env}.env`;
|
||||||
alink.click();
|
alink.click();
|
||||||
}
|
};
|
||||||
|
|
||||||
export default downloadDotEnv;
|
export default downloadDotEnv;
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
// import YAML from 'yaml';
|
// import YAML from 'yaml';
|
||||||
// import { YAMLSeq } from 'yaml/types';
|
// import { YAMLSeq } from 'yaml/types';
|
||||||
|
|
||||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||||
|
|
||||||
// import { envMapping } from "../../../public/data/frequentConstants";
|
// import { envMapping } from "../../../public/data/frequentConstants";
|
||||||
// import checkOverrides from './checkOverrides';
|
// import checkOverrides from './checkOverrides';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function downloads the secrets as a .yml file
|
* This function downloads the secrets as a .yml file
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||||
* @param {string} obj.env - used for naming the file
|
* @param {string} obj.env - used for naming the file
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||||
// if (!data) return;
|
// if (!data) return;
|
||||||
// const doc = new YAML.Document();
|
// const doc = new YAML.Document();
|
||||||
// doc.contents = new YAMLSeq();
|
// doc.contents = new YAMLSeq();
|
||||||
@@ -27,20 +27,17 @@ const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: strin
|
|||||||
// .join('\n');
|
// .join('\n');
|
||||||
// doc.add(pair);
|
// doc.add(pair);
|
||||||
// });
|
// });
|
||||||
|
|
||||||
// const file = doc
|
// const file = doc
|
||||||
// .toString()
|
// .toString()
|
||||||
// .split('\n')
|
// .split('\n')
|
||||||
// .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
|
// .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
|
||||||
// .join('\n');
|
// .join('\n');
|
||||||
|
|
||||||
// const blob = new Blob([file]);
|
// const blob = new Blob([file]);
|
||||||
// const fileDownloadUrl = URL.createObjectURL(blob);
|
// const fileDownloadUrl = URL.createObjectURL(blob);
|
||||||
// const alink = document.createElement('a');
|
// const alink = document.createElement('a');
|
||||||
// alink.href = fileDownloadUrl;
|
// alink.href = fileDownloadUrl;
|
||||||
// alink.download = envMapping[env] + '.yml';
|
// alink.download = envMapping[env] + '.yml';
|
||||||
// alink.click();
|
// alink.click();
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default downloadYaml;
|
export default downloadYaml;
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
import crypto from 'crypto';
|
||||||
|
|
||||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
|
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||||
const crypto = require("crypto");
|
|
||||||
const {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptSymmetric,
|
|
||||||
} = require("../cryptography/crypto");
|
|
||||||
const nacl = require("tweetnacl");
|
|
||||||
nacl.util = require("tweetnacl-util");
|
|
||||||
|
|
||||||
|
import { decryptAssymmetric, encryptSymmetric } from '../cryptography/crypto';
|
||||||
|
|
||||||
interface EncryptedSecretProps {
|
interface EncryptedSecretProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -24,22 +18,30 @@ interface EncryptedSecretProps {
|
|||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
type: "personal" | "shared";
|
type: 'personal' | 'shared';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encypt secrets before pushing the to the DB
|
* Encypt secrets before pushing the to the DB
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
|
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
|
||||||
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
|
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsToEncrypt: SecretDataProps[]; workspaceId: string; env: string; }) => {
|
const encryptSecrets = async ({
|
||||||
|
secretsToEncrypt,
|
||||||
|
workspaceId,
|
||||||
|
env
|
||||||
|
}: {
|
||||||
|
secretsToEncrypt: SecretDataProps[];
|
||||||
|
workspaceId: string;
|
||||||
|
env: string;
|
||||||
|
}) => {
|
||||||
let secrets;
|
let secrets;
|
||||||
try {
|
try {
|
||||||
const sharedKey = await getLatestFileKey({ workspaceId });
|
const sharedKey = await getLatestFileKey({ workspaceId });
|
||||||
|
|
||||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
|
|
||||||
let randomBytes: string;
|
let randomBytes: string;
|
||||||
if (Object.keys(sharedKey).length > 0) {
|
if (Object.keys(sharedKey).length > 0) {
|
||||||
@@ -48,42 +50,42 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
|
|||||||
ciphertext: sharedKey.latestKey.encryptedKey,
|
ciphertext: sharedKey.latestKey.encryptedKey,
|
||||||
nonce: sharedKey.latestKey.nonce,
|
nonce: sharedKey.latestKey.nonce,
|
||||||
publicKey: sharedKey.latestKey.sender.publicKey,
|
publicKey: sharedKey.latestKey.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY,
|
privateKey: PRIVATE_KEY
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// case: a (shared) key does not exist for the workspace
|
// case: a (shared) key does not exist for the workspace
|
||||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
randomBytes = crypto.randomBytes(16).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets = secretsToEncrypt.map((secret) => {
|
secrets = secretsToEncrypt.map((secret) => {
|
||||||
// encrypt key
|
// encrypt key
|
||||||
const {
|
const {
|
||||||
ciphertext: secretKeyCiphertext,
|
ciphertext: secretKeyCiphertext,
|
||||||
iv: secretKeyIV,
|
iv: secretKeyIV,
|
||||||
tag: secretKeyTag,
|
tag: secretKeyTag
|
||||||
} = encryptSymmetric({
|
} = encryptSymmetric({
|
||||||
plaintext: secret.key,
|
plaintext: secret.key,
|
||||||
key: randomBytes,
|
key: randomBytes
|
||||||
});
|
});
|
||||||
|
|
||||||
// encrypt value
|
// encrypt value
|
||||||
const {
|
const {
|
||||||
ciphertext: secretValueCiphertext,
|
ciphertext: secretValueCiphertext,
|
||||||
iv: secretValueIV,
|
iv: secretValueIV,
|
||||||
tag: secretValueTag,
|
tag: secretValueTag
|
||||||
} = encryptSymmetric({
|
} = encryptSymmetric({
|
||||||
plaintext: secret.value,
|
plaintext: secret.value,
|
||||||
key: randomBytes,
|
key: randomBytes
|
||||||
});
|
});
|
||||||
|
|
||||||
// encrypt comment
|
// encrypt comment
|
||||||
const {
|
const {
|
||||||
ciphertext: secretCommentCiphertext,
|
ciphertext: secretCommentCiphertext,
|
||||||
iv: secretCommentIV,
|
iv: secretCommentIV,
|
||||||
tag: secretCommentTag,
|
tag: secretCommentTag
|
||||||
} = encryptSymmetric({
|
} = encryptSymmetric({
|
||||||
plaintext: secret.comment ?? '',
|
plaintext: secret.comment ?? '',
|
||||||
key: randomBytes,
|
key: randomBytes
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: EncryptedSecretProps = {
|
const result: EncryptedSecretProps = {
|
||||||
@@ -99,17 +101,19 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
|
|||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal',
|
type:
|
||||||
|
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||||
|
? 'shared'
|
||||||
|
: 'personal'
|
||||||
};
|
};
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error while encrypting secrets");
|
console.log('Error while encrypting secrets');
|
||||||
}
|
}
|
||||||
|
|
||||||
return secrets;
|
return secrets;
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default encryptSecrets;
|
export default encryptSecrets;
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
import getSecrets from '~/pages/api/files/GetSecrets';
|
import getSecrets from '@app/pages/api/files/GetSecrets';
|
||||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
|
|
||||||
const {
|
import { decryptAssymmetric, decryptSymmetric } from '../cryptography/crypto';
|
||||||
decryptAssymmetric,
|
|
||||||
decryptSymmetric
|
|
||||||
} = require('../cryptography/crypto');
|
|
||||||
const nacl = require('tweetnacl');
|
|
||||||
nacl.util = require('tweetnacl-util');
|
|
||||||
|
|
||||||
interface EncryptedSecretProps {
|
interface EncryptedSecretProps {
|
||||||
_id: string;
|
_id: string;
|
||||||
@@ -21,14 +16,14 @@ interface EncryptedSecretProps {
|
|||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
type: "personal" | "shared";
|
type: 'personal' | 'shared';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SecretProps {
|
interface SecretProps {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
type: 'personal' | 'shared';
|
type: 'personal' | 'shared';
|
||||||
comment: string;
|
comment: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,11 +56,11 @@ const getSecretsForProject = async ({
|
|||||||
console.log('ERROR: Not able to access the latest version of secrets');
|
console.log('ERROR: Not able to access the latest version of secrets');
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestKey = await getLatestFileKey({ workspaceId })
|
const latestKey = await getLatestFileKey({ workspaceId });
|
||||||
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
|
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
|
||||||
setIsKeyAvailable(!latestKey ? encryptedSecrets.length == 0 : true);
|
setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
|
||||||
|
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
|
|
||||||
const tempDecryptedSecrets: SecretProps[] = [];
|
const tempDecryptedSecrets: SecretProps[] = [];
|
||||||
if (latestKey) {
|
if (latestKey) {
|
||||||
@@ -78,7 +73,7 @@ const getSecretsForProject = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// decrypt secret keys, values, and comments
|
// decrypt secret keys, values, and comments
|
||||||
encryptedSecrets.map((secret: EncryptedSecretProps) => {
|
encryptedSecrets.forEach((secret: EncryptedSecretProps) => {
|
||||||
const plainTextKey = decryptSymmetric({
|
const plainTextKey = decryptSymmetric({
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
iv: secret.secretKeyIV,
|
iv: secret.secretKeyIV,
|
||||||
@@ -102,7 +97,7 @@ const getSecretsForProject = async ({
|
|||||||
key
|
key
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
plainTextComment = "";
|
plainTextComment = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
tempDecryptedSecrets.push({
|
tempDecryptedSecrets.push({
|
||||||
@@ -115,20 +110,26 @@ const getSecretsForProject = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretKeys = [...new Set(tempDecryptedSecrets.map(secret => secret.key))];
|
const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))];
|
||||||
|
|
||||||
|
const result = secretKeys.map((key, index) => ({
|
||||||
const result = secretKeys.map((key, index) => {
|
id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
|
||||||
return {
|
?.id,
|
||||||
id: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.id,
|
idOverride: tempDecryptedSecrets.filter(
|
||||||
idOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.id,
|
(secret) => secret.key === key && secret.type === 'personal'
|
||||||
pos: index,
|
)[0]?.id,
|
||||||
key: key,
|
pos: index,
|
||||||
value: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.value,
|
key,
|
||||||
valueOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.value,
|
value: tempDecryptedSecrets.filter(
|
||||||
comment: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.comment,
|
(secret) => secret.key === key && secret.type === 'shared'
|
||||||
}
|
)[0]?.value,
|
||||||
});
|
valueOverride: tempDecryptedSecrets.filter(
|
||||||
|
(secret) => secret.key === key && secret.type === 'personal'
|
||||||
|
)[0]?.value,
|
||||||
|
comment: tempDecryptedSecrets.filter(
|
||||||
|
(secret) => secret.key === key && secret.type === 'shared'
|
||||||
|
)[0]?.comment
|
||||||
|
}));
|
||||||
|
|
||||||
setData(result);
|
setData(result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { PostHog } from 'posthog-js';
|
import { PostHog } from 'posthog-js';
|
||||||
import { initPostHog } from '~/components/analytics/posthog';
|
import { initPostHog } from '@app/components/analytics/posthog';
|
||||||
import { ENV } from '~/components/utilities/config';
|
import { ENV } from '@app/components/utilities/config';
|
||||||
|
|
||||||
declare let TELEMETRY_CAPTURING_ENABLED: any;
|
declare let TELEMETRY_CAPTURING_ENABLED: any;
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class Capturer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
capture(item: string) {
|
capture(item: string) {
|
||||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||||
try {
|
try {
|
||||||
this.api.capture(item);
|
this.api.capture(item);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -23,7 +23,7 @@ class Capturer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
identify(id: string) {
|
identify(id: string) {
|
||||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||||
try {
|
try {
|
||||||
this.api.identify(id);
|
this.api.identify(id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface WorkspaceProps {
|
||||||
interface workspaceProps {
|
actionId: string;
|
||||||
actionId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,21 +10,18 @@ interface workspaceProps {
|
|||||||
* @param {string} obj.actionId - id of an action for which we are trying to get data
|
* @param {string} obj.actionId - id of an action for which we are trying to get data
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getActionData = async ({ actionId }: workspaceProps) => {
|
const getActionData = async ({ actionId }: WorkspaceProps) =>
|
||||||
return SecurityClient.fetchCall(
|
SecurityClient.fetchCall(`/api/v1/action/${actionId}`, {
|
||||||
'/api/v1/action/' + actionId, {
|
method: 'GET',
|
||||||
method: 'GET',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return (await res.json()).action;
|
return (await res.json()).action;
|
||||||
} else {
|
|
||||||
console.log('Failed to get the info about an action');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to get the info about an action');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default getActionData;
|
export default getActionData;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface WorkspaceProps {
|
||||||
interface workspaceProps {
|
workspaceId: string;
|
||||||
workspaceId: string;
|
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -12,48 +11,53 @@ interface workspaceProps {
|
|||||||
/**
|
/**
|
||||||
* This function fetches the activity logs for a certain project
|
* This function fetches the activity logs for a certain project
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
|
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
|
||||||
* @param {object} obj.offset - teh starting point of logs that we want to pull
|
* @param {object} obj.offset - teh starting point of logs that we want to pull
|
||||||
* @param {object} obj.limit - how many logs will we output
|
* @param {object} obj.limit - how many logs will we output
|
||||||
* @param {object} obj.userId - optional userId filter - will only query logs for that user
|
* @param {object} obj.userId - optional userId filter - will only query logs for that user
|
||||||
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
|
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
|
const getProjectLogs = async ({
|
||||||
|
workspaceId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
userId,
|
||||||
|
actionNames
|
||||||
|
}: WorkspaceProps) => {
|
||||||
let payload;
|
let payload;
|
||||||
if (userId != "" && actionNames != '') {
|
if (userId !== '' && actionNames !== '') {
|
||||||
payload = {
|
payload = {
|
||||||
offset: String(offset),
|
offset: String(offset),
|
||||||
limit: String(limit),
|
limit: String(limit),
|
||||||
sortBy: 'recent',
|
sortBy: 'recent',
|
||||||
userId: JSON.stringify(userId),
|
userId: JSON.stringify(userId),
|
||||||
actionNames: actionNames
|
actionNames
|
||||||
}
|
};
|
||||||
} else if (userId != "") {
|
} else if (userId !== '') {
|
||||||
payload = {
|
payload = {
|
||||||
offset: String(offset),
|
offset: String(offset),
|
||||||
limit: String(limit),
|
limit: String(limit),
|
||||||
sortBy: 'recent',
|
sortBy: 'recent',
|
||||||
userId: JSON.stringify(userId)
|
userId: JSON.stringify(userId)
|
||||||
}
|
};
|
||||||
} else if (actionNames != "") {
|
} else if (actionNames !== '') {
|
||||||
payload = {
|
payload = {
|
||||||
offset: String(offset),
|
offset: String(offset),
|
||||||
limit: String(limit),
|
limit: String(limit),
|
||||||
sortBy: 'recent',
|
sortBy: 'recent',
|
||||||
actionNames: actionNames
|
actionNames
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
payload = {
|
payload = {
|
||||||
offset: String(offset),
|
offset: String(offset),
|
||||||
limit: String(limit),
|
limit: String(limit),
|
||||||
sortBy: 'recent'
|
sortBy: 'recent'
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return SecurityClient.fetchCall(
|
return SecurityClient.fetchCall(
|
||||||
'/api/v1/workspace/' + workspaceId + '/logs?' +
|
`/api/v1/workspace/${workspaceId}/logs?${new URLSearchParams(payload)}`,
|
||||||
new URLSearchParams(payload),
|
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -61,11 +65,11 @@ const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return (await res.json()).logs;
|
return (await res.json()).logs;
|
||||||
} else {
|
|
||||||
console.log('Failed to get project logs');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to get project logs');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface WorkspaceProps {
|
||||||
interface workspaceProps {
|
workspaceId: string;
|
||||||
workspaceId: string;
|
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
@@ -10,30 +9,29 @@ interface workspaceProps {
|
|||||||
/**
|
/**
|
||||||
* This function fetches the secret snapshots for a certain project
|
* This function fetches the secret snapshots for a certain project
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||||
* @param {object} obj.offset - teh starting point of snapshots that we want to pull
|
* @param {object} obj.offset - teh starting point of snapshots that we want to pull
|
||||||
* @param {object} obj.limit - how many snapshots will we output
|
* @param {object} obj.limit - how many snapshots will we output
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => {
|
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: WorkspaceProps) =>
|
||||||
return SecurityClient.fetchCall(
|
SecurityClient.fetchCall(
|
||||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' +
|
`/api/v1/workspace/${workspaceId}/secret-snapshots?${new URLSearchParams({
|
||||||
new URLSearchParams({
|
|
||||||
offset: String(offset),
|
offset: String(offset),
|
||||||
limit: String(limit)
|
limit: String(limit)
|
||||||
}), {
|
})}`,
|
||||||
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return (await res.json()).secretSnapshots;
|
return (await res.json()).secretSnapshots;
|
||||||
} else {
|
|
||||||
console.log('Failed to get project secret snapshots');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to get project secret snapshots');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default getProjectSecretShanpshots;
|
export default getProjectSecretShanpshots;
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface WorkspaceProps {
|
||||||
interface workspaceProps {
|
workspaceId: string;
|
||||||
workspaceId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function fetches the count of secret snapshots for a certain project
|
* This function fetches the count of secret snapshots for a certain project
|
||||||
* @param {object} obj
|
* @param {object} obj
|
||||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
|
const getProjectSercetSnapshotsCount = async ({ workspaceId }: WorkspaceProps) =>
|
||||||
return SecurityClient.fetchCall(
|
SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/count`, {
|
||||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
|
method: 'GET',
|
||||||
method: 'GET',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return (await res.json()).count;
|
return (await res.json()).count;
|
||||||
} else {
|
|
||||||
console.log('Failed to get the count of project secret snapshots');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to get the count of project secret snapshots');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default getProjectSercetSnapshotsCount;
|
export default getProjectSercetSnapshotsCount;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
|
||||||
interface SnapshotProps {
|
interface SnapshotProps {
|
||||||
secretSnapshotId: string;
|
secretSnapshotId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,21 +10,18 @@ interface SnapshotProps {
|
|||||||
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
|
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => {
|
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) =>
|
||||||
return SecurityClient.fetchCall(
|
SecurityClient.fetchCall(`/api/v1/secret-snapshot/${secretSnapshotId}`, {
|
||||||
'/api/v1/secret-snapshot/' + secretSnapshotId, {
|
method: 'GET',
|
||||||
method: 'GET',
|
headers: {
|
||||||
headers: {
|
'Content-Type': 'application/json'
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return (await res.json()).secretSnapshot;
|
return (await res.json()).secretSnapshot;
|
||||||
} else {
|
|
||||||
console.log('Failed to get the secrets of a certain snapshot');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to get the secrets of a certain snapshot');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default getSecretSnapshotData;
|
export default getSecretSnapshotData;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
|
interface SecretVersionProps {
|
||||||
interface secretVersionProps {
|
secretId: string;
|
||||||
secretId: string;
|
offset: number;
|
||||||
offset: number;
|
|
||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,13 +14,12 @@ interface secretVersionProps {
|
|||||||
* @param {number} obj.limit - how far our query goes
|
* @param {number} obj.limit - how far our query goes
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
|
const getSecretVersions = async ({ secretId, offset, limit }: SecretVersionProps) =>
|
||||||
return SecurityClient.fetchCall(
|
SecurityClient.fetchCall(
|
||||||
'/api/v1/secret/' + secretId + '/secret-versions?' +
|
`/api/v1/secret/${secretId}/secret-versions?${new URLSearchParams({
|
||||||
new URLSearchParams({
|
offset: String(offset),
|
||||||
offset: String(offset),
|
limit: String(limit)
|
||||||
limit: String(limit)
|
})}`,
|
||||||
}),
|
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -29,12 +27,11 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return await res.json();
|
return res.json();
|
||||||
} else {
|
|
||||||
console.log('Failed to get secret version history');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to get secret version history');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default getSecretVersions;
|
export default getSecretVersions;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function performs a rollback of secrets in a certain project
|
* This function performs a rollback of secrets in a certain project
|
||||||
@@ -7,24 +7,27 @@ import SecurityClient from '~/utilities/SecurityClient';
|
|||||||
* @param {number} obj.version - version to which we are rolling back
|
* @param {number} obj.version - version to which we are rolling back
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
|
const performSecretRollback = async ({
|
||||||
return SecurityClient.fetchCall(
|
workspaceId,
|
||||||
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
|
version
|
||||||
method: 'POST',
|
}: {
|
||||||
headers: {
|
workspaceId: string;
|
||||||
'Content-Type': 'application/json'
|
version: number;
|
||||||
},
|
}) =>
|
||||||
body: JSON.stringify({
|
SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`, {
|
||||||
version
|
method: 'POST',
|
||||||
})
|
headers: {
|
||||||
}
|
'Content-Type': 'application/json'
|
||||||
).then(async (res) => {
|
},
|
||||||
if (res && res.status == 200) {
|
body: JSON.stringify({
|
||||||
return (await res.json());
|
version
|
||||||
} else {
|
})
|
||||||
console.log('Failed to perform the secret rollback');
|
}).then(async (res) => {
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
return res.json();
|
||||||
}
|
}
|
||||||
|
console.log('Failed to perform the secret rollback');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default performSecretRollback;
|
export default performSecretRollback;
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import getActionData from "@app/ee/api/secrets/GetActionData";
|
import getActionData from '@app/ee/api/secrets/GetActionData';
|
||||||
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
||||||
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
|
||||||
|
|
||||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
||||||
|
import {
|
||||||
|
|
||||||
const {
|
|
||||||
decryptAssymmetric,
|
decryptAssymmetric,
|
||||||
decryptSymmetric
|
decryptSymmetric
|
||||||
} = require('../../components/utilities/cryptography/crypto');
|
} from '../../components/utilities/cryptography/crypto';
|
||||||
const nacl = require('tweetnacl');
|
|
||||||
nacl.util = require('tweetnacl-util');
|
|
||||||
|
|
||||||
|
|
||||||
interface SideBarProps {
|
interface SideBarProps {
|
||||||
toggleSidebar: (value: string) => void;
|
toggleSidebar: (value: string) => void;
|
||||||
currentAction: string;
|
currentAction: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,11 +35,11 @@ interface DecryptedSecretProps {
|
|||||||
newSecretVersion: {
|
newSecretVersion: {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
};
|
||||||
oldSecretVersion: {
|
oldSecretVersion: {
|
||||||
key: string;
|
key: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActionProps {
|
interface ActionProps {
|
||||||
@@ -58,10 +52,7 @@ interface ActionProps {
|
|||||||
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
|
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
|
||||||
* @returns the sidebar with the payload of user activity logs
|
* @returns the sidebar with the payload of user activity logs
|
||||||
*/
|
*/
|
||||||
const ActivitySideBar = ({
|
const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||||
toggleSidebar,
|
|
||||||
currentAction
|
|
||||||
}: SideBarProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||||
@@ -72,7 +63,7 @@ const ActivitySideBar = ({
|
|||||||
const getLogData = async () => {
|
const getLogData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const tempActionData = await getActionData({ actionId: currentAction });
|
const tempActionData = await getActionData({ actionId: currentAction });
|
||||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
// #TODO: make this a separate function and reuse across the app
|
// #TODO: make this a separate function and reuse across the app
|
||||||
@@ -86,12 +77,12 @@ const ActivitySideBar = ({
|
|||||||
privateKey: String(PRIVATE_KEY)
|
privateKey: String(PRIVATE_KEY)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
|
const decryptedSecretVersions = tempActionData.payload.secretVersions.map(
|
||||||
newSecretVersion?: SecretProps;
|
(encryptedSecretVersion: {
|
||||||
oldSecretVersion?: SecretProps;
|
newSecretVersion?: SecretProps;
|
||||||
}) => {
|
oldSecretVersion?: SecretProps;
|
||||||
return {
|
}) => ({
|
||||||
newSecretVersion: {
|
newSecretVersion: {
|
||||||
key: decryptSymmetric({
|
key: decryptSymmetric({
|
||||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
|
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
|
||||||
@@ -107,79 +98,134 @@ const ActivitySideBar = ({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
oldSecretVersion: {
|
oldSecretVersion: {
|
||||||
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
|
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
|
||||||
? decryptSymmetric({
|
? decryptSymmetric({
|
||||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
|
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
|
||||||
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
|
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
|
||||||
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
|
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
|
||||||
key: decryptedLatestKey
|
key: decryptedLatestKey
|
||||||
}): undefined,
|
})
|
||||||
|
: undefined,
|
||||||
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
|
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
|
||||||
? decryptSymmetric({
|
? decryptSymmetric({
|
||||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
|
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
|
||||||
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
|
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
|
||||||
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
|
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
|
||||||
key: decryptedLatestKey
|
key: decryptedLatestKey
|
||||||
}): undefined
|
})
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
})
|
);
|
||||||
|
|
||||||
setActionData(decryptedSecretVersions);
|
setActionData(decryptedSecretVersions);
|
||||||
setActionMetaData({name: tempActionData.name});
|
setActionMetaData({ name: tempActionData.name });
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
getLogData();
|
getLogData();
|
||||||
}, [currentAction]);
|
}, [currentAction]);
|
||||||
|
|
||||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
|
return (
|
||||||
{isLoading ? (
|
<div
|
||||||
<div className="flex items-center justify-center h-full mb-8">
|
className={`absolute border-l border-mineshaft-500 ${
|
||||||
<Image
|
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||||
src="/images/loading/loading.gif"
|
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
||||||
height={60}
|
>
|
||||||
width={100}
|
{isLoading ? (
|
||||||
alt="infisical loading indicator"
|
<div className="flex items-center justify-center h-full mb-8">
|
||||||
></Image>
|
<Image
|
||||||
</div>
|
src="/images/loading/loading.gif"
|
||||||
) : (
|
height={60}
|
||||||
<div className='h-min overflow-y-auto'>
|
width={100}
|
||||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
alt="infisical loading indicator"
|
||||||
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p>
|
/>
|
||||||
<div className='p-1' onClick={() => toggleSidebar("")}>
|
</div>
|
||||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
) : (
|
||||||
|
<div className="h-min overflow-y-auto">
|
||||||
|
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||||
|
<p className="font-semibold text-lg text-bunker-200">
|
||||||
|
{t(`activity:event.${actionMetaData?.name}`)}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="p-1"
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => toggleSidebar('')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col px-4">
|
||||||
|
{(actionMetaData?.name === 'readSecrets' ||
|
||||||
|
actionMetaData?.name === 'addSecrets' ||
|
||||||
|
actionMetaData?.name === 'deleteSecrets') &&
|
||||||
|
actionData?.map((item, id) => (
|
||||||
|
<div key={`secret.${id + 1}`}>
|
||||||
|
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||||
|
{item.newSecretVersion.key}
|
||||||
|
</div>
|
||||||
|
<DashboardInputField
|
||||||
|
onChangeHandler={() => {}}
|
||||||
|
type="value"
|
||||||
|
position={1}
|
||||||
|
value={item.newSecretVersion.value}
|
||||||
|
isDuplicate={false}
|
||||||
|
blurred={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{actionMetaData?.name === 'updateSecrets' &&
|
||||||
|
actionData?.map((item, id) => (
|
||||||
|
<>
|
||||||
|
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||||
|
{item.newSecretVersion.key}
|
||||||
|
</div>
|
||||||
|
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
|
||||||
|
<div className="bg-red/30 px-2">
|
||||||
|
-{' '}
|
||||||
|
{patienceDiff(
|
||||||
|
item.oldSecretVersion.value.split(''),
|
||||||
|
item.newSecretVersion.value.split(''),
|
||||||
|
false
|
||||||
|
).lines.map(
|
||||||
|
(character, lineId) =>
|
||||||
|
character.bIndex !== -1 && (
|
||||||
|
<span
|
||||||
|
key={`actionData.${id + 1}.line.${lineId + 1}`}
|
||||||
|
className={`${character.aIndex === -1 && 'bg-red-700/80'}`}
|
||||||
|
>
|
||||||
|
{character.line}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-500/30 px-2">
|
||||||
|
+{' '}
|
||||||
|
{patienceDiff(
|
||||||
|
item.oldSecretVersion.value.split(''),
|
||||||
|
item.newSecretVersion.value.split(''),
|
||||||
|
false
|
||||||
|
).lines.map(
|
||||||
|
(character, lineId) =>
|
||||||
|
character.aIndex !== -1 && (
|
||||||
|
<span
|
||||||
|
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
|
||||||
|
className={`${character.bIndex === -1 && 'bg-green-700/80'}`}
|
||||||
|
>
|
||||||
|
{character.line}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col px-4'>
|
)}
|
||||||
{(actionMetaData?.name == 'readSecrets'
|
</div>
|
||||||
|| actionMetaData?.name == 'addSecrets'
|
);
|
||||||
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
|
|
||||||
<div key={id}>
|
|
||||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
|
||||||
<DashboardInputField
|
|
||||||
key={id}
|
|
||||||
onChangeHandler={() => {}}
|
|
||||||
type="value"
|
|
||||||
position={1}
|
|
||||||
value={item.newSecretVersion.value}
|
|
||||||
isDuplicate={false}
|
|
||||||
blurred={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
|
|
||||||
<>
|
|
||||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
|
||||||
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
|
|
||||||
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
|
|
||||||
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActivitySideBar;
|
export default ActivitySideBar;
|
||||||
|
|||||||
@@ -1,24 +1,20 @@
|
|||||||
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
import timeSince from '@app/ee/utilities/timeSince';
|
import timeSince from '@app/ee/utilities/timeSince';
|
||||||
import {
|
import { faAngleDown, faAngleRight, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
|
||||||
faAngleDown,
|
|
||||||
faAngleRight,
|
|
||||||
faUpRightFromSquare
|
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import guidGenerator from '../../components/utilities/randomId';
|
import guidGenerator from '../../components/utilities/randomId';
|
||||||
|
|
||||||
|
|
||||||
interface PayloadProps {
|
interface PayloadProps {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
secretVersions: string[];
|
secretVersions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface logData {
|
interface LogData {
|
||||||
_id: string;
|
_id: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -27,15 +23,20 @@ interface logData {
|
|||||||
payload: PayloadProps[];
|
payload: PayloadProps[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a single row of the activity table
|
* This is a single row of the activity table
|
||||||
* @param obj
|
* @param obj
|
||||||
* @param {logData} obj.row - data for a certain event
|
* @param {LogData} obj.row - data for a certain event
|
||||||
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
|
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
|
const ActivityLogsRow = ({
|
||||||
|
row,
|
||||||
|
toggleSidebar
|
||||||
|
}: {
|
||||||
|
row: LogData;
|
||||||
|
toggleSidebar: (value: string) => void;
|
||||||
|
}) => {
|
||||||
const [payloadOpened, setPayloadOpened] = useState(false);
|
const [payloadOpened, setPayloadOpened] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -43,6 +44,7 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
|||||||
<>
|
<>
|
||||||
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
||||||
<td
|
<td
|
||||||
|
onKeyDown={() => null}
|
||||||
onClick={() => setPayloadOpened(!payloadOpened)}
|
onClick={() => setPayloadOpened(!payloadOpened)}
|
||||||
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
||||||
>
|
>
|
||||||
@@ -54,40 +56,58 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
|
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
|
||||||
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")}
|
{row.payload
|
||||||
</td>
|
?.map(
|
||||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
(action) =>
|
||||||
{row.user}
|
`${String(action.secretVersions.length)} ${t(`activity:event.${action.name}`)}`
|
||||||
</td>
|
)
|
||||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
.join(' and ')}
|
||||||
{row.channel}
|
|
||||||
</td>
|
</td>
|
||||||
|
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.user}</td>
|
||||||
|
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.channel}</td>
|
||||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||||
{timeSince(new Date(row.createdAt))}
|
{timeSince(new Date(row.createdAt))}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{payloadOpened &&
|
{payloadOpened && (
|
||||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||||
<td></td>
|
<td />
|
||||||
<td>{String(t("common:timestamp"))}</td>
|
<td>{String(t('common:timestamp'))}</td>
|
||||||
<td>{row.createdAt}</td>
|
<td>{row.createdAt}</td>
|
||||||
</tr>}
|
|
||||||
{payloadOpened && row.payload?.map((action, index) => {
|
|
||||||
return action.secretVersions.length > 0 && <tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
|
||||||
<td></td>
|
|
||||||
<td className="">{t("activity:event." + action.name)}</td>
|
|
||||||
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
|
|
||||||
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
|
|
||||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
})}
|
)}
|
||||||
{payloadOpened &&
|
{payloadOpened &&
|
||||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
row.payload?.map(
|
||||||
<td></td>
|
(action) =>
|
||||||
<td>{String(t("activity:ip-address"))}</td>
|
action.secretVersions.length > 0 && (
|
||||||
<td>{row.ipAddress}</td>
|
<tr
|
||||||
</tr>}
|
key={action._id}
|
||||||
|
className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm"
|
||||||
|
>
|
||||||
|
<td />
|
||||||
|
<td className="">{t(`activity:event.${action.name}`)}</td>
|
||||||
|
<td
|
||||||
|
onKeyDown={() => null}
|
||||||
|
className="text-primary-300 cursor-pointer hover:text-primary duration-200"
|
||||||
|
onClick={() => toggleSidebar(action._id)}
|
||||||
|
>
|
||||||
|
{action.secretVersions.length +
|
||||||
|
(action.secretVersions.length !== 1 ? ' secrets' : ' secret')}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faUpRightFromSquare}
|
||||||
|
className="ml-2 mb-0.5 font-light w-3 h-3"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{payloadOpened && (
|
||||||
|
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||||
|
<td />
|
||||||
|
<td>{String(t('activity:ip-address'))}</td>
|
||||||
|
<td>{row.ipAddress}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -100,37 +120,61 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
|||||||
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
|
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const ActivityTable = ({ data, toggleSidebar, isLoading }: { data: logData[], toggleSidebar: (value: string) => void; isLoading: boolean; }) => {
|
const ActivityTable = ({
|
||||||
|
data,
|
||||||
|
toggleSidebar,
|
||||||
|
isLoading
|
||||||
|
}: {
|
||||||
|
data: LogData[];
|
||||||
|
toggleSidebar: (value: string) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-6 mt-8">
|
<div className="w-full px-6 mt-8">
|
||||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
|
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
|
||||||
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div>
|
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5" />
|
||||||
<table className="w-full my-1">
|
<table className="w-full my-1">
|
||||||
<thead className="text-bunker-300">
|
<thead className="text-bunker-300">
|
||||||
<tr className='text-sm'>
|
<tr className="text-sm">
|
||||||
<th className="text-left pl-6 pt-2.5 pb-3"></th>
|
<th aria-label="actions" className="text-left pl-6 pt-2.5 pb-3" />
|
||||||
<th className="text-left font-semibold pt-2.5 pb-3">{String(t("common:event")).toUpperCase()}</th>
|
<th className="text-left font-semibold pt-2.5 pb-3">
|
||||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:user")).toUpperCase()}</th>
|
{String(t('common:event')).toUpperCase()}
|
||||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:source")).toUpperCase()}</th>
|
</th>
|
||||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:time")).toUpperCase()}</th>
|
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||||
<th></th>
|
{String(t('common:user')).toUpperCase()}
|
||||||
|
</th>
|
||||||
|
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||||
|
{String(t('common:source')).toUpperCase()}
|
||||||
|
</th>
|
||||||
|
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||||
|
{String(t('common:time')).toUpperCase()}
|
||||||
|
</th>
|
||||||
|
<th aria-label="action" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data?.map((row, index) => {
|
{data?.map((row, index) => (
|
||||||
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
|
<ActivityLogsRow
|
||||||
})}
|
key={`activity.${index + 1}.${row._id}`}
|
||||||
|
row={row}
|
||||||
|
toggleSidebar={toggleSidebar}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{isLoading && <div className='w-full flex justify-center mb-8 mt-4'><Image
|
{isLoading && (
|
||||||
src="/images/loading/loading.gif"
|
<div className="w-full flex justify-center mb-8 mt-4">
|
||||||
height={60}
|
<Image
|
||||||
width={100}
|
src="/images/loading/loading.gif"
|
||||||
alt="loading animation"
|
height={60}
|
||||||
></Image></div>}
|
width={100}
|
||||||
|
alt="loading animation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { useEffect, useState } from "react";
|
/* eslint-disable no-nested-ternary */
|
||||||
import Image from "next/image";
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from "next/router";
|
import Image from 'next/image';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useRouter } from 'next/router';
|
||||||
import getProjectSecretShanpshots from "@app/ee/api/secrets/GetProjectSercetShanpshots";
|
import { useTranslation } from 'next-i18next';
|
||||||
import getSecretSnapshotData from "@app/ee/api/secrets/GetSecretSnapshotData";
|
import Button from '@app/components/basic/buttons/Button';
|
||||||
import timeSince from "@app/ee/utilities/timeSince";
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
decryptSymmetric
|
||||||
|
} from '@app/components/utilities/cryptography/crypto';
|
||||||
|
import getProjectSecretShanpshots from '@app/ee/api/secrets/GetProjectSercetShanpshots';
|
||||||
|
import getSecretSnapshotData from '@app/ee/api/secrets/GetSecretSnapshotData';
|
||||||
|
import timeSince from '@app/ee/utilities/timeSince';
|
||||||
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import Button from "~/components/basic/buttons/Button";
|
|
||||||
import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto";
|
|
||||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
|
||||||
|
|
||||||
|
|
||||||
export interface SecretDataProps {
|
export interface SecretDataProps {
|
||||||
pos: number;
|
pos: number;
|
||||||
key: string;
|
key: string;
|
||||||
@@ -23,7 +25,7 @@ export interface SecretDataProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SideBarProps {
|
interface SideBarProps {
|
||||||
toggleSidebar: (value: boolean) => void;
|
toggleSidebar: (value: boolean) => void;
|
||||||
setSnapshotData: (value: any) => void;
|
setSnapshotData: (value: any) => void;
|
||||||
chosenSnapshot: string;
|
chosenSnapshot: string;
|
||||||
}
|
}
|
||||||
@@ -44,7 +46,7 @@ interface EncrypetedSecretVersionListProps {
|
|||||||
secretKeyIV: string;
|
secretKeyIV: string;
|
||||||
secretKeyTag: string;
|
secretKeyTag: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
type: "personal" | "shared";
|
type: 'personal' | 'shared';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,11 +56,7 @@ interface EncrypetedSecretVersionListProps {
|
|||||||
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
|
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
|
||||||
* @returns the sidebar with the options for point-in-time recovery (commits)
|
* @returns the sidebar with the options for point-in-time recovery (commits)
|
||||||
*/
|
*/
|
||||||
const PITRecoverySidebar = ({
|
const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }: SideBarProps) => {
|
||||||
toggleSidebar,
|
|
||||||
setSnapshotData,
|
|
||||||
chosenSnapshot
|
|
||||||
}: SideBarProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -68,22 +66,26 @@ const PITRecoverySidebar = ({
|
|||||||
|
|
||||||
const loadMoreSnapshots = () => {
|
const loadMoreSnapshots = () => {
|
||||||
setCurrentOffset(currentOffset + currentLimit);
|
setCurrentOffset(currentOffset + currentLimit);
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getLogData = async () => {
|
const getLogData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset })
|
const results = await getProjectSecretShanpshots({
|
||||||
|
workspaceId: String(router.query.id),
|
||||||
|
limit: currentLimit,
|
||||||
|
offset: currentOffset
|
||||||
|
});
|
||||||
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
|
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
getLogData();
|
getLogData();
|
||||||
}, [currentOffset]);
|
}, [currentOffset]);
|
||||||
|
|
||||||
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => {
|
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string }) => {
|
||||||
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
|
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
|
||||||
|
|
||||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
let decryptedLatestKey: string;
|
let decryptedLatestKey: string;
|
||||||
@@ -97,10 +99,10 @@ const PITRecoverySidebar = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => {
|
const decryptedSecretVersions = secretSnapshotData.secretVersions.map(
|
||||||
return {
|
(encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
|
||||||
id: encryptedSecretVersion._id,
|
id: encryptedSecretVersion._id,
|
||||||
pos: pos,
|
pos,
|
||||||
type: encryptedSecretVersion.type,
|
type: encryptedSecretVersion.type,
|
||||||
environment: encryptedSecretVersion.environment,
|
environment: encryptedSecretVersion.environment,
|
||||||
key: decryptSymmetric({
|
key: decryptSymmetric({
|
||||||
@@ -115,69 +117,132 @@ const PITRecoverySidebar = ({
|
|||||||
tag: encryptedSecretVersion.secretValueTag,
|
tag: encryptedSecretVersion.secretValueTag,
|
||||||
key: decryptedLatestKey
|
key: decryptedLatestKey
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
})
|
);
|
||||||
|
|
||||||
|
const secretKeys = [
|
||||||
|
...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))
|
||||||
|
];
|
||||||
|
|
||||||
const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))];
|
const result = secretKeys.map((key, index) => ({
|
||||||
|
id: decryptedSecretVersions.filter(
|
||||||
const result = secretKeys.map((key, index) => {
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||||
return {
|
)[0].id,
|
||||||
id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id,
|
pos: index,
|
||||||
pos: index,
|
key,
|
||||||
key: key,
|
environment: decryptedSecretVersions.filter(
|
||||||
environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment,
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||||
value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value,
|
)[0].environment,
|
||||||
valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value,
|
value: decryptedSecretVersions.filter(
|
||||||
}
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||||
|
)[0]?.value,
|
||||||
|
valueOverride: decryptedSecretVersions.filter(
|
||||||
|
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||||
|
)[0]?.value
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSnapshotData({
|
||||||
|
id: secretSnapshotData._id,
|
||||||
|
version: secretSnapshotData.version,
|
||||||
|
createdAt: secretSnapshotData.createdAt,
|
||||||
|
secretVersions: result,
|
||||||
|
comment: ''
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' })
|
return (
|
||||||
}
|
<div
|
||||||
|
className={`absolute border-l border-mineshaft-500 ${
|
||||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
|
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||||
{isLoading ? (
|
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
||||||
<div className="flex items-center justify-center h-full mb-8">
|
>
|
||||||
<Image
|
{isLoading ? (
|
||||||
src="/images/loading/loading.gif"
|
<div className="flex items-center justify-center h-full mb-8">
|
||||||
height={60}
|
<Image
|
||||||
width={100}
|
src="/images/loading/loading.gif"
|
||||||
alt="infisical loading indicator"
|
height={60}
|
||||||
></Image>
|
width={100}
|
||||||
</div>
|
alt="infisical loading indicator"
|
||||||
) : (
|
/>
|
||||||
<div className='h-min'>
|
|
||||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
|
||||||
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
|
|
||||||
<div className='p-1' onClick={() => toggleSidebar(false)}>
|
|
||||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'>
|
) : (
|
||||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
|
<div className="h-min">
|
||||||
<div
|
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||||
key={snapshot._id}
|
<p className="font-semibold text-lg text-bunker-200">{t('Point-in-time Recovery')}</p>
|
||||||
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
<div
|
||||||
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black pointer-events-none" : "bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className="p-1"
|
||||||
|
onClick={() => toggleSidebar(false)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-start">
|
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
|
</div>
|
||||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
|
</div>
|
||||||
|
<div className="flex flex-col px-2 py-2 overflow-y-auto h-[92vh]">
|
||||||
|
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => (
|
||||||
|
<div
|
||||||
|
onKeyDown={() => null}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
key={snapshot._id}
|
||||||
|
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
||||||
|
className={`${
|
||||||
|
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||||
|
? 'bg-primary text-black pointer-events-none'
|
||||||
|
: 'bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer'
|
||||||
|
} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-start">
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||||
|
? 'text-bunker-800'
|
||||||
|
: 'text-bunker-200'
|
||||||
|
} text-sm mr-1.5`}
|
||||||
|
>
|
||||||
|
{timeSince(new Date(snapshot.createdAt))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||||
|
? 'text-bunker-900'
|
||||||
|
: 'text-bunker-300'
|
||||||
|
} text-sm `}
|
||||||
|
>{` - ${snapshot.secretVersions.length} Secrets`}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||||
|
? 'text-bunker-800 pointer-events-none'
|
||||||
|
: 'text-bunker-200 hover:text-primary duration-200 cursor-pointer'
|
||||||
|
} text-sm`}
|
||||||
|
>
|
||||||
|
{id === 0
|
||||||
|
? 'Current Version'
|
||||||
|
: chosenSnapshot === snapshot._id
|
||||||
|
? 'Currently Viewing'
|
||||||
|
: 'Explore'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
))}
|
||||||
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
|
<div className="flex justify-center w-full mb-14">
|
||||||
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
|
<div className="items-center w-40">
|
||||||
|
<Button
|
||||||
|
text="View More"
|
||||||
|
textDisabled="End of History"
|
||||||
|
active={secretSnapshotsMetadata.length % 15 === 0}
|
||||||
|
onButtonPressed={loadMoreSnapshots}
|
||||||
|
size="md"
|
||||||
|
color="mineshaft"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>)}
|
|
||||||
<div className='flex justify-center w-full mb-14'>
|
|
||||||
<div className='items-center w-40'>
|
|
||||||
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PITRecoverySidebar;
|
export default PITRecoverySidebar;
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import {
|
||||||
|
decryptAssymmetric,
|
||||||
|
decryptSymmetric
|
||||||
|
} from '@app/components/utilities/cryptography/crypto';
|
||||||
import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions';
|
import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions';
|
||||||
|
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
import { decryptAssymmetric, decryptSymmetric } from '~/components/utilities/cryptography/crypto';
|
|
||||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
|
||||||
|
|
||||||
interface DecryptedSecretVersionListProps {
|
interface DecryptedSecretVersionListProps {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -21,24 +23,23 @@ interface EncrypetedSecretVersionListProps {
|
|||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} secretId - the id of a secret for which are querying version history
|
* @param {string} secretId - the id of a secret for which are querying version history
|
||||||
* @returns a list of versions for a specific secret
|
* @returns a list of versions for a specific secret
|
||||||
*/
|
*/
|
||||||
const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
|
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getSecretVersionHistory = async () => {
|
const getSecretVersionHistory = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
|
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10 });
|
||||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||||
|
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
|
||||||
let decryptedLatestKey: string;
|
let decryptedLatestKey: string;
|
||||||
@@ -52,8 +53,8 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
|
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map(
|
||||||
return {
|
(encryptedSecretVersion: EncrypetedSecretVersionListProps) => ({
|
||||||
createdAt: encryptedSecretVersion.createdAt,
|
createdAt: encryptedSecretVersion.createdAt,
|
||||||
value: decryptSymmetric({
|
value: decryptSymmetric({
|
||||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||||
@@ -61,63 +62,76 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
|||||||
tag: encryptedSecretVersion.secretValueTag,
|
tag: encryptedSecretVersion.secretValueTag,
|
||||||
key: decryptedLatestKey
|
key: decryptedLatestKey
|
||||||
})
|
})
|
||||||
}
|
})
|
||||||
})
|
);
|
||||||
|
|
||||||
setSecretVersions(decryptedSecretVersions);
|
setSecretVersions(decryptedSecretVersions);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
getSecretVersionHistory();
|
getSecretVersionHistory();
|
||||||
}, [secretId]);
|
}, [secretId]);
|
||||||
|
|
||||||
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
return (
|
||||||
<p className=''>{t("dashboard:sidebar.version-history")}</p>
|
<div className="w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
|
||||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'>
|
<p className="">{t('dashboard:sidebar.version-history')}</p>
|
||||||
{isLoading ? (
|
<div className="p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
|
||||||
<div className="flex items-center justify-center h-full">
|
{isLoading ? (
|
||||||
<Image
|
<div className="flex items-center justify-center h-full">
|
||||||
src="/images/loading/loading.gif"
|
<Image
|
||||||
height={60}
|
src="/images/loading/loading.gif"
|
||||||
width={100}
|
height={60}
|
||||||
alt="infisical loading indicator"
|
width={100}
|
||||||
></Image>
|
alt="infisical loading indicator"
|
||||||
</div>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
) : (
|
||||||
{secretVersions
|
<div className="h-48 overflow-y-auto overflow-x-none">
|
||||||
? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
{secretVersions ? (
|
||||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
secretVersions
|
||||||
<div key={index} className='flex flex-row'>
|
?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||||
<div className='pr-1 flex flex-col items-center'>
|
.map((version: DecryptedSecretVersionListProps, index: number) => (
|
||||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
<div key={`${version.createdAt}.${index + 1}`} className="flex flex-row">
|
||||||
<div className='w-0 h-full border-l mt-1'></div>
|
<div className="pr-1 flex flex-col items-center">
|
||||||
</div>
|
<div className="p-1">
|
||||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
|
||||||
<div className='pr-2 pt-1'>
|
</div>
|
||||||
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
|
<div className="w-0 h-full border-l mt-1" />
|
||||||
year: 'numeric',
|
</div>
|
||||||
month: '2-digit',
|
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
|
||||||
day: '2-digit',
|
<div className="pr-2 pt-1">
|
||||||
hour: '2-digit',
|
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
||||||
minute: '2-digit',
|
year: 'numeric',
|
||||||
second: '2-digit'
|
month: '2-digit',
|
||||||
})}
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<p className="break-words">
|
||||||
|
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5">
|
||||||
|
Value:
|
||||||
|
</span>
|
||||||
|
{version.value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
))
|
||||||
</div>
|
) : (
|
||||||
|
<div className="w-full h-full flex items-center justify-center text-bunker-400">
|
||||||
|
No version history yet.
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
: (
|
</div>
|
||||||
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
|
)}
|
||||||
)
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SecretVersionList;
|
export default SecretVersionList;
|
||||||
|
|||||||
@@ -1,270 +1,284 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// TODO(akhilmhdh): Danger. This file functions needs to simplified.
|
||||||
|
// Disabled eslinting as below algos uses unnary operator and need to be careful on handling it
|
||||||
|
// Will revisit this and change to simplier non-recursive to dynamic programming with space allocation strategy
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param textOld - old secret
|
* @param textOld - old secret
|
||||||
* @param textNew - new (updated) secret
|
* @param textNew - new (updated) secret
|
||||||
* @param diffPlusFlag - a flag for whether we want to detect moving segments
|
* @param diffPlusFlag - a flag for whether we want to detect moving segments
|
||||||
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
|
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
|
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* findUnique finds all unique values in arr[lo..hi], inclusive. This
|
* findUnique finds all unique values in arr[lo..hi], inclusive. This
|
||||||
* function is used in preparation for determining the longest common
|
* function is used in preparation for determining the longest common
|
||||||
* subsequence. Specifically, it first reduces the array range in question
|
* subsequence. Specifically, it first reduces the array range in question
|
||||||
* to unique values.
|
* to unique values.
|
||||||
* @param chars - an array of characters
|
* @param chars - an array of characters
|
||||||
* @param lo
|
* @param lo
|
||||||
* @param hi
|
* @param hi
|
||||||
* @returns - an ordered Map, with the arr[i] value as the Map key and the
|
* @returns - an ordered Map, with the arr[i] value as the Map key and the
|
||||||
* array index i as the Map value.
|
* array index i as the Map value.
|
||||||
*/
|
*/
|
||||||
function findUnique(chars: string[], lo: number, hi: number) {
|
function findUnique(chars: string[], lo: number, hi: number) {
|
||||||
const characterMap = new Map();
|
const characterMap = new Map();
|
||||||
|
|
||||||
for (let i=lo; i<=hi; i++) {
|
for (let i = lo; i <= hi; i += 1) {
|
||||||
const character = chars[i];
|
const character = chars[i];
|
||||||
|
|
||||||
if (characterMap.has(character)) {
|
|
||||||
characterMap.get(character).count++;
|
|
||||||
characterMap.get(character).index = i;
|
|
||||||
} else {
|
|
||||||
characterMap.set(character, { count: 1, index: i });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
characterMap.forEach((val, key, map) => {
|
if (characterMap.has(character)) {
|
||||||
if (val.count !== 1) {
|
characterMap.get(character).count += 1;
|
||||||
map.delete(key);
|
characterMap.get(character).index = i;
|
||||||
} else {
|
} else {
|
||||||
map.set(key, val.index);
|
characterMap.set(character, { count: 1, index: i });
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return characterMap;
|
characterMap.forEach((val, key, map) => {
|
||||||
}
|
if (val.count !== 1) {
|
||||||
|
map.delete(key);
|
||||||
|
} else {
|
||||||
|
map.set(key, val.index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return characterMap;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param aArray
|
* @param aArray
|
||||||
* @param aLo
|
* @param aLo
|
||||||
* @param aHi
|
* @param aHi
|
||||||
* @param bArray
|
* @param bArray
|
||||||
* @param bLo
|
* @param bLo
|
||||||
* @param bHi
|
* @param bHi
|
||||||
* @returns an ordered Map, with the Map key as the common line between aArray
|
* @returns an ordered Map, with the Map key as the common line between aArray
|
||||||
* and bArray, with the Map value as an object containing the array indexes of
|
* and bArray, with the Map value as an object containing the array indexes of
|
||||||
* the matching unique lines.
|
* the matching unique lines.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
|
function uniqueCommon(
|
||||||
const ma = findUnique(aArray, aLo, aHi);
|
aArray: string[],
|
||||||
const mb = findUnique(bArray, bLo, bHi);
|
aLo: number,
|
||||||
|
aHi: number,
|
||||||
|
bArray: string[],
|
||||||
|
bLo: number,
|
||||||
|
bHi: number
|
||||||
|
) {
|
||||||
|
const ma = findUnique(aArray, aLo, aHi);
|
||||||
|
const mb = findUnique(bArray, bLo, bHi);
|
||||||
|
|
||||||
ma.forEach((val, key, map) => {
|
ma.forEach((val, key, map) => {
|
||||||
if (mb.has(key)) {
|
if (mb.has(key)) {
|
||||||
map.set(key, {
|
map.set(key, {
|
||||||
indexA: val,
|
indexA: val,
|
||||||
indexB: mb.get(key)
|
indexB: mb.get(key)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
map.delete(key);
|
map.delete(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return ma;
|
return ma;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
|
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
|
||||||
* and determines the Longest Common Subsequence (LCS).
|
* and determines the Longest Common Subsequence (LCS).
|
||||||
* @param abMap
|
* @param abMap
|
||||||
* @returns an ordered array of objects containing the array indexes of the
|
* @returns an ordered array of objects containing the array indexes of the
|
||||||
* matching lines for a LCS.
|
* matching lines for a LCS.
|
||||||
*/
|
*/
|
||||||
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
|
function longestCommonSubsequence(
|
||||||
const ja: any = [];
|
abMap: Map<number, { indexA: number; indexB: number; prev?: number }>
|
||||||
|
) {
|
||||||
|
const ja: any = [];
|
||||||
|
|
||||||
// First, walk the list creating the jagged array.
|
// First, walk the list creating the jagged array.
|
||||||
abMap.forEach((val, key, map) => {
|
abMap.forEach((val, key, map) => {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ja[i]) {
|
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
|
||||||
ja[i] = [];
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (0 < i) {
|
if (!ja[i]) {
|
||||||
val.prev = ja[i-1][ja[i - 1].length - 1];
|
ja[i] = [];
|
||||||
}
|
}
|
||||||
ja[i].push(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Now, pull out the longest common subsequence.
|
if (i > 0) {
|
||||||
let lcs: any[] = [];
|
val.prev = ja[i - 1][ja[i - 1].length - 1];
|
||||||
|
}
|
||||||
if (0 < ja.length) {
|
ja[i].push(val);
|
||||||
const n = ja.length - 1;
|
});
|
||||||
lcs = [ja[n][ja[n].length - 1]];
|
|
||||||
|
|
||||||
while (lcs[lcs.length - 1].prev) {
|
|
||||||
lcs.push(lcs[lcs.length - 1].prev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lcs.reverse();
|
// Now, pull out the longest common subsequence.
|
||||||
}
|
let lcs: any[] = [];
|
||||||
|
|
||||||
// "result" is the array used to accumulate the textOld that are deleted, the
|
if (ja.length > 0) {
|
||||||
// lines that are shared between textOld and textNew, and the textNew that were
|
const n = ja.length - 1;
|
||||||
// inserted.
|
lcs = [ja[n][ja[n].length - 1]];
|
||||||
|
|
||||||
const result: any[] = [];
|
|
||||||
let deleted = 0;
|
|
||||||
let inserted = 0;
|
|
||||||
|
|
||||||
// aMove and bMove will contain the lines that don't match, and will be returned
|
while (lcs[lcs.length - 1].prev) {
|
||||||
// for possible searching of lines that moved.
|
lcs.push(lcs[lcs.length - 1].prev);
|
||||||
|
}
|
||||||
const aMove: any[] = [];
|
}
|
||||||
const aMoveIndex: any[] = [];
|
|
||||||
const bMove: any[] = [];
|
return lcs.reverse();
|
||||||
const bMoveIndex: any[] = [];
|
}
|
||||||
|
|
||||||
|
// "result" is the array used to accumulate the textOld that are deleted, the
|
||||||
|
// lines that are shared between textOld and textNew, and the textNew that were
|
||||||
|
// inserted.
|
||||||
|
|
||||||
|
const result: any[] = [];
|
||||||
|
let deleted = 0;
|
||||||
|
let inserted = 0;
|
||||||
|
|
||||||
|
// aMove and bMove will contain the lines that don't match, and will be returned
|
||||||
|
// for possible searching of lines that moved.
|
||||||
|
|
||||||
|
const aMove: any[] = [];
|
||||||
|
const aMoveIndex: any[] = [];
|
||||||
|
const bMove: any[] = [];
|
||||||
|
const bMoveIndex: any[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* addToResult simply pushes the latest value onto the "result" array. This
|
* addToResult simply pushes the latest value onto the "result" array. This
|
||||||
* array captures the diff of the line, aIndex, and bIndex from the textOld
|
* array captures the diff of the line, aIndex, and bIndex from the textOld
|
||||||
* and textNew array.
|
* and textNew array.
|
||||||
* @param aIndex
|
* @param aIndex
|
||||||
* @param bIndex
|
* @param bIndex
|
||||||
*/
|
*/
|
||||||
function addToResult(aIndex: number, bIndex: number) {
|
function addToResult(aIndex: number, bIndex: number) {
|
||||||
if (bIndex < 0) {
|
if (bIndex < 0) {
|
||||||
aMove.push(textOld[aIndex]);
|
aMove.push(textOld[aIndex]);
|
||||||
aMoveIndex.push(result.length);
|
aMoveIndex.push(result.length);
|
||||||
deleted++;
|
deleted += 1;
|
||||||
} else if (aIndex < 0) {
|
} else if (aIndex < 0) {
|
||||||
bMove.push(textNew[bIndex]);
|
bMove.push(textNew[bIndex]);
|
||||||
bMoveIndex.push(result.length);
|
bMoveIndex.push(result.length);
|
||||||
inserted++;
|
inserted += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
line: aIndex >= 0 ? textOld[aIndex] : textNew[bIndex],
|
||||||
|
aIndex,
|
||||||
|
bIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
result.push({
|
|
||||||
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
|
|
||||||
aIndex: aIndex,
|
|
||||||
bIndex: bIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
|
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
|
||||||
* this function might recursively call recurseLCS to further match the lines
|
* this function might recursively call recurseLCS to further match the lines
|
||||||
* between textOld and textNew.
|
* between textOld and textNew.
|
||||||
* @param aLo
|
* @param aLo
|
||||||
* @param aHi
|
* @param aHi
|
||||||
* @param bLo
|
* @param bLo
|
||||||
* @param bHi
|
* @param bHi
|
||||||
*/
|
*/
|
||||||
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
|
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
|
||||||
// Match any lines at the beginning of textOld and textNew.
|
// Match any lines at the beginning of textOld and textNew.
|
||||||
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
|
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
|
||||||
addToResult(aLo++, bLo++);
|
addToResult(aLo++, bLo++);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match any lines at the end of textOld and textNew, but don't place them
|
// Match any lines at the end of textOld and textNew, but don't place them
|
||||||
// in the "result" array just yet, as the lines between these matches at
|
// in the "result" array just yet, as the lines between these matches at
|
||||||
// the beginning and the end need to be analyzed first.
|
// the beginning and the end need to be analyzed first.
|
||||||
|
|
||||||
const aHiTemp = aHi;
|
|
||||||
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
|
|
||||||
aHi--;
|
|
||||||
bHi--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now, check to determine with the remaining lines in the subsequence
|
const aHiTemp = aHi;
|
||||||
// whether there are any unique common lines between textOld and textNew.
|
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
|
||||||
//
|
aHi -= 1;
|
||||||
// If not, add the subsequence to the result (all textOld having been
|
bHi -= 1;
|
||||||
// deleted, and all textNew having been inserted).
|
}
|
||||||
//
|
|
||||||
// If there are unique common lines between textOld and textNew, then let's
|
|
||||||
// recursively perform the patience diff on the subsequence.
|
|
||||||
|
|
||||||
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
|
|
||||||
|
|
||||||
if (uniqueCommonMap.size === 0) {
|
|
||||||
while (aLo <= aHi) {
|
|
||||||
addToResult(aLo++, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (bLo <= bHi) {
|
// Now, check to determine with the remaining lines in the subsequence
|
||||||
addToResult(-1, bLo++);
|
// whether there are any unique common lines between textOld and textNew.
|
||||||
}
|
//
|
||||||
} else {
|
// If not, add the subsequence to the result (all textOld having been
|
||||||
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
|
// deleted, and all textNew having been inserted).
|
||||||
}
|
//
|
||||||
|
// If there are unique common lines between textOld and textNew, then let's
|
||||||
|
// recursively perform the patience diff on the subsequence.
|
||||||
|
|
||||||
|
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
|
||||||
|
|
||||||
|
if (uniqueCommonMap.size === 0) {
|
||||||
|
while (aLo <= aHi) {
|
||||||
|
addToResult(aLo++, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (bLo <= bHi) {
|
||||||
|
addToResult(-1, bLo++);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, let's add the matches at the end to the result.
|
||||||
|
while (aHi < aHiTemp) {
|
||||||
|
addToResult(++aHi, ++bHi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, let's add the matches at the end to the result.
|
|
||||||
while (aHi < aHiTemp) {
|
|
||||||
addToResult(++aHi, ++bHi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* recurseLCS finds the longest common subsequence (LCS) between the arrays
|
* recurseLCS finds the longest common subsequence (LCS) between the arrays
|
||||||
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
|
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
|
||||||
* recursively performs another LCS search (via addSubMatch), until there are
|
* recursively performs another LCS search (via addSubMatch), until there are
|
||||||
* none found, at which point the subsequence is dumped to the result.
|
* none found, at which point the subsequence is dumped to the result.
|
||||||
* @param aLo
|
* @param aLo
|
||||||
* @param aHi
|
* @param aHi
|
||||||
* @param bLo
|
* @param bLo
|
||||||
* @param bHi
|
* @param bHi
|
||||||
* @param uniqueCommonMap
|
* @param uniqueCommonMap
|
||||||
*/
|
*/
|
||||||
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
|
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
|
||||||
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi));
|
const x = longestCommonSubsequence(
|
||||||
|
uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi)
|
||||||
if (x.length === 0) {
|
);
|
||||||
addSubMatch(aLo, aHi, bLo, bHi);
|
|
||||||
} else {
|
|
||||||
if (aLo < x[0].indexA || bLo < x[0].indexB) {
|
|
||||||
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let i;
|
if (x.length === 0) {
|
||||||
for (i = 0; i < x.length - 1; i++) {
|
addSubMatch(aLo, aHi, bLo, bHi);
|
||||||
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
|
} else {
|
||||||
}
|
if (aLo < x[0].indexA || bLo < x[0].indexB) {
|
||||||
|
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
|
||||||
|
}
|
||||||
|
|
||||||
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
|
let i;
|
||||||
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
for (i = 0; i < x.length - 1; i += 1) {
|
||||||
}
|
addSubMatch(x[i].indexA, x[i + 1].indexA - 1, x[i].indexB, x[i + 1].indexB - 1);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
|
||||||
|
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (diffPlusFlag) {
|
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
||||||
return {
|
|
||||||
lines: result,
|
|
||||||
lineCountDeleted: deleted,
|
|
||||||
lineCountInserted: inserted,
|
|
||||||
lineCountMoved: 0,
|
|
||||||
aMove: aMove,
|
|
||||||
aMoveIndex: aMoveIndex,
|
|
||||||
bMove: bMove,
|
|
||||||
bMoveIndex: bMoveIndex,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
if (diffPlusFlag) {
|
||||||
lines: result,
|
return {
|
||||||
lineCountDeleted: deleted,
|
lines: result,
|
||||||
lineCountInserted: inserted,
|
lineCountDeleted: deleted,
|
||||||
lineCountMoved: 0,
|
lineCountInserted: inserted,
|
||||||
};
|
lineCountMoved: 0,
|
||||||
|
aMove,
|
||||||
|
aMoveIndex,
|
||||||
|
bMove,
|
||||||
|
bMoveIndex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lines: result,
|
||||||
|
lineCountDeleted: deleted,
|
||||||
|
lineCountInserted: inserted,
|
||||||
|
lineCountMoved: 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -289,58 +303,54 @@ function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boole
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function patienceDiffPlus(textOld: string[], textNew: string[]) {
|
function patienceDiffPlus(textOld: string[], textNew: string[]) {
|
||||||
|
const difference = patienceDiff(textOld, textNew, true);
|
||||||
|
|
||||||
const difference = patienceDiff(textOld, textNew, true);
|
let aMoveNext = difference.aMove;
|
||||||
|
let aMoveIndexNext = difference.aMoveIndex;
|
||||||
|
let bMoveNext = difference.bMove;
|
||||||
|
let bMoveIndexNext = difference.bMoveIndex;
|
||||||
|
|
||||||
let aMoveNext = difference.aMove;
|
delete difference.aMove;
|
||||||
let aMoveIndexNext = difference.aMoveIndex;
|
delete difference.aMoveIndex;
|
||||||
let bMoveNext = difference.bMove;
|
delete difference.bMove;
|
||||||
let bMoveIndexNext = difference.bMoveIndex;
|
delete difference.bMoveIndex;
|
||||||
|
|
||||||
delete difference.aMove;
|
let lastLineCountMoved;
|
||||||
delete difference.aMoveIndex;
|
|
||||||
delete difference.bMove;
|
|
||||||
delete difference.bMoveIndex;
|
|
||||||
|
|
||||||
let lastLineCountMoved;
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const aMove = aMoveNext;
|
const aMove = aMoveNext;
|
||||||
const aMoveIndex = aMoveIndexNext;
|
const aMoveIndex = aMoveIndexNext;
|
||||||
const bMove = bMoveNext;
|
const bMove = bMoveNext;
|
||||||
const bMoveIndex = bMoveIndexNext;
|
const bMoveIndex = bMoveIndexNext;
|
||||||
|
|
||||||
aMoveNext = [];
|
aMoveNext = [];
|
||||||
aMoveIndexNext = [];
|
aMoveIndexNext = [];
|
||||||
bMoveNext = [];
|
bMoveNext = [];
|
||||||
bMoveIndexNext = [];
|
bMoveIndexNext = [];
|
||||||
|
|
||||||
const subDiff = patienceDiff(aMove!, bMove!);
|
const subDiff = patienceDiff(aMove!, bMove!);
|
||||||
|
|
||||||
lastLineCountMoved = difference.lineCountMoved;
|
|
||||||
|
|
||||||
subDiff.lines.forEach((v, i) => {
|
lastLineCountMoved = difference.lineCountMoved;
|
||||||
|
|
||||||
if (0 <= v.aIndex && 0 <= v.bIndex) {
|
subDiff.lines.forEach((v, i) => {
|
||||||
|
if (v.aIndex >= 0 && v.bIndex >= 0) {
|
||||||
difference.lines[aMoveIndex![v.aIndex]].moved = true;
|
difference.lines[aMoveIndex![v.aIndex]].moved = true;
|
||||||
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
|
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
|
||||||
difference.lines[bMoveIndex![v.bIndex]].moved = true;
|
difference.lines[bMoveIndex![v.bIndex]].moved = true;
|
||||||
difference.lineCountInserted--;
|
difference.lineCountInserted -= 1;
|
||||||
difference.lineCountDeleted--;
|
difference.lineCountDeleted -= 1;
|
||||||
difference.lineCountMoved++;
|
difference.lineCountMoved += 1;
|
||||||
} else if (v.bIndex < 0) {
|
} else if (v.bIndex < 0) {
|
||||||
aMoveNext!.push(aMove![v.aIndex]);
|
aMoveNext!.push(aMove![v.aIndex]);
|
||||||
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
|
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
|
||||||
} else {
|
} else {
|
||||||
bMoveNext!.push(bMove![v.bIndex]);
|
bMoveNext!.push(bMove![v.bIndex]);
|
||||||
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
|
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} while (0 < difference.lineCountMoved - lastLineCountMoved);
|
} while (difference.lineCountMoved - lastLineCountMoved > 0);
|
||||||
|
|
||||||
return difference;
|
|
||||||
|
|
||||||
|
return difference;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default patienceDiff;
|
export default patienceDiff;
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ function timeSince(date: Date) {
|
|||||||
let interval = seconds / 31536000;
|
let interval = seconds / 31536000;
|
||||||
|
|
||||||
if (interval > 1) {
|
if (interval > 1) {
|
||||||
return Math.floor(interval) + ' years ago';
|
return `${Math.floor(interval) } years ago`;
|
||||||
}
|
}
|
||||||
interval = seconds / 2592000;
|
interval = seconds / 2592000;
|
||||||
if (interval > 1) {
|
if (interval > 1) {
|
||||||
return Math.floor(interval) + ' months ago';
|
return `${Math.floor(interval) } months ago`;
|
||||||
}
|
}
|
||||||
interval = seconds / 86400;
|
interval = seconds / 86400;
|
||||||
if (interval > 1) {
|
if (interval > 1) {
|
||||||
return Math.floor(interval) + ' days ago';
|
return `${Math.floor(interval) } days ago`;
|
||||||
}
|
}
|
||||||
interval = seconds / 3600;
|
interval = seconds / 3600;
|
||||||
if (interval > 1) {
|
if (interval > 1) {
|
||||||
return Math.floor(interval) + ' hours ago';
|
return `${Math.floor(interval) } hours ago`;
|
||||||
}
|
}
|
||||||
interval = seconds / 60;
|
interval = seconds / 60;
|
||||||
if (interval > 1) {
|
if (interval > 1) {
|
||||||
return Math.floor(interval) + ' minutes ago';
|
return `${Math.floor(interval) } minutes ago`;
|
||||||
}
|
}
|
||||||
return Math.floor(seconds) + ' seconds ago';
|
return `${Math.floor(seconds) } seconds ago`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default timeSince;
|
export default timeSince;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
interface usePopUpProps {
|
interface UsePopUpProps {
|
||||||
name: Readonly<string>;
|
name: Readonly<string>;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
@@ -10,18 +10,18 @@ interface usePopUpProps {
|
|||||||
* checks which type of inputProps were given and converts them into key-names
|
* checks which type of inputProps were given and converts them into key-names
|
||||||
* SIDENOTE: On inputting give it as const and not string with (as const)
|
* SIDENOTE: On inputting give it as const and not string with (as const)
|
||||||
*/
|
*/
|
||||||
type usePopUpState<T extends Readonly<string[]> | usePopUpProps[]> = {
|
type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
|
||||||
[P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: {
|
[P in T extends UsePopUpProps[] ? T[number]['name'] : T[number]]: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
|
interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
|
||||||
popUp: usePopUpState<T>;
|
popUp: UsePopUpState<T>;
|
||||||
handlePopUpOpen: (popUpName: keyof usePopUpState<T>, data?: unknown) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
|
||||||
handlePopUpClose: (popUpName: keyof usePopUpState<T>) => void;
|
handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
|
||||||
handlePopUpToggle: (popUpName: keyof usePopUpState<T>) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<T>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,34 +29,31 @@ interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
|
|||||||
* Provides api to open,close,toggle and also store temporary data for the popUp
|
* Provides api to open,close,toggle and also store temporary data for the popUp
|
||||||
* @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}]
|
* @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}]
|
||||||
*/
|
*/
|
||||||
export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>(
|
export const usePopUp = <T extends Readonly<string[]> | UsePopUpProps[]>(
|
||||||
popUpNames: T
|
popUpNames: T
|
||||||
): usePopUpReturn<T> => {
|
): UsePopUpReturn<T> => {
|
||||||
const [popUp, setPopUp] = useState<usePopUpState<T>>(
|
const [popUp, setPopUp] = useState<UsePopUpState<T>>(
|
||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
popUpNames.map((popUpName) =>
|
popUpNames.map((popUpName) =>
|
||||||
typeof popUpName === 'string'
|
typeof popUpName === 'string'
|
||||||
? [popUpName, { isOpen: false }]
|
? [popUpName, { isOpen: false }]
|
||||||
: [popUpName.name, { isOpen: popUpName.isOpen }]
|
: [popUpName.name, { isOpen: popUpName.isOpen }]
|
||||||
) // convert into an array of [[popUpName,state]] then into Object
|
) // convert into an array of [[popUpName,state]] then into Object
|
||||||
) as usePopUpState<T> // to override generic string return type of the function
|
) as UsePopUpState<T> // to override generic string return type of the function
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePopUpOpen = useCallback(
|
const handlePopUpOpen = useCallback((popUpName: keyof UsePopUpState<T>, data?: unknown) => {
|
||||||
(popUpName: keyof usePopUpState<T>, data?: unknown) => {
|
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: true, data } }));
|
||||||
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } }));
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePopUpClose = useCallback((popUpName: keyof usePopUpState<T>) => {
|
|
||||||
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } }));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => {
|
const handlePopUpClose = useCallback((popUpName: keyof UsePopUpState<T>) => {
|
||||||
setPopUp((popUp) => ({
|
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: false } }));
|
||||||
...popUp,
|
}, []);
|
||||||
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
|
|
||||||
|
const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState<T>) => {
|
||||||
|
setPopUp((oldState) => ({
|
||||||
|
...oldState,
|
||||||
|
[popUpName]: { isOpen: !oldState[popUpName].isOpen }
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -64,6 +61,6 @@ export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>(
|
|||||||
popUp,
|
popUp,
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpClose,
|
handlePopUpClose,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function Custom404() {
|
|||||||
height={554}
|
height={554}
|
||||||
width={942}
|
width={942}
|
||||||
alt='infisical dragon - page not found'
|
alt='infisical dragon - page not found'
|
||||||
></Image>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
|
import Layout from '@app/components/basic/Layout';
|
||||||
|
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||||
|
import RouteGuard from '@app/components/RouteGuard';
|
||||||
|
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||||
|
import { publicPaths } from '@app/const';
|
||||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||||
|
|
||||||
import Layout from '~/components/basic/Layout';
|
|
||||||
import NotificationProvider from '~/components/context/Notifications/NotificationProvider';
|
|
||||||
import RouteGuard from '~/components/RouteGuard';
|
|
||||||
import { publicPaths } from '~/const';
|
|
||||||
import Telemetry from '~/utilities/telemetry/Telemetry';
|
|
||||||
|
|
||||||
import '@fortawesome/fontawesome-svg-core/styles.css';
|
import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||||
import '../styles/globals.css';
|
import '../styles/globals.css';
|
||||||
|
|
||||||
@@ -19,18 +19,14 @@ type NextAppProp = AppProps & {
|
|||||||
Component: AppProps['Component'] & { requireAuth: boolean };
|
Component: AppProps['Component'] & { requireAuth: boolean };
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = ({
|
const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => {
|
||||||
Component,
|
|
||||||
pageProps,
|
|
||||||
...appProps
|
|
||||||
}: NextAppProp): JSX.Element => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedLang = localStorage.getItem('lang');
|
const storedLang = localStorage.getItem('lang');
|
||||||
if (router.locale ?? 'en' !== storedLang ?? 'en') {
|
if (router.locale ?? storedLang !== 'en' ?? 'en') {
|
||||||
router.push(router.asPath, router.asPath, {
|
router.push(router.asPath, router.asPath, {
|
||||||
locale: storedLang ?? 'en',
|
locale: storedLang ?? 'en'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [router.locale, router.pathname]);
|
}, [router.locale, router.pathname]);
|
||||||
@@ -54,7 +50,7 @@ const App = ({
|
|||||||
|
|
||||||
// If it's one of these routes, don't add the layout (e.g., these routes are external)
|
// If it's one of these routes, don't add the layout (e.g., these routes are external)
|
||||||
if (
|
if (
|
||||||
publicPaths.includes('/' + appProps.router.pathname.split('/')[1]) ||
|
publicPaths.includes(`/${appProps.router.pathname.split('/')[1]}`) ||
|
||||||
!Component.requireAuth
|
!Component.requireAuth
|
||||||
) {
|
) {
|
||||||
return <Component {...pageProps} />;
|
return <Component {...pageProps} />;
|
||||||
@@ -73,8 +69,7 @@ const App = ({
|
|||||||
|
|
||||||
export default appWithTranslation(App);
|
export default appWithTranslation(App);
|
||||||
|
|
||||||
{
|
/* <Script
|
||||||
/* <Script
|
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1"
|
src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1"
|
||||||
strategy="afterInteractive"
|
strategy="afterInteractive"
|
||||||
/>
|
/>
|
||||||
@@ -87,4 +82,3 @@ strategy="afterInteractive"
|
|||||||
gtag('config', 'G-DQ1XLJJGG1');
|
gtag('config', 'G-DQ1XLJJGG1');
|
||||||
`}
|
`}
|
||||||
</Script> */
|
</Script> */
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import Button from '@app/components/basic/buttons/Button';
|
||||||
|
import EventFilter from '@app/components/basic/EventFilter';
|
||||||
|
import NavHeader from '@app/components/navigation/NavHeader';
|
||||||
|
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||||
import ActivitySideBar from '@app/ee/components/ActivitySideBar';
|
import ActivitySideBar from '@app/ee/components/ActivitySideBar';
|
||||||
|
|
||||||
import Button from '~/components/basic/buttons/Button';
|
|
||||||
import EventFilter from '~/components/basic/EventFilter';
|
|
||||||
import NavHeader from '~/components/navigation/NavHeader';
|
|
||||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
|
||||||
|
|
||||||
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
|
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
|
||||||
import ActivityTable from '../../ee/components/ActivityTable';
|
import ActivityTable from '../../ee/components/ActivityTable';
|
||||||
|
|
||||||
|
interface LogData {
|
||||||
interface logData {
|
|
||||||
_id: string;
|
_id: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -25,17 +23,17 @@ interface logData {
|
|||||||
name: string;
|
name: string;
|
||||||
payload: {
|
payload: {
|
||||||
secretVersions: string[];
|
secretVersions: string[];
|
||||||
}
|
};
|
||||||
}[]
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PayloadProps {
|
interface PayloadProps {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
secretVersions: string[];
|
secretVersions: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface logDataPoint {
|
interface LogDataPoint {
|
||||||
_id: string;
|
_id: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -50,11 +48,11 @@ interface logDataPoint {
|
|||||||
export default function Activity() {
|
export default function Activity() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [eventChosen, setEventChosen] = useState('');
|
const [eventChosen, setEventChosen] = useState('');
|
||||||
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
|
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [currentOffset, setCurrentOffset] = useState(0);
|
const [currentOffset, setCurrentOffset] = useState(0);
|
||||||
const currentLimit = 10;
|
const currentLimit = 10;
|
||||||
const [currentSidebarAction, toggleSidebar] = useState<string>()
|
const [currentSidebarAction, toggleSidebar] = useState<string>();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// this use effect updates the data in case of a new filter being added
|
// this use effect updates the data in case of a new filter being added
|
||||||
@@ -62,25 +60,29 @@ export default function Activity() {
|
|||||||
setCurrentOffset(0);
|
setCurrentOffset(0);
|
||||||
const getLogData = async () => {
|
const getLogData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen })
|
const tempLogsData = await getProjectLogs({
|
||||||
setLogsData(tempLogsData.map((log: logData) => {
|
workspaceId: String(router.query.id),
|
||||||
return {
|
offset: 0,
|
||||||
_id: log._id,
|
limit: currentLimit,
|
||||||
channel: log.channel,
|
userId: '',
|
||||||
createdAt: log.createdAt,
|
actionNames: eventChosen
|
||||||
ipAddress: log.ipAddress,
|
});
|
||||||
user: log.user.email,
|
setLogsData(
|
||||||
payload: log.actions.map(action => {
|
tempLogsData.map((log: LogData) => ({
|
||||||
return {
|
_id: log._id,
|
||||||
_id: action._id,
|
channel: log.channel,
|
||||||
name: action.name,
|
createdAt: log.createdAt,
|
||||||
secretVersions: action.payload.secretVersions
|
ipAddress: log.ipAddress,
|
||||||
}
|
user: log.user.email,
|
||||||
})
|
payload: log.actions.map((action) => ({
|
||||||
}
|
_id: action._id,
|
||||||
}))
|
name: action.name,
|
||||||
|
secretVersions: action.payload.secretVersions
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
getLogData();
|
getLogData();
|
||||||
}, [eventChosen]);
|
}, [eventChosen]);
|
||||||
|
|
||||||
@@ -88,58 +90,64 @@ export default function Activity() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getLogData = async () => {
|
const getLogData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen })
|
const tempLogsData = await getProjectLogs({
|
||||||
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
|
workspaceId: String(router.query.id),
|
||||||
return {
|
offset: currentOffset,
|
||||||
_id: log._id,
|
limit: currentLimit,
|
||||||
channel: log.channel,
|
userId: '',
|
||||||
createdAt: log.createdAt,
|
actionNames: eventChosen
|
||||||
ipAddress: log.ipAddress,
|
});
|
||||||
user: log.user.email,
|
setLogsData(
|
||||||
payload: log.actions.map(action => {
|
logsData.concat(
|
||||||
return {
|
tempLogsData.map((log: LogData) => ({
|
||||||
|
_id: log._id,
|
||||||
|
channel: log.channel,
|
||||||
|
createdAt: log.createdAt,
|
||||||
|
ipAddress: log.ipAddress,
|
||||||
|
user: log.user.email,
|
||||||
|
payload: log.actions.map((action) => ({
|
||||||
_id: action._id,
|
_id: action._id,
|
||||||
name: action.name,
|
name: action.name,
|
||||||
secretVersions: action.payload.secretVersions
|
secretVersions: action.payload.secretVersions
|
||||||
}
|
}))
|
||||||
})
|
}))
|
||||||
}
|
)
|
||||||
})))
|
);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
getLogData();
|
getLogData();
|
||||||
}, [currentLimit, currentOffset]);
|
}, [currentLimit, currentOffset]);
|
||||||
|
|
||||||
const loadMoreLogs = () => {
|
const loadMoreLogs = () => {
|
||||||
setCurrentOffset(currentOffset + currentLimit);
|
setCurrentOffset(currentOffset + currentLimit);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
|
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
|
||||||
<NavHeader pageName="Project Activity" isProjectRelated={true} />
|
<NavHeader pageName="Project Activity" isProjectRelated />
|
||||||
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
|
{currentSidebarAction && (
|
||||||
|
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||||
|
)}
|
||||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||||
<div className="flex flex-row justify-start items-center text-3xl">
|
<div className="flex flex-row justify-start items-center text-3xl">
|
||||||
<p className="font-semibold mr-4 text-bunker-100">{t("activity:title")}</p>
|
<p className="font-semibold mr-4 text-bunker-100">{t('activity:title')}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mr-4 text-base text-gray-400">
|
<p className="mr-4 text-base text-gray-400">{t('activity:subtitle')}</p>
|
||||||
{t("activity:subtitle")}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="px-6 h-8 mt-2">
|
<div className="px-6 h-8 mt-2">
|
||||||
<EventFilter
|
<EventFilter selected={eventChosen} select={setEventChosen} />
|
||||||
selected={eventChosen}
|
|
||||||
select={setEventChosen}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<ActivityTable
|
<ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
|
||||||
data={logsData}
|
<div className="flex justify-center w-full mb-6">
|
||||||
toggleSidebar={toggleSidebar}
|
<div className="items-center w-60">
|
||||||
isLoading={isLoading}
|
<Button
|
||||||
/>
|
text={String(t('common:view-more'))}
|
||||||
<div className='flex justify-center w-full mb-6'>
|
textDisabled={String(t('common:end-of-history'))}
|
||||||
<div className='items-center w-60'>
|
active={logsData.length % 10 === 0}
|
||||||
<Button text={String(t("common:view-more"))} textDisabled={String(t("common:end-of-history"))} active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
|
onButtonPressed={loadMoreLogs}
|
||||||
|
size="md"
|
||||||
|
color="mineshaft"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,4 +156,4 @@ export default function Activity() {
|
|||||||
|
|
||||||
Activity.requireAuth = true;
|
Activity.requireAuth = true;
|
||||||
|
|
||||||
export const getServerSideProps = getTranslatedServerSideProps(["activity", "common"]);
|
export const getServerSideProps = getTranslatedServerSideProps(['activity', 'common']);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -12,11 +12,8 @@ interface Props {
|
|||||||
* @param {string} obj.expiresIn - how soon the API key expires in ms
|
* @param {string} obj.expiresIn - how soon the API key expires in ms
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const addAPIKey = ({
|
const addAPIKey = ({ name, expiresIn }: Props) =>
|
||||||
name,
|
SecurityClient.fetchCall('/api/v2/api-key/', {
|
||||||
expiresIn,
|
|
||||||
}: Props) => {
|
|
||||||
return SecurityClient.fetchCall('/api/v2/api-key/', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
@@ -26,12 +23,11 @@ const addAPIKey = ({
|
|||||||
expiresIn
|
expiresIn
|
||||||
})
|
})
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return (await res.json());
|
return res.json();
|
||||||
} else {
|
|
||||||
console.log('Failed to add API key');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to add API key');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default addAPIKey;
|
export default addAPIKey;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
apiKeyId: string;
|
apiKeyId: string;
|
||||||
@@ -10,21 +10,18 @@ interface Props {
|
|||||||
* @param {string} obj.apiKeyId - id of the API key to delete
|
* @param {string} obj.apiKeyId - id of the API key to delete
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const deleteAPIKey = ({
|
const deleteAPIKey = ({ apiKeyId }: Props) =>
|
||||||
apiKeyId
|
SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
|
||||||
}: Props) => {
|
|
||||||
return SecurityClient.fetchCall('/api/v2/api-key/' + apiKeyId, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
|
||||||
}).then(async (res) => {
|
|
||||||
if (res && res.status == 200) {
|
|
||||||
return (await res.json());
|
|
||||||
} else {
|
|
||||||
console.log('Failed to delete API key');
|
|
||||||
}
|
}
|
||||||
|
}).then(async (res) => {
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
console.log('Failed to delete API key');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default deleteAPIKey;
|
export default deleteAPIKey;
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This route gets API keys for the user
|
* This route gets API keys for the user
|
||||||
* @param {*} param0
|
* @param {*} param0
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getAPIKeys = () => {
|
const getAPIKeys = () =>
|
||||||
return SecurityClient.fetchCall(
|
SecurityClient.fetchCall('/api/v2/api-key', {
|
||||||
'/api/v2/api-key',
|
method: 'GET',
|
||||||
{
|
headers: {
|
||||||
method: 'GET',
|
'Content-Type': 'application/json'
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return (await res.json()).apiKeyData;
|
return (await res.json()).apiKeyData;
|
||||||
} else {
|
|
||||||
console.log('Failed to get API keys');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to get API keys');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default getAPIKeys;
|
export default getAPIKeys;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
@@ -14,34 +14,26 @@ interface Props {
|
|||||||
* @param {*} clientPublicKey
|
* @param {*} clientPublicKey
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const changePassword2 = ({
|
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
|
||||||
encryptedPrivateKey,
|
SecurityClient.fetchCall('/api/v1/password/change-password', {
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
clientProof
|
|
||||||
}: Props) => {
|
|
||||||
return SecurityClient.fetchCall('/api/v1/password/change-password', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
clientProof: clientProof,
|
clientProof,
|
||||||
encryptedPrivateKey: encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: iv,
|
iv,
|
||||||
tag: tag,
|
tag,
|
||||||
salt: salt,
|
salt,
|
||||||
verifier: verifier
|
verifier
|
||||||
})
|
})
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res && res.status == 200) {
|
if (res && res.status === 200) {
|
||||||
return res;
|
return res;
|
||||||
} else {
|
|
||||||
console.log('Failed to change the password');
|
|
||||||
}
|
}
|
||||||
|
console.log('Failed to change the password');
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default changePassword2;
|
export default changePassword2;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function is used to check if the user is authenticated.
|
* This function is used to check if the user is authenticated.
|
||||||
* To do that, we get their tokens from cookies, and verify if they are good.
|
* To do that, we get their tokens from cookies, and verify if they are good.
|
||||||
*/
|
*/
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () =>
|
||||||
return SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
|
SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}).then((res) => res);
|
}).then((res) => res);
|
||||||
};
|
|
||||||
|
|
||||||
export default checkAuth;
|
export default checkAuth;
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ interface Props {
|
|||||||
* @param {string} obj.code
|
* @param {string} obj.code
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const checkEmailVerificationCode = ({ email, code }: Props) => {
|
const checkEmailVerificationCode = ({ email, code }: Props) => fetch('/api/v1/signup/email/verify', {
|
||||||
return fetch('/api/v1/signup/email/verify', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email,
|
email,
|
||||||
code: code
|
code
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default checkEmailVerificationCode;
|
export default checkEmailVerificationCode;
|
||||||
|
|||||||
@@ -41,12 +41,11 @@ const completeAccountInformationSignup = ({
|
|||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
token
|
token
|
||||||
}: Props) => {
|
}: Props) => fetch('/api/v1/signup/complete-account/signup', {
|
||||||
return fetch('/api/v1/signup/complete-account/signup', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: 'Bearer ' + token
|
Authorization: `Bearer ${ token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email,
|
email,
|
||||||
@@ -61,6 +60,5 @@ const completeAccountInformationSignup = ({
|
|||||||
verifier
|
verifier
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default completeAccountInformationSignup;
|
export default completeAccountInformationSignup;
|
||||||
|
|||||||
@@ -38,25 +38,23 @@ const completeAccountInformationSignupInvite = ({
|
|||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
token
|
token
|
||||||
}: Props) => {
|
}: Props) => fetch('/api/v1/signup/complete-account/invite', {
|
||||||
return fetch('/api/v1/signup/complete-account/invite', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
Authorization: 'Bearer ' + token
|
Authorization: `Bearer ${ token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email,
|
email,
|
||||||
firstName: firstName,
|
firstName,
|
||||||
lastName: lastName,
|
lastName,
|
||||||
publicKey: publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey: ciphertext,
|
||||||
iv: iv,
|
iv,
|
||||||
tag: tag,
|
tag,
|
||||||
salt: salt,
|
salt,
|
||||||
verifier: verifier
|
verifier
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default completeAccountInformationSignupInvite;
|
export default completeAccountInformationSignupInvite;
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email,
|
email,
|
||||||
code: code
|
code
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if (response?.status === 200) {
|
if (response?.status === 200) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import SecurityClient from '~/utilities/SecurityClient';
|
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKey: string;
|
||||||
@@ -27,19 +27,19 @@ const issueBackupPrivateKey = ({
|
|||||||
salt,
|
salt,
|
||||||
verifier,
|
verifier,
|
||||||
clientProof
|
clientProof
|
||||||
}: Props) => {
|
}: Props) =>
|
||||||
return SecurityClient.fetchCall('/api/v1/password/backup-private-key', {
|
SecurityClient.fetchCall('/api/v1/password/backup-private-key', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
clientProof: clientProof,
|
clientProof,
|
||||||
encryptedPrivateKey: encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: iv,
|
iv,
|
||||||
tag: tag,
|
tag,
|
||||||
salt: salt,
|
salt,
|
||||||
verifier: verifier
|
verifier
|
||||||
})
|
})
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
if (res?.status !== 200) {
|
if (res?.status !== 200) {
|
||||||
@@ -47,6 +47,5 @@ const issueBackupPrivateKey = ({
|
|||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
export default issueBackupPrivateKey;
|
export default issueBackupPrivateKey;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email,
|
email,
|
||||||
clientPublicKey,
|
clientPublicKey,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,24 +13,24 @@ interface Login2Response {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const login2 = async (email: string, clientProof: string) => {
|
const login2 = async (email: string, clientProof: string) => {
|
||||||
const response = await fetch("/api/v1/auth/login2", {
|
const response = await fetch('/api/v1/auth/login2', {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email: email,
|
email,
|
||||||
clientProof,
|
clientProof
|
||||||
}),
|
}),
|
||||||
credentials: "include",
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
// need precise error handling about the status code
|
// need precise error handling about the status code
|
||||||
if (response.status == 200) {
|
if (response.status === 200) {
|
||||||
const data = (await response.json()) as unknown as Login2Response;
|
const data = (await response.json()) as unknown as Login2Response;
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Password verification failed");
|
throw new Error('Password verification failed');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default login2;
|
export default login2;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user