mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
Merge pull request #235 from akhilmhdh/chore/eslint-frontend
airbnb eslint and sorting
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)$"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
73
frontend/.eslintrc.js
Normal file
73
frontend/.eslintrc.js
Normal file
@ -0,0 +1,73 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: ['airbnb', 'airbnb-typescript', 'airbnb/hooks', 'plugin:react/recommended', 'prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json',
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
},
|
||||
tsconfigRootDir: __dirname
|
||||
},
|
||||
plugins: ['react', 'prettier', 'simple-import-sort', 'import'],
|
||||
rules: {
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||
// TODO: This rule will be switched ON after complete revamp of frontend
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'no-console': 'off',
|
||||
'arrow-body-style': 'off',
|
||||
'no-underscore-dangle': ['error', { allow: ['_id'] }],
|
||||
'jsx-a11y/anchor-is-valid': 'off', // all those <a> tags must be converted to label or a p component
|
||||
//
|
||||
'react/require-default-props': 'off',
|
||||
'react/jsx-filename-extension': [1, { extensions: ['.tsx', '.ts'] }],
|
||||
// TODO: turn this rule ON after migration. everything should use arrow functions
|
||||
'react/function-component-definition': [
|
||||
0,
|
||||
{
|
||||
namedComponents: 'arrow-function'
|
||||
}
|
||||
],
|
||||
'react/no-unknown-property': ['error', { ignore: ['jsx'] }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'simple-import-sort/exports': 'warn',
|
||||
'simple-import-sort/imports': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
// Node.js builtins. You could also generate this regex if you use a `.js` config.
|
||||
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
|
||||
// Note that if you use the `node:` prefix for Node.js builtins,
|
||||
// you can avoid this complexity: You can simply use "^node:".
|
||||
[
|
||||
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
|
||||
],
|
||||
// Packages `react` related packages
|
||||
['^react', '^next', '^@?\\w'],
|
||||
['^@app'],
|
||||
// Internal packages.
|
||||
['^~(/.*|$)'],
|
||||
// Relative imports
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
// Style imports.
|
||||
['^.+\\.?(css|scss)$']
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
7
frontend/.prettierrc
Normal file
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",
|
||||
"start": "next start",
|
||||
"start:docker": "next build && next start",
|
||||
"lint": "next lint",
|
||||
"lint": "eslint --fix --ext js,ts,tsx ./src",
|
||||
"type-check": "tsc --project tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -62,14 +62,23 @@
|
||||
"@types/jsrp": "^0.2.4",
|
||||
"@types/node": "18.11.9",
|
||||
"@types/react": "^18.0.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "^8.29.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^13.0.5",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.2",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.32.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||
"postcss": "^8.4.14",
|
||||
"prettier": "^2.8.3",
|
||||
"tailwindcss": "^3.1.4",
|
||||
"typescript": "^4.9.3"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { publicPaths } from '~/const';
|
||||
import checkAuth from '~/pages/api/auth/CheckAuth';
|
||||
import { publicPaths } from '@app/const';
|
||||
import checkAuth from '@app/pages/api/auth/CheckAuth';
|
||||
|
||||
// #TODO: finish spinner only when the data loads fully
|
||||
// #TODO: Redirect somewhere if the page does not exist
|
||||
@ -13,8 +13,36 @@ type Prop = {
|
||||
|
||||
export default function RouteGuard({ children }: Prop): JSX.Element {
|
||||
const router = useRouter();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [authorized, setAuthorized] = useState(false);
|
||||
|
||||
/**
|
||||
* redirect to login page if accessing a private page and not logged in
|
||||
*/
|
||||
async function authCheck(url: string) {
|
||||
// Make sure that we don't redirect when the user is on the following pages.
|
||||
const path = `/${url.split('?')[0].split('/')[1]}`;
|
||||
|
||||
// Check if the user is authenticated
|
||||
const response = await checkAuth();
|
||||
// #TODO: figure our why sometimes it doesn't output a response
|
||||
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
|
||||
if (!publicPaths.includes(path)) {
|
||||
try {
|
||||
if (response.status !== 200) {
|
||||
router.push('/login');
|
||||
console.log('Unauthorized to access.');
|
||||
setAuthorized(false);
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
console.log('Authorized to access.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error (probably the authCheck route is stuck again...):', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// on initial load - run auth check
|
||||
(async () => {
|
||||
@ -40,35 +68,5 @@ export default function RouteGuard({ children }: Prop): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* redirect to login page if accessing a private page and not logged in
|
||||
*/
|
||||
async function authCheck(url: string) {
|
||||
// Make sure that we don't redirect when the user is on the following pages.
|
||||
const path = '/' + url.split('?')[0].split('/')[1];
|
||||
|
||||
// Check if the user is authenticated
|
||||
const response = await checkAuth();
|
||||
// #TODO: figure our why sometimes it doesn't output a response
|
||||
// ANS(akhilmhdh): Because inside the security client the await token() doesn't have try/catch
|
||||
if (!publicPaths.includes(path)) {
|
||||
try {
|
||||
if (response.status !== 200) {
|
||||
router.push('/login');
|
||||
console.log('Unauthorized to access.');
|
||||
setAuthorized(false);
|
||||
} else {
|
||||
setAuthorized(true);
|
||||
console.log('Authorized to access.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'Error (probably the authCheck route is stuck again...):',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children as JSX.Element;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export const initPostHog = () => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
posthog.init(POSTHOG_API_KEY, {
|
||||
api_host: POSTHOG_HOST
|
||||
});
|
||||
@ -19,4 +19,6 @@ export const initPostHog = () => {
|
||||
} catch (e) {
|
||||
console.log("posthog err", e)
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
export default function Error({ text }: { text: string }): JSX.Element {
|
||||
const Error = ({ text }: { text: string }): JSX.Element => {
|
||||
return (
|
||||
<div className='relative flex flex-row justify-center m-auto items-center w-fit rounded-full'>
|
||||
<FontAwesomeIcon
|
||||
icon={faExclamationTriangle}
|
||||
className='text-red mt-1.5 mb-2 mx-2'
|
||||
/>
|
||||
{text && (
|
||||
<p className='relative top-0 text-red mr-2 text-sm py-1'>{text}</p>
|
||||
)}
|
||||
<div className="relative flex flex-row justify-center m-auto items-center w-fit rounded-full">
|
||||
<FontAwesomeIcon icon={faExclamationTriangle} className="text-red mt-1.5 mb-2 mx-2" />
|
||||
{text && <p className="relative top-0 text-red mr-2 text-sm py-1">{text}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Error;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React, { Fragment } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
faAngleDown,
|
||||
faEye,
|
||||
@ -42,27 +41,20 @@ const eventOptions = [
|
||||
* @param {string} obj.selected - the event that is currently selected
|
||||
* @param {function} obj.select - an action that happens when an item is selected
|
||||
*/
|
||||
export default function EventFilter({
|
||||
selected,
|
||||
select
|
||||
}: ListBoxProps): JSX.Element {
|
||||
const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Listbox value={t("activity:event." + selected)} onChange={select}>
|
||||
<Listbox value={t(`activity:event.${selected}`)} onChange={select}>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="bg-mineshaft-800 hover:bg-mineshaft-700 duration-200 cursor-pointer rounded-md h-10 flex items-center justify-between pl-4 pr-2 w-52 text-bunker-200 text-sm">
|
||||
{selected != '' ? (
|
||||
<p className="select-none text-bunker-100">{t("activity:event." + selected)}</p>
|
||||
{selected !== '' ? (
|
||||
<p className="select-none text-bunker-100">{t(`activity:event.${selected}`)}</p>
|
||||
) : (
|
||||
<p className="select-none">{String(t("common:select-event"))}</p>
|
||||
<p className="select-none">{String(t('common:select-event'))}</p>
|
||||
)}
|
||||
{selected != '' ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className="pl-2 w-2 p-2"
|
||||
onClick={() => select('')}
|
||||
/>
|
||||
{selected !== '' ? (
|
||||
<FontAwesomeIcon icon={faX} className="pl-2 w-2 p-2" onClick={() => select('')} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
|
||||
)}
|
||||
@ -74,33 +66,29 @@ export default function EventFilter({
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="border border-mineshaft-700 z-50 w-52 p-1 absolute mt-1 max-h-60 overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{eventOptions.map((event, id) => {
|
||||
return (
|
||||
<Listbox.Option
|
||||
key={id}
|
||||
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
|
||||
selected == t("activity:event." + event.name) && 'bg-mineshaft-700'
|
||||
}`}
|
||||
value={event.name}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate ${
|
||||
selected ? 'font-semibold' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
|
||||
{t("activity:event." + event.name)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
);
|
||||
})}
|
||||
{eventOptions.map((event, id) => (
|
||||
<Listbox.Option
|
||||
key={`${event.name}.${id + 1}`}
|
||||
className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${
|
||||
selected === t(`activity:event.${event.name}`) && 'bg-mineshaft-700'
|
||||
}`}
|
||||
value={event.name}
|
||||
>
|
||||
{({ selected: isSelected }) => (
|
||||
<span
|
||||
className={`block truncate ${isSelected ? 'font-semibold' : 'font-normal'}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={event.icon} className="pr-4" />{' '}
|
||||
{t(`activity:event.${event.name}`)}
|
||||
</span>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default EventFilter;
|
||||
|
@ -5,13 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
|
||||
interface InputFieldProps {
|
||||
static?: boolean;
|
||||
isStatic?: boolean;
|
||||
label: string;
|
||||
type: string;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
isRequired: boolean;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
text?: string;
|
||||
name?: string;
|
||||
@ -20,46 +19,54 @@ interface InputFieldProps {
|
||||
onChangeHandler: (value: string) => void;
|
||||
}
|
||||
|
||||
const InputField = (
|
||||
props: InputFieldProps &
|
||||
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>
|
||||
) => {
|
||||
const InputField = ({
|
||||
isRequired,
|
||||
label,
|
||||
onChangeHandler,
|
||||
type,
|
||||
value,
|
||||
autoComplete,
|
||||
blurred,
|
||||
error,
|
||||
errorText,
|
||||
id,
|
||||
name,
|
||||
placeholder,
|
||||
isStatic,
|
||||
text
|
||||
}: InputFieldProps & Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>) => {
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
if (props.static === true) {
|
||||
if (isStatic === true) {
|
||||
return (
|
||||
<div className='flex flex-col my-2 md:my-4 justify-center w-full max-w-md'>
|
||||
<p className='text-sm font-semibold text-gray-400 mb-0.5'>
|
||||
{props.label}
|
||||
</p>
|
||||
{props.text && (
|
||||
<p className='text-xs text-gray-400 mb-2'>{props.text}</p>
|
||||
)}
|
||||
<div className="flex flex-col my-2 md:my-4 justify-center w-full max-w-md">
|
||||
<p className="text-sm font-semibold text-gray-400 mb-0.5">{label}</p>
|
||||
{text && <p className="text-xs text-gray-400 mb-2">{text}</p>}
|
||||
<input
|
||||
onChange={(e) => props.onChangeHandler(e.target.value)}
|
||||
type={props.type}
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
required={props.isRequired}
|
||||
className='bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none'
|
||||
name={props.name}
|
||||
onChange={(e) => onChangeHandler(e.target.value)}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
required={isRequired}
|
||||
className="bg-bunker-800 text-gray-400 border border-gray-600 rounded-md text-md p-2 w-full min-w-16 outline-none"
|
||||
name={name}
|
||||
readOnly
|
||||
autoComplete={props.autoComplete}
|
||||
id={props.id}
|
||||
autoComplete={autoComplete}
|
||||
id={id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className='flex-col w-full'>
|
||||
<div className='flex flex-row text-mineshaft-300 items-center mb-0.5'>
|
||||
<p className='text-sm font-semibold mr-1'>{props.label}</p>
|
||||
{/* {props.label == "Password" && router.asPath != "/login" && (
|
||||
}
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<div className="flex flex-row text-mineshaft-300 items-center mb-0.5">
|
||||
<p className="text-sm font-semibold mr-1">{label}</p>
|
||||
{/* {label === "Password" && router.asPath !== "/login" && (
|
||||
<div className="mb-0.5 relative inline-block text-gray-400 underline hover:text-primary duration-200">
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleExclamation}
|
||||
className={`text-sm peer ${
|
||||
props.error && "text-red"
|
||||
error && "text-red"
|
||||
}`}
|
||||
/>
|
||||
<span className="absolute hidden peer-hover:block duration-200 w-60 -left-28 -top-2 -translate-y-full px-2 py-2 bg-gray-700 rounded-md text-center text-gray-200 text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
@ -70,74 +77,69 @@ const InputField = (
|
||||
</span>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
<div
|
||||
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
||||
props.error ? 'border-red' : 'border-mineshaft-500'
|
||||
} rounded-md`}
|
||||
>
|
||||
<input
|
||||
onChange={(e) => props.onChangeHandler(e.target.value)}
|
||||
type={passwordVisible === false ? props.type : 'text'}
|
||||
placeholder={props.placeholder}
|
||||
value={props.value}
|
||||
required={props.isRequired}
|
||||
className={`${
|
||||
props.blurred
|
||||
? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400'
|
||||
: ''
|
||||
} ${
|
||||
props.error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
|
||||
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
|
||||
name={props.name}
|
||||
spellCheck='false'
|
||||
autoComplete={props.autoComplete}
|
||||
id={props.id}
|
||||
/>
|
||||
{props.label?.includes('Password') && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => {
|
||||
setPasswordVisible(!passwordVisible);
|
||||
}}
|
||||
className='absolute self-end mr-3 text-gray-400 cursor-pointer'
|
||||
>
|
||||
{passwordVisible ? (
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{props.blurred && (
|
||||
<div className='peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden'>
|
||||
<p className='ml-2'></p>
|
||||
{props.value
|
||||
.split('')
|
||||
.slice(0, 54)
|
||||
.map(() => (
|
||||
<FontAwesomeIcon
|
||||
key={guidGenerator()}
|
||||
className='text-xxs mx-0.5'
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* {props.error && (
|
||||
</div>
|
||||
<div
|
||||
className={`group relative flex flex-col justify-center w-full max-w-2xl border ${
|
||||
error ? 'border-red' : 'border-mineshaft-500'
|
||||
} rounded-md`}
|
||||
>
|
||||
<input
|
||||
onChange={(e) => onChangeHandler(e.target.value)}
|
||||
type={passwordVisible === false ? type : 'text'}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
required={isRequired}
|
||||
className={`${
|
||||
blurred
|
||||
? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400'
|
||||
: ''
|
||||
} ${
|
||||
error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
|
||||
} relative peer bg-bunker-800 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`}
|
||||
name={name}
|
||||
spellCheck="false"
|
||||
autoComplete={autoComplete}
|
||||
id={id}
|
||||
/>
|
||||
{label?.includes('Password') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPasswordVisible(!passwordVisible);
|
||||
}}
|
||||
className="absolute self-end mr-3 text-gray-400 cursor-pointer"
|
||||
>
|
||||
{passwordVisible ? (
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{blurred && (
|
||||
<div className="peer group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible absolute h-10 w-fit max-w-xl rounded-md flex items-center text-gray-400/50 text-clip overflow-hidden">
|
||||
<p className="ml-2" />
|
||||
{value
|
||||
.split('')
|
||||
.slice(0, 54)
|
||||
.map(() => (
|
||||
<FontAwesomeIcon
|
||||
key={guidGenerator()}
|
||||
className="text-xxs mx-0.5"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* {error && (
|
||||
<div className="absolute z-20 flex items-end justify-end mt-4 mr-1.5 self-end">
|
||||
<Error />
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
{props.error && (
|
||||
<p className='text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs'>
|
||||
{props.errorText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
{error && <p className="text-red text-xs mt-0.5 mx-0 mb-2 max-w-xs">{errorText}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InputField);
|
||||
|
@ -1,9 +1,12 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
@ -11,56 +14,51 @@ import {
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
faPlus,
|
||||
faUser
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import getOrganizations from "~/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
||||
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
||||
import getUser from "~/pages/api/user/getUser";
|
||||
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
||||
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
||||
import uploadKeys from "~/pages/api/workspace/uploadKeys";
|
||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||
import getOrganizationUsers from '@app/pages/api/organization/GetOrgUsers';
|
||||
import getUser from '@app/pages/api/user/getUser';
|
||||
import addUserToWorkspace from '@app/pages/api/workspace/addUserToWorkspace';
|
||||
import createWorkspace from '@app/pages/api/workspace/createWorkspace';
|
||||
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
|
||||
import NavBarDashboard from "../navigation/NavBarDashboard";
|
||||
import onboardingCheck from "../utilities/checks/OnboardingCheck";
|
||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric,
|
||||
} from "../utilities/cryptography/crypto";
|
||||
import Button from "./buttons/Button";
|
||||
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
|
||||
import Listbox from "./Listbox";
|
||||
import NavBarDashboard from '../navigation/NavBarDashboard';
|
||||
import onboardingCheck from '../utilities/checks/OnboardingCheck';
|
||||
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
|
||||
import { decryptAssymmetric, encryptAssymmetric } from '../utilities/cryptography/crypto';
|
||||
import Button from './buttons/Button';
|
||||
import AddWorkspaceDialog from './dialog/AddWorkspaceDialog';
|
||||
import Listbox from './Listbox';
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
const crypto = require("crypto");
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState("");
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState('∞');
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] =
|
||||
useState(0);
|
||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
function closeModal() {
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
function openModal() {
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: what to do about the fact that 2ids can have the same name
|
||||
|
||||
@ -68,7 +66,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
* When a user creates a new workspace, redirect them to the page of the new workspace.
|
||||
* @param {*} workspaceName
|
||||
*/
|
||||
async function submitModal(workspaceName: string, addAllUsers: boolean) {
|
||||
const submitModal = async (workspaceName: string, addAllUsers: boolean) => {
|
||||
setLoading(true);
|
||||
// timeout code.
|
||||
setTimeout(() => setLoading(false), 1500);
|
||||
@ -79,62 +77,49 @@ export default function Layout({ children }: LayoutProps) {
|
||||
if (!currentWorkspaces.includes(workspaceName)) {
|
||||
const newWorkspace = await createWorkspace({
|
||||
workspaceName,
|
||||
organizationId: tempLocalStorage("orgData.id"),
|
||||
organizationId: tempLocalStorage('orgData.id')
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadKeys(
|
||||
newWorkspaceId,
|
||||
myUser._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
await uploadKeys(newWorkspaceId, myUser._id, ciphertext, nonce);
|
||||
|
||||
if (addAllUsers) {
|
||||
console.log('adding other users')
|
||||
console.log('adding other users');
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: tempLocalStorage("orgData.id"),
|
||||
orgId: tempLocalStorage('orgData.id')
|
||||
});
|
||||
orgUsers.map(async (user: any) => {
|
||||
if (user.status == "accepted" && user.email != myUser.email) {
|
||||
const result = await addUserToWorkspace(
|
||||
user.user.email,
|
||||
newWorkspaceId
|
||||
);
|
||||
if (user.status === 'accepted' && user.email !== myUser.email) {
|
||||
const result = await addUserToWorkspace(user.user.email, newWorkspaceId);
|
||||
if (result?.invitee && result?.latestKey) {
|
||||
const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY");
|
||||
const TEMP_PRIVATE_KEY = tempLocalStorage('PRIVATE_KEY');
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
privateKey: TEMP_PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: result.invitee.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(
|
||||
newWorkspaceId,
|
||||
result.invitee._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
uploadKeys(newWorkspaceId, result.invitee._id, inviteeCipherText, inviteeNonce);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -142,12 +127,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
setWorkspaceMapping((prevState) => ({
|
||||
...prevState,
|
||||
[workspaceName]: newWorkspaceId
|
||||
}))
|
||||
}));
|
||||
setWorkspaceSelected(workspaceName);
|
||||
setIsOpen(false);
|
||||
setNewWorkspaceName("");
|
||||
setNewWorkspaceName('');
|
||||
} else {
|
||||
console.error("A project with this name already exists.");
|
||||
console.error('A project with this name already exists.');
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
@ -156,37 +141,35 @@ export default function Layout({ children }: LayoutProps) {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
href:
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.secrets"),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />,
|
||||
href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.secrets'),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />
|
||||
},
|
||||
{
|
||||
href: "/users/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.members"),
|
||||
emoji: <FontAwesomeIcon icon={faUser} />,
|
||||
href: `/users/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.members'),
|
||||
emoji: <FontAwesomeIcon icon={faUser} />
|
||||
},
|
||||
{
|
||||
href: "/integrations/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />,
|
||||
href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.integrations'),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />
|
||||
},
|
||||
{
|
||||
href: '/activity/' + workspaceMapping[workspaceSelected as any],
|
||||
href: `/activity/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: 'Activity Logs',
|
||||
emoji: <FontAwesomeIcon icon={faFileLines} />
|
||||
},
|
||||
{
|
||||
href: "/settings/project/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.project-settings"),
|
||||
emoji: <FontAwesomeIcon icon={faGear} />,
|
||||
},
|
||||
href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t('nav:menu.project-settings'),
|
||||
emoji: <FontAwesomeIcon icon={faGear} />
|
||||
}
|
||||
],
|
||||
[t, workspaceMapping, workspaceSelected]
|
||||
);
|
||||
@ -194,61 +177,50 @@ export default function Layout({ children }: LayoutProps) {
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage("orgData.id") === "") {
|
||||
if (tempLocalStorage('orgData.id') === '') {
|
||||
const userOrgs = await getOrganizations();
|
||||
localStorage.setItem("orgData.id", userOrgs[0]._id);
|
||||
localStorage.setItem('orgData.id', userOrgs[0]._id);
|
||||
}
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: tempLocalStorage("orgData.id"),
|
||||
orgId: tempLocalStorage('orgData.id')
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
userWorkspaces.length == 0 &&
|
||||
router.asPath != "/noprojects" &&
|
||||
!router.asPath.includes("home")&&
|
||||
!router.asPath.includes("settings")
|
||||
userWorkspaces.length === 0 &&
|
||||
router.asPath !== '/noprojects' &&
|
||||
!router.asPath.includes('home') &&
|
||||
!router.asPath.includes('settings') ||
|
||||
router.asPath === '/dashboard/undefined'
|
||||
) {
|
||||
router.push("/noprojects");
|
||||
} else if (router.asPath != "/noprojects") {
|
||||
router.push('/noprojects');
|
||||
} else if (router.asPath !== '/noprojects') {
|
||||
const intendedWorkspaceId = router.asPath
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0];
|
||||
|
||||
if (
|
||||
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId)
|
||||
) {
|
||||
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
.split('/')
|
||||
[router.asPath.split('/').length - 1].split('?')[0];
|
||||
|
||||
if (!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId)) {
|
||||
localStorage.setItem('projectData.id', intendedWorkspaceId);
|
||||
}
|
||||
|
||||
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
!["heroku", "vercel", "github", "netlify"].includes(intendedWorkspaceId) &&
|
||||
!['heroku', 'vercel', 'github', 'netlify'].includes(intendedWorkspaceId) &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||
} else {
|
||||
setWorkspaceMapping(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [
|
||||
workspace.name,
|
||||
workspace._id,
|
||||
])
|
||||
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
|
||||
) as any
|
||||
);
|
||||
setWorkspaceSelected(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [
|
||||
workspace._id,
|
||||
workspace.name,
|
||||
])
|
||||
)[
|
||||
router.asPath
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0]
|
||||
]
|
||||
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
|
||||
)[router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]]
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -262,31 +234,20 @@ export default function Layout({ children }: LayoutProps) {
|
||||
if (
|
||||
workspaceMapping[workspaceSelected as any] &&
|
||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||
router.asPath
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0]
|
||||
router.asPath.split('/')[router.asPath.split('/').length - 1].split('?')[0]
|
||||
) {
|
||||
localStorage.setItem(
|
||||
"projectData.id",
|
||||
`${workspaceMapping[workspaceSelected as any]}`
|
||||
);
|
||||
router.push(
|
||||
"/dashboard/" +
|
||||
workspaceMapping[workspaceSelected as any]
|
||||
);
|
||||
localStorage.setItem('projectData.id', `${workspaceMapping[workspaceSelected as any]}`);
|
||||
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, [workspaceSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed w-full hidden md:block flex flex-col h-screen">
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js"
|
||||
defer
|
||||
></script>
|
||||
<div className="fixed w-full md:block flex flex-col h-screen">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js" defer />
|
||||
<NavBarDashboard />
|
||||
<div className="flex flex-col md:flex-row flex-1">
|
||||
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
|
||||
@ -295,11 +256,11 @@ export default function Layout({ children }: LayoutProps) {
|
||||
<div>
|
||||
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
{t("nav:menu.project")}
|
||||
{t('nav:menu.project')}
|
||||
</div>
|
||||
{Object.keys(workspaceMapping).length > 0 ? (
|
||||
<Listbox
|
||||
selected={workspaceSelected}
|
||||
isSelected={workspaceSelected}
|
||||
onChange={setWorkspaceSelected}
|
||||
data={Object.keys(workspaceMapping)}
|
||||
buttonAction={openModal}
|
||||
@ -319,35 +280,27 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{Object.keys(workspaceMapping).length > 0 &&
|
||||
menuItems.map(({ href, title, emoji }) => (
|
||||
<li className="mt-0.5 mx-2" key={title}>
|
||||
{router.asPath.split("/")[1] === href.split("/")[1] &&
|
||||
(["project", "billing", "org", "personal"].includes(
|
||||
router.asPath.split("/")[2]
|
||||
{router.asPath.split('/')[1] === href.split('/')[1] &&
|
||||
(['project', 'billing', 'org', 'personal'].includes(
|
||||
router.asPath.split('/')[2]
|
||||
)
|
||||
? router.asPath.split("/")[2] === href.split("/")[2]
|
||||
? router.asPath.split('/')[2] === href.split('/')[2]
|
||||
: true) ? (
|
||||
<div
|
||||
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
||||
>
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
|
||||
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
) : router.asPath == "/noprojects" ? (
|
||||
<div
|
||||
className={`flex p-2.5 text-white text-sm rounded`}
|
||||
>
|
||||
<p className="w-10 flex items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
) : router.asPath === '/noprojects' ? (
|
||||
<div className="flex p-2.5 text-white text-sm rounded">
|
||||
<p className="w-10 flex items-center justify-center text-lg">{emoji}</p>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<div
|
||||
className={`flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5`}
|
||||
>
|
||||
<div className="flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5">
|
||||
<p className="w-10 flex items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
@ -360,53 +313,47 @@ export default function Layout({ children }: LayoutProps) {
|
||||
</ul>
|
||||
</div>
|
||||
<div className="w-full mt-40 mb-4 px-2">
|
||||
{router.asPath.split("/")[1] === "home" ? (
|
||||
<div
|
||||
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
||||
>
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
|
||||
{router.asPath.split('/')[1] === 'home' ? (
|
||||
<div className="flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10">
|
||||
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1" />
|
||||
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${
|
||||
totalOnboardingActionsDone == 0 ? "0" : ""
|
||||
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
|
||||
totalOnboardingActionsDone == 2 ? "28" : ""
|
||||
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
|
||||
totalOnboardingActionsDone == 4 ? "57" : ""
|
||||
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
|
||||
totalOnboardingActionsDone === 1 ? '14' : ''
|
||||
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
|
||||
totalOnboardingActionsDone === 3 ? '43' : ''
|
||||
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
|
||||
totalOnboardingActionsDone === 5 ? '71' : ''
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
></img>
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href={`/home/` + workspaceMapping[workspaceSelected as any]}
|
||||
>
|
||||
<div
|
||||
className={`relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max`}
|
||||
>
|
||||
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
|
||||
<div className="relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max">
|
||||
<p className="w-10 flex items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${
|
||||
totalOnboardingActionsDone == 0 ? "0" : ""
|
||||
}${totalOnboardingActionsDone == 1 ? "14" : ""}${
|
||||
totalOnboardingActionsDone == 2 ? "28" : ""
|
||||
}${totalOnboardingActionsDone == 3 ? "43" : ""}${
|
||||
totalOnboardingActionsDone == 4 ? "57" : ""
|
||||
}${totalOnboardingActionsDone == 5 ? "71" : ""}.svg`}
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? '0' : ''}${
|
||||
totalOnboardingActionsDone === 1 ? '14' : ''
|
||||
}${totalOnboardingActionsDone === 2 ? '28' : ''}${
|
||||
totalOnboardingActionsDone === 3 ? '43' : ''
|
||||
}${totalOnboardingActionsDone === 4 ? '57' : ''}${
|
||||
totalOnboardingActionsDone === 5 ? '71' : ''
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
></img>
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
@ -426,14 +373,13 @@ export default function Layout({ children }: LayoutProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={faMobile}
|
||||
className="text-gray-300 text-7xl mb-8"
|
||||
/>
|
||||
<FontAwesomeIcon icon={faMobile} className="text-gray-300 text-7xl mb-8" />
|
||||
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
||||
{` ${t("common:no-mobile")} `}
|
||||
{` ${t('common:no-mobile')} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
@ -1,15 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import {
|
||||
faAngleDown,
|
||||
faCheck,
|
||||
faPlus,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import React, { Fragment } from 'react';
|
||||
import { faAngleDown, faCheck, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
|
||||
interface ListBoxProps {
|
||||
selected: string;
|
||||
isSelected: string;
|
||||
onChange: (arg: string) => void;
|
||||
data: string[] | null;
|
||||
text?: string;
|
||||
@ -20,55 +15,55 @@ interface ListBoxProps {
|
||||
/**
|
||||
* This is the component that we use for drop down lists.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.selected - the item that is currently selected
|
||||
* @param {string} obj.isSelected - the item that is currently selected
|
||||
* @param {function} obj.onChange - what happends if you select the item inside a list
|
||||
* @param {string[]} obj.data - all the options available
|
||||
* @param {string} obj.text - the text that shows us in front of the select option
|
||||
* @param {function} obj.buttonAction - if there is a button at the bottom of the list, this is the action that happens when you click the button
|
||||
* @returns
|
||||
*/
|
||||
export default function ListBox({
|
||||
selected,
|
||||
const ListBox = ({
|
||||
isSelected,
|
||||
onChange,
|
||||
data,
|
||||
text,
|
||||
buttonAction,
|
||||
isFull,
|
||||
}: ListBoxProps): JSX.Element {
|
||||
isFull
|
||||
}: ListBoxProps): JSX.Element => {
|
||||
return (
|
||||
<Listbox value={selected} onChange={onChange}>
|
||||
<div className='relative'>
|
||||
<Listbox value={isSelected} onChange={onChange}>
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`text-gray-400 relative ${
|
||||
isFull ? 'w-full' : 'w-52'
|
||||
} cursor-default rounded-md bg-white/[0.07] hover:bg-white/[0.11] duration-200 py-2.5 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm`}
|
||||
>
|
||||
<div className='flex flex-row'>
|
||||
<div className="flex flex-row">
|
||||
{text}
|
||||
<span className='ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize'>
|
||||
<span className="ml-1 cursor-pointer block truncate font-semibold text-gray-300">
|
||||
{' '}
|
||||
{selected}
|
||||
{isSelected}
|
||||
</span>
|
||||
</div>
|
||||
{data && (
|
||||
<div className='cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2'>
|
||||
<FontAwesomeIcon icon={faAngleDown} className='text-md mr-1.5' />
|
||||
<div className="cursor-pointer pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<FontAwesomeIcon icon={faAngleDown} className="text-md mr-1.5" />
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Button>
|
||||
{data && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave='transition ease-in duration-100'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className='border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm'>
|
||||
<Listbox.Options className="border border-mineshaft-700 z-50 p-2 absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-bunker text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{data.map((person, personIdx) => (
|
||||
<Listbox.Option
|
||||
key={personIdx}
|
||||
key={`${person}.${personIdx + 1}`}
|
||||
className={({ active, selected }) =>
|
||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
|
||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md ${
|
||||
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
||||
} ${
|
||||
active && !selected
|
||||
@ -88,11 +83,8 @@ export default function ListBox({
|
||||
{person}
|
||||
</span>
|
||||
{selected ? (
|
||||
<span className='text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3'>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='text-md ml-1'
|
||||
/>
|
||||
<span className="text-primary rounded-lg absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md ml-1" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
@ -100,13 +92,10 @@ export default function ListBox({
|
||||
</Listbox.Option>
|
||||
))}
|
||||
{buttonAction && (
|
||||
<button
|
||||
onClick={buttonAction}
|
||||
className='cursor-pointer w-full'
|
||||
>
|
||||
<div className='my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2'>
|
||||
<span className='rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4'>
|
||||
<FontAwesomeIcon icon={faPlus} className='text-lg' />
|
||||
<button type="button" onClick={buttonAction} className="cursor-pointer w-full">
|
||||
<div className="my-0.5 relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-lime-300 duration-200 hover:text-black hover:font-semibold mt-2">
|
||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="text-lg" />
|
||||
</span>
|
||||
Add Project
|
||||
</div>
|
||||
@ -118,4 +107,6 @@ export default function ListBox({
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ListBox;
|
||||
|
@ -16,17 +16,12 @@ interface ToggleProps {
|
||||
* @param {number} obj.pos - position of a certain secret
|
||||
* @returns
|
||||
*/
|
||||
export default function Toggle({
|
||||
enabled,
|
||||
setEnabled,
|
||||
addOverride,
|
||||
pos,
|
||||
}: ToggleProps): JSX.Element {
|
||||
const Toggle = ({ enabled, setEnabled, addOverride, pos }: ToggleProps): JSX.Element => {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled == false) {
|
||||
if (enabled === false) {
|
||||
addOverride('', pos);
|
||||
} else {
|
||||
addOverride(undefined, pos);
|
||||
@ -37,7 +32,7 @@ export default function Toggle({
|
||||
enabled ? 'bg-primary' : 'bg-bunker-400'
|
||||
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||
>
|
||||
<span className='sr-only'>Enable notifications</span>
|
||||
<span className="sr-only">Enable notifications</span>
|
||||
<span
|
||||
className={`${
|
||||
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
||||
@ -45,4 +40,6 @@ export default function Toggle({
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Toggle;
|
||||
|
@ -1,11 +1,10 @@
|
||||
import React, { ButtonHTMLAttributes } from "react";
|
||||
import Image from "next/image";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
} from "@fortawesome/react-fontawesome";
|
||||
/* eslint-disable react/button-has-type */
|
||||
import React, { ButtonHTMLAttributes } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
const classNames = require("classnames");
|
||||
const classNames = require('classnames');
|
||||
|
||||
type ButtonProps = {
|
||||
text?: string;
|
||||
@ -17,7 +16,7 @@ type ButtonProps = {
|
||||
active?: boolean;
|
||||
iconDisabled?: IconProp;
|
||||
textDisabled?: string;
|
||||
type?: ButtonHTMLAttributes<any>['type'];
|
||||
type?: ButtonHTMLAttributes<any>["type"];
|
||||
};
|
||||
|
||||
/**
|
||||
@ -34,72 +33,76 @@ type ButtonProps = {
|
||||
* @param {string} props.textDisable - text inside the button when it is disabled
|
||||
* @returns
|
||||
*/
|
||||
export default function Button(props: ButtonProps): JSX.Element {
|
||||
const Button = ({
|
||||
active,
|
||||
text,
|
||||
textDisabled,
|
||||
color,
|
||||
size,
|
||||
onButtonPressed,
|
||||
loading,
|
||||
icon,
|
||||
iconDisabled,
|
||||
type = "button",
|
||||
}: ButtonProps): JSX.Element => {
|
||||
// Check if the button show always be 'active' - then true;
|
||||
// or if it should switch between 'active' and 'disabled' - then give the status
|
||||
const activityStatus =
|
||||
props.active || (props.text != "" && props.textDisabled == undefined);
|
||||
const activityStatus = active || (text !== '' && textDisabled === undefined);
|
||||
|
||||
const styleButton = classNames(
|
||||
"group m-auto md:m-0 inline-block rounded-md duration-200",
|
||||
'group m-auto md:m-0 inline-block rounded-md duration-200',
|
||||
|
||||
// Setting background colors and hover modes
|
||||
props.color == "mineshaft" &&
|
||||
activityStatus &&
|
||||
"bg-mineshaft-700 hover:bg-primary",
|
||||
props.color == "mineshaft" && !activityStatus && "bg-mineshaft",
|
||||
(props.color == "primary" || !props.color) &&
|
||||
activityStatus &&
|
||||
"bg-primary hover:opacity-80",
|
||||
(props.color == "primary" || !props.color) &&
|
||||
!activityStatus &&
|
||||
"bg-primary",
|
||||
props.color == "red" && "bg-red",
|
||||
color === 'mineshaft' && activityStatus && 'bg-mineshaft-700 hover:bg-primary',
|
||||
color === 'mineshaft' && !activityStatus && 'bg-mineshaft',
|
||||
(color === 'primary' || !color) && activityStatus && 'bg-primary hover:opacity-80',
|
||||
(color === 'primary' || !color) && !activityStatus && 'bg-primary',
|
||||
color === 'red' && 'bg-red',
|
||||
|
||||
// Changing the opacity when active vs when not
|
||||
activityStatus ? "opacity-100 cursor-pointer" : "opacity-40",
|
||||
activityStatus ? 'opacity-100 cursor-pointer' : 'opacity-40',
|
||||
|
||||
// Setting the button sizes
|
||||
props.size == "md" && "h-10 w-full px-2 md:px-4",
|
||||
props.size == "lg" && "h-12 w-full px-2 md:px-8",
|
||||
!props.size && "md:py-1 px-3 md:px-8",
|
||||
props.size == "icon-md" && "h-10 w-10 flex items-center justify-center",
|
||||
props.size == "icon-sm" && "h-9 w-9 flex items-center justify-center"
|
||||
size === 'md' && 'h-10 w-full px-2 md:px-4',
|
||||
size === 'lg' && 'h-12 w-full px-2 md:px-8',
|
||||
!size && 'md:py-1 px-3 md:px-8',
|
||||
size === 'icon-md' && 'h-10 w-10 flex items-center justify-center',
|
||||
size === 'icon-sm' && 'h-9 w-9 flex items-center justify-center'
|
||||
);
|
||||
|
||||
const styleMainDiv = classNames(
|
||||
"relative font-medium flex items-center",
|
||||
'relative font-medium flex items-center',
|
||||
|
||||
// Setting the text color for the text and icon
|
||||
props.color == "mineshaft" && "text-gray-400",
|
||||
props.color != "mineshaft" && props.color != "red" && props.color != "none" && "text-black",
|
||||
props.color == "red" && "text-gray-200",
|
||||
props.color == "none" && "text-gray-200 text-xl",
|
||||
activityStatus && props.color != "red" && props.color != "none" ? "group-hover:text-black" : "",
|
||||
color === 'mineshaft' && 'text-gray-400',
|
||||
color !== 'mineshaft' && color !== 'red' && color !== 'none' && 'text-black',
|
||||
color === 'red' && 'text-gray-200',
|
||||
color === 'none' && 'text-gray-200 text-xl',
|
||||
activityStatus && color !== 'red' && color !== 'none' ? 'group-hover:text-black' : '',
|
||||
|
||||
props.size == "icon" && "flex items-center justify-center"
|
||||
size === 'icon' && 'flex items-center justify-center'
|
||||
);
|
||||
|
||||
const textStyle = classNames(
|
||||
"relative duration-200 text-center w-full",
|
||||
'relative duration-200 text-center w-full',
|
||||
|
||||
// Show the loading sign if the loading indicator is on
|
||||
props.loading ? "opacity-0" : "opacity-100",
|
||||
props.size == "md" && "text-sm",
|
||||
props.size == "lg" && "text-lg"
|
||||
loading ? 'opacity-0' : 'opacity-100',
|
||||
size === 'md' && 'text-sm',
|
||||
size === 'lg' && 'text-lg'
|
||||
);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
disabled={!activityStatus}
|
||||
type={props.type}
|
||||
onClick={props.onButtonPressed}
|
||||
type={type}
|
||||
onClick={onButtonPressed}
|
||||
className={styleButton}
|
||||
>
|
||||
<div className={styleMainDiv}>
|
||||
<div
|
||||
className={`${
|
||||
props.loading == true ? "opacity-100" : "opacity-0"
|
||||
loading === true ? 'opacity-100' : 'opacity-0'
|
||||
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
||||
>
|
||||
<Image
|
||||
@ -107,33 +110,33 @@ export default function Button(props: ButtonProps): JSX.Element {
|
||||
height={25}
|
||||
width={42}
|
||||
alt="loading animation"
|
||||
className={`rounded-xl`}
|
||||
></Image>
|
||||
className="rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
{props.icon && (
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={props.icon}
|
||||
className={`flex my-auto font-extrabold ${
|
||||
props.size == "icon-sm" ? "text-sm" : "text-sm"
|
||||
} ${(props.text || props.textDisabled) && "mr-2"}`}
|
||||
icon={icon}
|
||||
className={`flex my-auto font-extrabold ${size === 'icon-sm' ? 'text-sm' : 'text-sm'} ${
|
||||
(text || textDisabled) && 'mr-2'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{props.iconDisabled && (
|
||||
{iconDisabled && (
|
||||
<FontAwesomeIcon
|
||||
icon={props.iconDisabled as IconProp}
|
||||
className={`flex my-auto font-extrabold ${
|
||||
props.size == "icon-sm" ? "text-sm" : "text-md"
|
||||
} ${(props.text || props.textDisabled) && "mr-2"}`}
|
||||
icon={iconDisabled as IconProp}
|
||||
className={`flex my-auto font-extrabold ${size === 'icon-sm' ? 'text-sm' : 'text-md'} ${
|
||||
(text || textDisabled) && 'mr-2'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
{(props.text || props.textDisabled) && (
|
||||
<p className={textStyle}>
|
||||
{activityStatus ? props.text : props.textDisabled}
|
||||
</p>
|
||||
{(text || textDisabled) && (
|
||||
<p className={textStyle}>{activityStatus ? text : textDisabled}</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
return button;
|
||||
}
|
||||
};
|
||||
|
||||
export default Button;
|
||||
|
@ -17,7 +17,7 @@ const ActivateBotDialog = ({
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption,
|
||||
handleIntegrationOption
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -30,7 +30,7 @@ const ActivateBotDialog = ({
|
||||
if (!selectedIntegrationOption) return;
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption,
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
@ -42,47 +42,44 @@ const ActivateBotDialog = ({
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative z-10' onClose={closeModal}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400'
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{t('integrations:grant-access-to-secrets')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-2'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('integrations:why-infisical-needs-access')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-6 max-w-max'>
|
||||
<div className="mt-6 max-w-max">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color='mineshaft'
|
||||
color="mineshaft"
|
||||
text={t('integrations:grant-access-button') as string}
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
|
@ -4,7 +4,7 @@ import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import addAPIKey from '~/pages/api/apiKey/addAPIKey';
|
||||
import addAPIKey from '@app/pages/api/apiKey/addAPIKey';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
@ -99,7 +99,7 @@ const AddApiKeyDialog = ({
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
{apiKey == '' ? (
|
||||
{apiKey === '' ? (
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
@ -126,7 +126,7 @@ const AddApiKeyDialog = ({
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<ListBox
|
||||
selected={apiKeyExpiresIn}
|
||||
isSelected={apiKeyExpiresIn}
|
||||
onChange={setApiKeyExpiresIn}
|
||||
data={[
|
||||
'1 day',
|
||||
@ -135,7 +135,7 @@ const AddApiKeyDialog = ({
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
isFull={true}
|
||||
isFull
|
||||
text={`${t('common:expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
@ -149,7 +149,7 @@ const AddApiKeyDialog = ({
|
||||
t('section-api-key:add-dialog.add') as string
|
||||
}
|
||||
size='md'
|
||||
active={apiKeyName == '' ? false : true}
|
||||
active={apiKeyName !== ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -176,15 +176,16 @@ const AddApiKeyDialog = ({
|
||||
<input
|
||||
type='text'
|
||||
value={apiKey}
|
||||
disabled={true}
|
||||
disabled
|
||||
id='apiKey'
|
||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||
></input>
|
||||
/>
|
||||
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||
{apiKey}
|
||||
</div>
|
||||
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyToClipboard}
|
||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||
>
|
||||
|
@ -2,7 +2,7 @@ import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import addIncidentContact from '~/pages/api/organization/addIncidentContact';
|
||||
import addIncidentContact from '@app/pages/api/organization/addIncidentContact';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
@ -10,7 +10,6 @@ import InputField from '../InputField';
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
workspaceId: string;
|
||||
incidentContacts: string[];
|
||||
setIncidentContacts: (arg: string[]) => void;
|
||||
};
|
||||
|
@ -27,122 +27,105 @@ const AddProjectMemberDialog = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
<div className="z-50">
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative' onClose={closeModal}>
|
||||
<Dialog as="div" className="relative" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto'>
|
||||
<div className='flex min-h-full items-center justify-center p-4 text-center'>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0 scale-95'
|
||||
enterTo='opacity-100 scale-100'
|
||||
leave='ease-in duration-200'
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
{data?.length > 0 ? (
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
>
|
||||
{t('section-members:add-dialog.add-member-to-project')}
|
||||
</Dialog.Title>
|
||||
) : (
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400 z-50"
|
||||
>
|
||||
{t('section-members:add-dialog.already-all-invited')}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className="mt-2 mb-4">
|
||||
{data?.length > 0 ? (
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section-members:add-dialog.user-will-email')}
|
||||
</p>
|
||||
<div className=''>
|
||||
<div className="">
|
||||
<Trans
|
||||
i18nKey='section-members:add-dialog.looking-add'
|
||||
i18nKey="section-members:add-dialog.looking-add"
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/settings/org/' + router.query.id
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
aria-label="add member"
|
||||
/>,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<button
|
||||
type='button'
|
||||
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
type="button"
|
||||
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/settings/org/' +
|
||||
router.query.id +
|
||||
'?invite'
|
||||
)
|
||||
router.push(`/settings/org/${router.query.id}?invite`)
|
||||
}
|
||||
/>,
|
||||
aria-label="add member"
|
||||
/>
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-sm text-gray-500'>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('section-members:add-dialog.add-user-org-first')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<div className="max-h-28">
|
||||
{data?.length > 0 && (
|
||||
<ListBox
|
||||
selected={email ? email : data[0]}
|
||||
onChange={setEmail}
|
||||
data={data}
|
||||
isFull={true}
|
||||
/>
|
||||
<ListBox isSelected={email || data[0]} onChange={setEmail} data={data} isFull />
|
||||
)}
|
||||
</div>
|
||||
<div className='max-w-max'>
|
||||
<div className="max-w-max">
|
||||
{data?.length > 0 ? (
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<div className="mt-6 flex flex-col justify-start w-max">
|
||||
<Button
|
||||
onButtonPressed={submitModal}
|
||||
color='mineshaft'
|
||||
color="mineshaft"
|
||||
text={t('section-members:add-member') as string}
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
router.push('/settings/org/' + router.query.id)
|
||||
}
|
||||
color='mineshaft'
|
||||
text={
|
||||
t(
|
||||
'section-members:add-dialog.add-user-to-org'
|
||||
) as string
|
||||
}
|
||||
size='md'
|
||||
onButtonPressed={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
color="mineshaft"
|
||||
text={t('section-members:add-dialog.add-user-to-org') as string}
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -6,8 +6,8 @@ import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import addServiceToken from '~/pages/api/serviceToken/addServiceToken';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
import addServiceToken from '@app/pages/api/serviceToken/addServiceToken';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
@ -86,7 +86,7 @@ const AddServiceTokenDialog = ({
|
||||
});
|
||||
|
||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||
setServiceToken(newServiceToken.serviceToken + '.' + randomBytes);
|
||||
setServiceToken(`${newServiceToken.serviceToken }.${ randomBytes}`);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
@ -141,7 +141,7 @@ const AddServiceTokenDialog = ({
|
||||
leaveFrom='opacity-100 scale-100'
|
||||
leaveTo='opacity-0 scale-95'
|
||||
>
|
||||
{serviceToken == '' ? (
|
||||
{serviceToken === '' ? (
|
||||
<Dialog.Panel className='w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all'>
|
||||
<Dialog.Title
|
||||
as='h3'
|
||||
@ -170,7 +170,7 @@ const AddServiceTokenDialog = ({
|
||||
</div>
|
||||
<div className='max-h-28 mb-2'>
|
||||
<ListBox
|
||||
selected={
|
||||
isSelected={
|
||||
selectedServiceTokenEnv?.name
|
||||
? selectedServiceTokenEnv?.name
|
||||
: environments[0]?.name
|
||||
@ -186,13 +186,13 @@ const AddServiceTokenDialog = ({
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull={true}
|
||||
isFull
|
||||
text={`${t('common:environment')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<ListBox
|
||||
selected={serviceTokenExpiresIn}
|
||||
isSelected={serviceTokenExpiresIn}
|
||||
onChange={setServiceTokenExpiresIn}
|
||||
data={[
|
||||
'1 day',
|
||||
@ -201,7 +201,7 @@ const AddServiceTokenDialog = ({
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
isFull={true}
|
||||
isFull
|
||||
text={`${t('common:expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
@ -215,7 +215,7 @@ const AddServiceTokenDialog = ({
|
||||
t('section-token:add-dialog.add') as string
|
||||
}
|
||||
size='md'
|
||||
active={serviceTokenName == '' ? false : true}
|
||||
active={serviceTokenName !== ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -244,13 +244,14 @@ const AddServiceTokenDialog = ({
|
||||
value={serviceToken}
|
||||
id='serviceToken'
|
||||
className='invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none'
|
||||
></input>
|
||||
/>
|
||||
<div className='bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none'>
|
||||
{serviceToken}
|
||||
</div>
|
||||
<div className='group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200'>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
type="button"
|
||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||
>
|
||||
{serviceTokenCopied ? (
|
||||
|
@ -10,7 +10,6 @@ type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (email: string) => void;
|
||||
workspaceId: string;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
currentPlan: string;
|
||||
@ -82,13 +81,13 @@ const AddUserDialog = ({
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
{currentPlan == STRIPE_PRODUCT_STARTER && (
|
||||
{currentPlan === STRIPE_PRODUCT_STARTER && (
|
||||
<div className='flex flex-row'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={() =>
|
||||
router.push('/settings/billing/' + router.query.id)
|
||||
router.push(`/settings/billing/${ router.query.id}`)
|
||||
}
|
||||
>
|
||||
You can add up to 5 members on a Free tier.
|
||||
@ -97,7 +96,7 @@ const AddUserDialog = ({
|
||||
type='button'
|
||||
className='ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={() =>
|
||||
router.push('/settings/billing/' + router.query.id)
|
||||
router.push(`/settings/billing/${ router.query.id}`)
|
||||
}
|
||||
>
|
||||
Upgrade now.
|
||||
|
@ -26,7 +26,14 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} />
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-70"
|
||||
onClick={onClose}
|
||||
onKeyDown={onClose}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close"
|
||||
/>
|
||||
</Transition.Child>
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
@ -39,10 +46,7 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-grey border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-gray-400"
|
||||
>
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{t('dashboard:sidebar.delete-key-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
|
@ -6,7 +6,6 @@ import InputField from "../InputField";
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (userIdToBeDeleted: string) => void;
|
||||
selectedIntegrationOption: string;
|
||||
handleBotActivate: () => void;
|
||||
handleIntegrationOption: (arg:{integrationOption:string})=>void;
|
||||
@ -20,6 +19,7 @@ const IntegrationAccessTokenDialog = ({
|
||||
handleIntegrationOption
|
||||
}:Props) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. activate bot
|
||||
@ -85,7 +85,7 @@ const IntegrationAccessTokenDialog = ({
|
||||
label="Access token"
|
||||
onChangeHandler={() => {}}
|
||||
type="varName"
|
||||
value={"Hello"}
|
||||
value="Hello"
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
|
@ -23,50 +23,47 @@ interface PopupProps {
|
||||
* @param {string} org.setCheckDocsPopUpVisible - the functions that closes the popup
|
||||
* @returns
|
||||
*/
|
||||
export default function BottonRightPopup({
|
||||
const BottonRightPopup = ({
|
||||
buttonText,
|
||||
buttonLink,
|
||||
titleText,
|
||||
emoji,
|
||||
textLine1,
|
||||
textLine2,
|
||||
setCheckDocsPopUpVisible,
|
||||
}: PopupProps): JSX.Element {
|
||||
setCheckDocsPopUpVisible
|
||||
}: PopupProps): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className='z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6'
|
||||
role='alert'
|
||||
className="z-50 drop-shadow-xl border-gray-600/50 border flex flex-col items-start bg-bunker max-w-xl text-gray-200 pt-3 pb-4 rounded-md absolute bottom-0 right-0 mr-6 mb-6"
|
||||
role="alert"
|
||||
>
|
||||
<div className='flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6'>
|
||||
<div className='font-bold text-xl mr-2 mt-0.5 flex flex-row'>
|
||||
<div className="flex flex-row items-center justify-between w-full border-b border-gray-600/70 pb-3 px-6">
|
||||
<div className="font-bold text-xl mr-2 mt-0.5 flex flex-row">
|
||||
<div>{titleText}</div>
|
||||
<div className='ml-2.5'>{emoji}</div>
|
||||
<div className="ml-2.5">{emoji}</div>
|
||||
</div>
|
||||
<button
|
||||
className='mt-1'
|
||||
onClick={() => setCheckDocsPopUpVisible(false)}
|
||||
>
|
||||
<button className="mt-1" onClick={() => setCheckDocsPopUpVisible(false)} type="button">
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className='text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer'
|
||||
className="text-gray-400 text-2xl hover:text-red duration-200 cursor-pointer"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className='block sm:inline px-6 mt-4 mb-0.5 text-gray-300'>
|
||||
{textLine1}
|
||||
</div>
|
||||
<div className='block sm:inline mb-4 px-6'>{textLine2}</div>
|
||||
<div className='flex flex-row px-6 w-full'>
|
||||
{/*eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<div className="block sm:inline px-6 mt-4 mb-0.5 text-gray-300">{textLine1}</div>
|
||||
<div className="block sm:inline mb-4 px-6">{textLine2}</div>
|
||||
<div className="flex flex-row px-6 w-full">
|
||||
{/* eslint-disable-next-line react/jsx-no-target-blank */}
|
||||
<a
|
||||
className='font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center'
|
||||
className="font-bold p-2 bg-white/10 rounded-md w-full hover:bg-primary duration-200 hover:text-black flex justify-center"
|
||||
href={buttonLink}
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
{buttonText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default BottonRightPopup;
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
|
||||
import deleteAPIKey from '../../../pages/api/apiKey/deleteAPIKey';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
@ -28,49 +28,47 @@ const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
return (
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
|
||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300 text-sm font-light">
|
||||
<tr>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
|
||||
<th></th>
|
||||
<th aria-label="button" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data?.map((row) => {
|
||||
return (
|
||||
<tr
|
||||
key={guidGenerator()}
|
||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
||||
>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.name}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{new Date(row.expiresAt).toUTCString()}
|
||||
</td>
|
||||
<td className="py-2 border-mineshaft-700 border-t">
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
deleteAPIKey({ apiKeyId: row._id} );
|
||||
setApiKeys(data.filter(token => token._id != row._id));
|
||||
createNotification({
|
||||
text: `'${row.name}' API key has been revoked.`,
|
||||
type: 'error'
|
||||
});
|
||||
}}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
data?.map((row) => (
|
||||
<tr
|
||||
key={guidGenerator()}
|
||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
||||
>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.name}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{new Date(row.expiresAt).toUTCString()}
|
||||
</td>
|
||||
<td className="py-2 border-mineshaft-700 border-t">
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
deleteAPIKey({ apiKeyId: row._id });
|
||||
setApiKeys(data.filter((token) => token._id !== row._id));
|
||||
createNotification({
|
||||
text: `'${row.name}' API key has been revoked.`,
|
||||
type: 'error'
|
||||
});
|
||||
}}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
||||
|
@ -1,31 +1,32 @@
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
type Props = {
|
||||
addAllUsers: boolean;
|
||||
setAddAllUsers: (arg: boolean) => void;
|
||||
};
|
||||
|
||||
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-row items-center'>
|
||||
{addAllUsers == true ? (
|
||||
<input
|
||||
type='checkbox'
|
||||
className='accent-primary h-4 w-4'
|
||||
checked
|
||||
readOnly
|
||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className='h-4 w-4 bg-bunker border border-gray-600 rounded-sm'
|
||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||
></div>
|
||||
)}
|
||||
export const Checkbox = ({ addAllUsers, setAddAllUsers }: Props) => (
|
||||
<div className="flex flex-row items-center">
|
||||
{addAllUsers === true ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-primary h-4 w-4"
|
||||
checked
|
||||
readOnly
|
||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
onKeyDown={() => setAddAllUsers(!addAllUsers)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="add all users"
|
||||
className="h-4 w-4 bg-bunker border border-gray-600 rounded-sm"
|
||||
onClick={() => setAddAllUsers(!addAllUsers)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<label className='ml-2 text-gray-500 text-sm'>
|
||||
Add all members of my organization to this project.
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
<label className="ml-2 text-gray-500 text-sm">
|
||||
Add all members of my organization to this project.
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
|
@ -14,15 +14,10 @@ type Props = {
|
||||
onDeleteEnv: (slug: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const EnvironmentTable = ({
|
||||
data = [],
|
||||
onCreateEnv,
|
||||
onDeleteEnv,
|
||||
onUpdateEnv,
|
||||
}: Props) => {
|
||||
const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'createUpdateEnv',
|
||||
'deleteEnv',
|
||||
'deleteEnv'
|
||||
] as const);
|
||||
|
||||
const onEnvCreateCB = async (env: Env) => {
|
||||
@ -36,10 +31,7 @@ const EnvironmentTable = ({
|
||||
|
||||
const onEnvUpdateCB = async (env: Env) => {
|
||||
try {
|
||||
await onUpdateEnv(
|
||||
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
|
||||
env
|
||||
);
|
||||
await onUpdateEnv((popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug, env);
|
||||
handlePopUpClose('createUpdateEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -48,9 +40,7 @@ const EnvironmentTable = ({
|
||||
|
||||
const onEnvDeleteCB = async () => {
|
||||
try {
|
||||
await onDeleteEnv(
|
||||
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
|
||||
);
|
||||
await onDeleteEnv((popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug);
|
||||
handlePopUpClose('deleteEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -59,83 +49,68 @@ const EnvironmentTable = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-row justify-between w-full'>
|
||||
<div className='flex flex-col w-full'>
|
||||
<p className='text-xl font-semibold mb-3'>Project Environments</p>
|
||||
<p className='text-base text-gray-400 mb-4'>
|
||||
Choose which environments will show up in your dashboard like
|
||||
development, staging, production
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<p className="text-xl font-semibold mb-3">Project Environments</p>
|
||||
<p className="text-base text-gray-400 mb-4">
|
||||
Choose which environments will show up in your dashboard like development, staging,
|
||||
production
|
||||
</p>
|
||||
<p className='text-sm mr-1 text-gray-500 self-start'>
|
||||
Note: the text in slugs shows how these environmant should be
|
||||
accessed in CLI.
|
||||
<p className="text-sm mr-1 text-gray-500 self-start">
|
||||
Note: the text in slugs shows how these environmant should be accessed in CLI.
|
||||
</p>
|
||||
</div>
|
||||
<div className='w-48'>
|
||||
<div className="w-48">
|
||||
<Button
|
||||
text='Add New Env'
|
||||
text="Add New Env"
|
||||
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
|
||||
color='mineshaft'
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1'>
|
||||
<div className='absolute rounded-t-md w-full h-12 bg-white/5'></div>
|
||||
<table className='w-full my-1'>
|
||||
<thead className='text-bunker-300'>
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<tr>
|
||||
<th className='text-left pl-6 pt-2.5 pb-2'>Name</th>
|
||||
<th className='text-left pl-6 pt-2.5 pb-2'>Slug</th>
|
||||
<th></th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Name</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">Slug</th>
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data.map(({ name, slug }) => {
|
||||
return (
|
||||
<tr
|
||||
key={name}
|
||||
className='bg-bunker-800 hover:bg-bunker-800/5 duration-100'
|
||||
>
|
||||
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize'>
|
||||
{name}
|
||||
</td>
|
||||
<td className='pl-6 py-2 border-mineshaft-700 border-t text-gray-300'>
|
||||
{slug}
|
||||
</td>
|
||||
<td className='py-2 border-mineshaft-700 border-t flex'>
|
||||
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center mr-8'>
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
handlePopUpOpen('createUpdateEnv', { name, slug })
|
||||
}
|
||||
color='red'
|
||||
size='icon-sm'
|
||||
icon={faPencil}
|
||||
/>
|
||||
</div>
|
||||
<div className='opacity-50 hover:opacity-100 duration-200 flex items-center'>
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
handlePopUpOpen('deleteEnv', { name, slug })
|
||||
}
|
||||
color='red'
|
||||
size='icon-sm'
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
data.map(({ name, slug }) => (
|
||||
<tr key={name} className="bg-bunker-800 hover:bg-bunker-800/5 duration-100">
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300 capitalize">
|
||||
{name}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">{slug}</td>
|
||||
<td className="py-2 border-mineshaft-700 border-t flex">
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center mr-8">
|
||||
<Button
|
||||
onButtonPressed={() => handlePopUpOpen('createUpdateEnv', { name, slug })}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faPencil}
|
||||
/>
|
||||
</div>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||
<Button
|
||||
onButtonPressed={() => handlePopUpOpen('deleteEnv', { name, slug })}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className='text-center pt-7 pb-4 text-bunker-400'
|
||||
>
|
||||
<td colSpan={4} className="text-center pt-7 pb-4 text-bunker-400">
|
||||
No environmants found
|
||||
</td>
|
||||
</tr>
|
||||
@ -143,7 +118,7 @@ const EnvironmentTable = ({
|
||||
</tbody>
|
||||
</table>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp['deleteEnv'].isOpen}
|
||||
isOpen={popUp.deleteEnv.isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
|
||||
}?`}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||
import deleteServiceToken from '../../../pages/api/serviceToken/deleteServiceToken';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
@ -33,7 +33,7 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
||||
|
||||
return (
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div>
|
||||
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300 text-sm font-light">
|
||||
<tr>
|
||||
@ -41,49 +41,47 @@ const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTok
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
|
||||
<th></th>
|
||||
<th aria-label="button" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.length > 0 ? (
|
||||
data?.map((row) => {
|
||||
return (
|
||||
<tr
|
||||
key={guidGenerator()}
|
||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
||||
>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.name}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{workspaceName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.environment}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{new Date(row.expiresAt).toUTCString()}
|
||||
</td>
|
||||
<td className="py-2 border-mineshaft-700 border-t">
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
deleteServiceToken({ serviceTokenId: row._id} );
|
||||
setServiceTokens(data.filter(token => token._id != row._id));
|
||||
createNotification({
|
||||
text: `'${row.name}' token has been revoked.`,
|
||||
type: 'error'
|
||||
});
|
||||
}}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
data?.map((row) => (
|
||||
<tr
|
||||
key={guidGenerator()}
|
||||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
|
||||
>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.name}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{workspaceName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.environment}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{new Date(row.expiresAt).toUTCString()}
|
||||
</td>
|
||||
<td className="py-2 border-mineshaft-700 border-t">
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
|
||||
<Button
|
||||
onButtonPressed={() => {
|
||||
deleteServiceToken({ serviceTokenId: row._id });
|
||||
setServiceTokens(data.filter((token) => token._id !== row._id));
|
||||
createNotification({
|
||||
text: `'${row.name}' token has been revoked.`,
|
||||
type: 'error'
|
||||
});
|
||||
}}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
||||
|
@ -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;
|
219
frontend/src/components/basic/table/UserTable.tsx
Normal file
219
frontend/src/components/basic/table/UserTable.tsx
Normal file
@ -0,0 +1,219 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import deleteUserFromOrganization from '@app/pages/api/organization/deleteUserFromOrganization';
|
||||
import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace';
|
||||
import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
|
||||
import { decryptAssymmetric, encryptAssymmetric } from '../../utilities/cryptography/crypto';
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
import Listbox from '../Listbox';
|
||||
|
||||
// const roles = ['admin', 'user'];
|
||||
// TODO: Set type for this
|
||||
type Props = {
|
||||
userData: any[];
|
||||
changeData: (users: any[]) => void;
|
||||
myUser: string;
|
||||
filter: string;
|
||||
resendInvite: (email: string) => void;
|
||||
isOrg: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too.
|
||||
* #TODO: add the possibility of choosing and doing operations on multiple users.
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }: Props) => {
|
||||
const [roleSelected, setRoleSelected] = useState(
|
||||
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||
);
|
||||
const router = useRouter();
|
||||
const [myRole, setMyRole] = useState('member');
|
||||
|
||||
const workspaceId = router.query.id as string;
|
||||
// Delete the row in the table (e.g. a user)
|
||||
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
|
||||
const handleDelete = (membershipId: string, index: number) => {
|
||||
// setUserIdToBeDeleted(userId);
|
||||
// onClick();
|
||||
if (isOrg) {
|
||||
deleteUserFromOrganization(membershipId);
|
||||
} else {
|
||||
deleteUserFromWorkspace(membershipId);
|
||||
}
|
||||
changeData(userData.filter((v, i) => i !== index));
|
||||
setRoleSelected([
|
||||
...roleSelected.slice(0, index),
|
||||
...roleSelected.slice(index + 1, userData?.length)
|
||||
]);
|
||||
};
|
||||
|
||||
// Update the rold of a certain user
|
||||
const handleRoleUpdate = (index: number, e: string) => {
|
||||
changeUserRoleInWorkspace(userData[index].membershipId, e);
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
{
|
||||
key: userData[index].key,
|
||||
firstName: userData[index].firstName,
|
||||
lastName: userData[index].lastName,
|
||||
email: userData[index].email,
|
||||
role: e,
|
||||
status: userData[index].status,
|
||||
userId: userData[index].userId,
|
||||
membershipId: userData[index].membershipId,
|
||||
publicKey: userData[index].publicKey
|
||||
}
|
||||
],
|
||||
...userData.slice(index + 1, userData?.length)
|
||||
]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
|
||||
}, [userData, myUser]);
|
||||
|
||||
const grantAccess = async (id: string, publicKey: string) => {
|
||||
const result = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(workspaceId, id, ciphertext, nonce);
|
||||
router.reload();
|
||||
};
|
||||
|
||||
const deleteMembershipAndResendInvite = (email: string) => {
|
||||
// deleteUserFromWorkspace(membershipId);
|
||||
resendInvite(email);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
|
||||
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
|
||||
<table className="w-full my-0.5">
|
||||
<thead className="text-gray-400 text-sm font-light">
|
||||
<tr>
|
||||
<th className="text-left pl-6 py-3.5">FIRST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">LAST NAME</th>
|
||||
<th className="text-left pl-6 py-3.5">EMAIL</th>
|
||||
<th aria-label="buttons" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{userData?.filter(
|
||||
(user) =>
|
||||
user.firstName?.toLowerCase().includes(filter) ||
|
||||
user.lastName?.toLowerCase().includes(filter) ||
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
).length > 0 &&
|
||||
userData
|
||||
?.filter(
|
||||
(user) =>
|
||||
user.firstName?.toLowerCase().includes(filter) ||
|
||||
user.lastName?.toLowerCase().includes(filter) ||
|
||||
user.email?.toLowerCase().includes(filter)
|
||||
)
|
||||
.map((row, index) => (
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-800/5">
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.firstName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.lastName}
|
||||
</td>
|
||||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.email}
|
||||
</td>
|
||||
<td className="flex flex-row justify-end pr-8 py-2 border-t border-0.5 border-mineshaft-700">
|
||||
<div className="justify-end mr-6 mx-2 w-full h-full flex flex-row items-center">
|
||||
{row.status === 'granted' &&
|
||||
((myRole === 'admin' && row.role !== 'owner') || myRole === 'owner') &&
|
||||
myUser !== row.email ? (
|
||||
<Listbox
|
||||
isSelected={row.role}
|
||||
onChange={(e) => handleRoleUpdate(index, e)}
|
||||
data={
|
||||
myRole === 'owner' ? ['owner', 'admin', 'member'] : ['admin', 'member']
|
||||
}
|
||||
text="Role: "
|
||||
/>
|
||||
) : (
|
||||
row.status !== 'invited' &&
|
||||
row.status !== 'verified' && (
|
||||
<Listbox
|
||||
isSelected={row.role}
|
||||
text="Role: "
|
||||
onChange={() => {
|
||||
throw new Error('Function not implemented.');
|
||||
}}
|
||||
data={null}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{(row.status === 'invited' || row.status === 'verified') && (
|
||||
<div className="w-full pl-9">
|
||||
<Button
|
||||
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
|
||||
color="mineshaft"
|
||||
text="Resend Invite"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{row.status === 'completed' && myUser !== row.email && (
|
||||
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
|
||||
color="mineshaft"
|
||||
text="Grant Access"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{myUser !== row.email &&
|
||||
// row.role !== "admin" &&
|
||||
myRole !== 'member' ? (
|
||||
<div className="opacity-50 hover:opacity-100 flex items-center">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-9 h-9" />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserTable;
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import StripeRedirect from '~/pages/api/organization/StripeRedirect';
|
||||
import StripeRedirect from '@app/pages/api/organization/StripeRedirect';
|
||||
|
||||
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
|
||||
|
||||
@ -21,54 +21,43 @@ export default function Plan({ plan }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={`relative flex flex-col justify-between border-2 min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
||||
plan.name != 'Starter' && plan.current == true
|
||||
? 'border-primary'
|
||||
: 'border-chicago-700'
|
||||
plan.name !== 'Starter' && plan.current === true ? 'border-primary' : 'border-chicago-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex flex-row justify-between items-center relative z-10'>
|
||||
<p className={`px-6 py-4 text-3xl font-semibold text-gray-400`}>
|
||||
{plan.name}
|
||||
</p>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-between items-center relative z-10">
|
||||
<p className="px-6 py-4 text-3xl font-semibold text-gray-400">{plan.name}</p>
|
||||
</div>
|
||||
<div className='flex flwx-row items-end justify-start mb-4'>
|
||||
<p className='pl-6 text-3xl font-semibold text-primary'>
|
||||
{plan.price}
|
||||
</p>
|
||||
<p className='pl-3 mb-1 text-lg text-gray-400'>
|
||||
{plan.priceExplanation}
|
||||
</p>
|
||||
<div className="flex flwx-row items-end justify-start mb-4">
|
||||
<p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
|
||||
<p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
|
||||
</div>
|
||||
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'>
|
||||
{plan.text}
|
||||
</p>
|
||||
<p className='relative z-10 max-w-fit px-6 text-base text-gray-400'>
|
||||
{plan.subtext}
|
||||
</p>
|
||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
|
||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
|
||||
</div>
|
||||
<div className='flex flex-row items-center'>
|
||||
{plan.current == false ? (
|
||||
<div className="flex flex-row items-center">
|
||||
{plan.current === false ? (
|
||||
<>
|
||||
{plan.buttonTextMain == 'Schedule a Demo' ? (
|
||||
<a href='/scheduledemo' target='_blank rel="noopener"'>
|
||||
<div className='relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max'>
|
||||
{plan.buttonTextMain === 'Schedule a Demo' ? (
|
||||
<a href="/scheduledemo" target='_blank rel="noopener"'>
|
||||
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
|
||||
{plan.buttonTextMain}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div
|
||||
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-gray-600 text-gray-400 font-semibold ${
|
||||
plan.buttonTextMain == 'Downgrade'
|
||||
plan.buttonTextMain === 'Downgrade'
|
||||
? 'hover:bg-red hover:text-white hover:border-red'
|
||||
: 'hover:bg-primary hover:text-black hover:border-primary'
|
||||
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
StripeRedirect({
|
||||
orgId: tempLocalStorage('orgData.id'),
|
||||
orgId: tempLocalStorage('orgData.id')
|
||||
})
|
||||
}
|
||||
>
|
||||
@ -76,11 +65,8 @@ export default function Plan({ plan }: Props) {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<a
|
||||
href='https://infisical.com/pricing'
|
||||
target='_blank rel="noopener"'
|
||||
>
|
||||
<div className='relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5'>
|
||||
<a href="https://infisical.com/pricing" target='_blank rel="noopener"'>
|
||||
<div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
|
||||
{plan.buttonTextSecondary}
|
||||
</div>
|
||||
</a>
|
||||
@ -88,12 +74,10 @@ export default function Plan({ plan }: Props) {
|
||||
) : (
|
||||
<div
|
||||
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
|
||||
plan.name != 'Starter' && plan.current == true
|
||||
? 'bg-primary'
|
||||
: 'bg-chicago-700'
|
||||
plan.name !== 'Starter' && plan.current === true ? 'bg-primary' : 'bg-chicago-700'
|
||||
}`}
|
||||
>
|
||||
<p className='text-xs text-black font-semibold'>CURRENT PLAN</p>
|
||||
<p className="text-xs text-black font-semibold">CURRENT PLAN</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -2,26 +2,26 @@ import { useEffect, useRef } from 'react';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { Notification as NotificationType } from './NotificationProvider';
|
||||
type NotificationType = 'success' | 'error' | 'info';
|
||||
|
||||
export type TNotification = {
|
||||
text: string;
|
||||
type?: NotificationType;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
interface NotificationProps {
|
||||
notification: Required<NotificationType>;
|
||||
notification: Required<TNotification>;
|
||||
clearNotification: (text: string) => void;
|
||||
}
|
||||
|
||||
const Notification = ({
|
||||
notification,
|
||||
clearNotification
|
||||
}: NotificationProps) => {
|
||||
const Notification = ({ notification, clearNotification }: NotificationProps) => {
|
||||
const timeout = useRef<number>();
|
||||
|
||||
const handleClearNotification = () => clearNotification(notification.text);
|
||||
|
||||
const setNotifTimeout = () => {
|
||||
timeout.current = window.setTimeout(
|
||||
handleClearNotification,
|
||||
notification.timeoutMs
|
||||
);
|
||||
timeout.current = window.setTimeout(handleClearNotification, notification.timeoutMs);
|
||||
};
|
||||
|
||||
const cancelNotifTimeout = () => {
|
||||
@ -40,25 +40,21 @@ const Notification = ({
|
||||
role="alert"
|
||||
>
|
||||
{notification.type === 'error' && (
|
||||
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md"></div>
|
||||
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md" />
|
||||
)}
|
||||
{notification.type === 'success' && (
|
||||
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md"></div>
|
||||
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md" />
|
||||
)}
|
||||
{notification.type === 'info' && (
|
||||
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md"></div>
|
||||
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md" />
|
||||
)}
|
||||
<p className="text-bunker-200 text-sm font-semibold mt-0.5">
|
||||
{notification.text}
|
||||
</p>
|
||||
<p className="text-bunker-200 text-sm font-semibold mt-0.5">{notification.text}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg"
|
||||
onClick={() => clearNotification(notification.text)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-white pl-2 w-4 h-3 hover:text-red"
|
||||
icon={faX}
|
||||
/>
|
||||
<FontAwesomeIcon className="text-white pl-2 w-4 h-3 hover:text-red" icon={faX} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,17 +1,10 @@
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
import { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react';
|
||||
|
||||
import { TNotification } from './Notification';
|
||||
import Notifications from './Notifications';
|
||||
|
||||
type NotificationType = 'success' | 'error' | 'info';
|
||||
|
||||
export type Notification = {
|
||||
text: string;
|
||||
type?: NotificationType;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
type NotificationContextState = {
|
||||
createNotification: (newNotification: Notification) => void;
|
||||
createNotification: (newNotification: TNotification) => void;
|
||||
};
|
||||
|
||||
const NotificationContext = createContext<NotificationContextState>({
|
||||
@ -24,43 +17,33 @@ interface NotificationProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
// TODO: Migration to radix toast
|
||||
const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
||||
const [notifications, setNotifications] = useState<Required<Notification>[]>(
|
||||
[]
|
||||
const [notifications, setNotifications] = useState<Required<TNotification>[]>([]);
|
||||
|
||||
const clearNotification = (text: string) =>
|
||||
setNotifications((state) => state.filter((notif) => notif.text !== text));
|
||||
|
||||
const createNotification = useCallback(
|
||||
({ text, type = 'success', timeoutMs = 4000 }: TNotification) => {
|
||||
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
||||
|
||||
if (doesNotifExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newNotification: Required<TNotification> = { text, type, timeoutMs };
|
||||
|
||||
setNotifications((state) => [...state, newNotification]);
|
||||
},
|
||||
[notifications]
|
||||
);
|
||||
|
||||
const clearNotification = (text: string) => {
|
||||
return setNotifications((state) =>
|
||||
state.filter((notif) => notif.text !== text)
|
||||
);
|
||||
};
|
||||
|
||||
const createNotification = ({
|
||||
text,
|
||||
type = 'success',
|
||||
timeoutMs = 4000
|
||||
}: Notification) => {
|
||||
const doesNotifExist = notifications.some((notif) => notif.text === text);
|
||||
|
||||
if (doesNotifExist) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newNotification: Required<Notification> = { text, type, timeoutMs };
|
||||
|
||||
return setNotifications((state) => [...state, newNotification]);
|
||||
};
|
||||
const value = useMemo(() => ({ createNotification }), [createNotification]);
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{
|
||||
createNotification
|
||||
}}
|
||||
>
|
||||
<Notifications
|
||||
notifications={notifications}
|
||||
clearNotification={clearNotification}
|
||||
/>
|
||||
<NotificationContext.Provider value={value}>
|
||||
<Notifications notifications={notifications} clearNotification={clearNotification} />
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
|
@ -1,27 +1,19 @@
|
||||
import Notification from './Notification';
|
||||
import { Notification as NotificationType } from './NotificationProvider';
|
||||
import Notification, { TNotification } from './Notification';
|
||||
|
||||
interface NoticationsProps {
|
||||
notifications: Required<NotificationType>[];
|
||||
notifications: Required<TNotification>[];
|
||||
clearNotification: (text: string) => void;
|
||||
}
|
||||
|
||||
const Notifications = ({
|
||||
notifications,
|
||||
clearNotification
|
||||
}: NoticationsProps) => {
|
||||
const Notifications = ({ notifications, clearNotification }: NoticationsProps) => {
|
||||
if (!notifications.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hidden fixed z-50 md:flex md:flex-col-reverse bottom-1 gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
|
||||
<div className="hidden fixed z-50 md:flex md:flex-col-reverse gap-y-2 w-96 h-full right-2 bottom-2 pointer-events-none">
|
||||
{notifications.map((notif) => (
|
||||
<Notification
|
||||
key={notif.text}
|
||||
notification={notif}
|
||||
clearNotification={clearNotification}
|
||||
/>
|
||||
<Notification key={notif.text} notification={notif} clearNotification={clearNotification} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,20 +1,30 @@
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
/**
|
||||
* This is the text field where people can add comments to particular secrets.
|
||||
*/
|
||||
const CommentField = ({ comment, modifyComment, position }: { comment: string; modifyComment: (value: string, posistion: number) => void; position: number;}) => {
|
||||
const CommentField = ({
|
||||
comment,
|
||||
modifyComment,
|
||||
position
|
||||
}: {
|
||||
comment: string;
|
||||
modifyComment: (value: string, posistion: number) => void;
|
||||
position: number;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className={`relative mt-4 px-4 pt-6`}>
|
||||
<p className='text-sm text-bunker-300 pl-0.5'>{t("dashboard:sidebar.comments")}</p>
|
||||
<textarea
|
||||
className="bg-bunker-800 placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, position)}
|
||||
placeholder="Leave any comments here..."
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<div className="relative mt-4 px-4 pt-6">
|
||||
<p className="text-sm text-bunker-300 pl-0.5">{t('dashboard:sidebar.comments')}</p>
|
||||
<textarea
|
||||
className="placeholder:text-bunker-400 h-32 w-full bg-bunker-800 px-2 py-1.5 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={comment}
|
||||
onChange={(e) => modifyComment(e.target.value, position)}
|
||||
placeholder="Leave any comments here..."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentField;
|
||||
|
@ -40,14 +40,14 @@ const DashboardInputField = ({
|
||||
}: DashboardInputFieldProps) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
if (ref.current == null) return;
|
||||
if (ref.current === null) return;
|
||||
|
||||
ref.current.scrollTop = e.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = e.currentTarget.scrollLeft;
|
||||
};
|
||||
|
||||
if (type === 'varName') {
|
||||
const startsWithNumber = !isNaN(Number(value?.charAt(0))) && value != '';
|
||||
const startsWithNumber = !Number.isNaN(Number(value?.charAt(0))) && value !== '';
|
||||
const error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
@ -58,9 +58,7 @@ const DashboardInputField = ({
|
||||
} rounded-md`}
|
||||
>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
onChangeHandler(e.target.value.toUpperCase(), position)
|
||||
}
|
||||
onChange={(e) => onChangeHandler(e.target.value.toUpperCase(), position)}
|
||||
type={type}
|
||||
value={value}
|
||||
className={`z-10 peer font-mono ph-no-capture bg-bunker-800 rounded-md caret-white text-gray-400 text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 ${
|
||||
@ -81,13 +79,16 @@ const DashboardInputField = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'value') {
|
||||
}
|
||||
if (type === 'value') {
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<div
|
||||
className={`group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md`}
|
||||
>
|
||||
{override == true && <div className='bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md'>Override enabled</div>}
|
||||
<div className="group relative whitespace-pre flex flex-col justify-center w-full border border-mineshaft-500 rounded-md">
|
||||
{override === true && (
|
||||
<div className="bg-primary-300 absolute top-[0.1rem] right-[0.1rem] z-10 w-min text-xxs px-1 text-black opacity-80 rounded-md">
|
||||
Override enabled
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
@ -105,20 +106,18 @@ const DashboardInputField = ({
|
||||
blurred && !override
|
||||
? 'text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-400 peer-active:text-gray-400'
|
||||
: ''
|
||||
} ${
|
||||
override ? 'text-primary-300' : 'text-gray-400'
|
||||
}
|
||||
} ${override ? 'text-primary-300' : 'text-gray-400'}
|
||||
absolute flex flex-row whitespace-pre font-mono z-0 ph-no-capture overflow-x-scroll bg-bunker-800 h-9 rounded-md text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
>
|
||||
{value?.split(REGEX).map((word, id) => {
|
||||
if (word.match(REGEX) !== null) {
|
||||
return (
|
||||
<span className="ph-no-capture text-yellow" key={id}>
|
||||
<span className="ph-no-capture text-yellow" key={`${word}.${id + 1}`}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">
|
||||
{word.slice(2, word.length - 1)}
|
||||
</span>
|
||||
{word.slice(word.length - 1, word.length) == '}' ? (
|
||||
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
@ -129,13 +128,12 @@ const DashboardInputField = ({
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span key={id} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span key={`${word}_${id + 1}`} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{blurred && (
|
||||
@ -160,7 +158,14 @@ const DashboardInputField = ({
|
||||
};
|
||||
|
||||
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
|
||||
return (
|
||||
prev.value === next.value &&
|
||||
prev.type === next.type &&
|
||||
prev.position === next.position &&
|
||||
prev.blurred === next.blurred &&
|
||||
prev.override === next.override &&
|
||||
prev.isDuplicate === next.isDuplicate
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||
|
@ -8,7 +8,7 @@ type Props = {
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
export function DeleteActionButton({ onSubmit }: Props) {
|
||||
export const DeleteActionButton = ({ onSubmit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
@ -10,57 +9,49 @@ import downloadYaml from '../utilities/secrets/downloadYaml';
|
||||
|
||||
/**
|
||||
* This is the menu that is used to download secrets as .env ad .yml files (in future we may have more options)
|
||||
* @param {object} obj
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to downlaod
|
||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||
*/
|
||||
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Menu
|
||||
as="div"
|
||||
className="relative inline-block text-left"
|
||||
>
|
||||
<Menu.Button
|
||||
as="div"
|
||||
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
size="icon-md"
|
||||
icon={faDownload}
|
||||
onButtonPressed={() => {}}
|
||||
/>
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadDotEnv({ data, env })}
|
||||
size="md"
|
||||
text="Download as .env"
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadYaml({ data, env })}
|
||||
size="md"
|
||||
text="Download as .yml"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
}
|
||||
const DownloadSecretMenu = ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<Menu.Button
|
||||
as="div"
|
||||
className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
|
||||
>
|
||||
<Button color="mineshaft" size="icon-md" icon={faDownload} onButtonPressed={() => {}} />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[12rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none p-2 space-y-2">
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadDotEnv({ data, env })}
|
||||
size="md"
|
||||
text="Download as .env"
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
onButtonPressed={() => downloadYaml({ data, env })}
|
||||
size="md"
|
||||
text="Download as .yml"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadSecretMenu;
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { type ChangeEvent, type DragEvent, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { type ChangeEvent, type DragEvent, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { parseDocument, Scalar, YAMLMap } from 'yaml';
|
||||
|
||||
import Button from "../basic/buttons/Button";
|
||||
import Error from "../basic/Error";
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Error from '../basic/Error';
|
||||
import { parseDotEnv } from '../utilities/parseDotEnv';
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
|
||||
interface DropZoneProps {
|
||||
// TODO: change Data type from any
|
||||
@ -28,7 +29,7 @@ const DropZone = ({
|
||||
errorDragAndDrop,
|
||||
setButtonReady,
|
||||
keysExist,
|
||||
numCurrentRows,
|
||||
numCurrentRows
|
||||
}: DropZoneProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -47,7 +48,7 @@ const DropZone = ({
|
||||
e.stopPropagation();
|
||||
|
||||
// set dropEffect to copy i.e copy of the source item
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
};
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -57,16 +58,14 @@ const DropZone = ({
|
||||
switch (fileType) {
|
||||
case 'env': {
|
||||
const keyPairs = parseDotEnv(file);
|
||||
secrets = Object.keys(keyPairs).map((key, index) => {
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key: key,
|
||||
value: keyPairs[key as keyof typeof keyPairs].value,
|
||||
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
|
||||
type: 'shared',
|
||||
};
|
||||
});
|
||||
secrets = Object.keys(keyPairs).map((key, index) => ({
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key,
|
||||
value: keyPairs[key as keyof typeof keyPairs].value,
|
||||
comment: keyPairs[key as keyof typeof keyPairs].comments.join('\n'),
|
||||
type: 'shared'
|
||||
}));
|
||||
break;
|
||||
}
|
||||
case 'yml': {
|
||||
@ -79,15 +78,15 @@ const DropZone = ({
|
||||
fileContent!.items
|
||||
.find((item) => item.key.value === key)
|
||||
?.key?.commentBefore?.split('\n')
|
||||
.map((comment) => comment.trim())
|
||||
.map((cmnt) => cmnt.trim())
|
||||
.join('\n') ?? '';
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key: key,
|
||||
key,
|
||||
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
|
||||
comment,
|
||||
type: 'shared',
|
||||
type: 'shared'
|
||||
};
|
||||
});
|
||||
break;
|
||||
@ -105,7 +104,7 @@ const DropZone = ({
|
||||
setTimeout(() => setLoading(false), 5000);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
const reader = new FileReader();
|
||||
@ -149,16 +148,11 @@ const DropZone = ({
|
||||
|
||||
return loading ? (
|
||||
<div className="flex items-center justify-center pt-16 mb-16">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
alt="google logo"
|
||||
></Image>
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="google logo" />
|
||||
</div>
|
||||
) : keysExist ? (
|
||||
<div
|
||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 outline max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
|
||||
className="opacity-60 hover:opacity-100 duration-200 relative bg-mineshaft-900 max-w-[calc(100%-1rem)] w-full outline-dashed outline-chicago-600 rounded-md outline-2 flex flex-col items-center justify-center mb-16 mx-auto mt-1 py-8 px-2"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
@ -171,36 +165,27 @@ const DropZone = ({
|
||||
accept=".txt,.env,.yml"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
{errorDragAndDrop ? (
|
||||
<div className="my-3 max-w-xl opacity-80"></div>
|
||||
) : (
|
||||
<div className=""></div>
|
||||
)}
|
||||
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
|
||||
<div className="flex flex-row">
|
||||
<FontAwesomeIcon
|
||||
icon={faUpload}
|
||||
className="text-bunker-300 text-3xl mr-6"
|
||||
/>
|
||||
<p className="text-bunker-300 mt-1">{t("common:drop-zone-keys")}</p>
|
||||
<FontAwesomeIcon icon={faUpload} className="text-bunker-300 text-3xl mr-6" />
|
||||
<p className="text-bunker-300 mt-1">{t('common:drop-zone-keys')}</p>
|
||||
</div>
|
||||
{errorDragAndDrop ? (
|
||||
{errorDragAndDrop && (
|
||||
<div className="mt-8 max-w-xl opacity-80">
|
||||
<Error text="Something went wrong! Make sure you drag the file directly from the folder in which it is located (e.g., not VS code). Tip: click 'Reveal in Finder/Explorer'" />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker outline max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4"
|
||||
className="opacity-80 hover:opacity-100 duration-200 relative bg-bunker max-w-2xl w-full outline-dashed outline-gray-700 rounded-md outline-2 flex flex-col items-center justify-center pt-16 mb-16 px-4"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
|
||||
<p className="">{t("common:drop-zone")}</p>
|
||||
<p className="">{t('common:drop-zone')}</p>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
@ -209,14 +194,14 @@ const DropZone = ({
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<div className="flex flex-row w-full items-center justify-center mb-6 mt-5">
|
||||
<div className="border-t border-gray-700 w-1/5"></div>
|
||||
<div className="border-t border-gray-700 w-1/5" />
|
||||
<p className="text-gray-400 text-xs mx-4">OR</p>
|
||||
<div className="border-t border-gray-700 w-1/5"></div>
|
||||
<div className="border-t border-gray-700 w-1/5" />
|
||||
</div>
|
||||
<div className="z-10 mb-6">
|
||||
<Button
|
||||
color="mineshaft"
|
||||
text={String(t("dashboard:add-secret"))}
|
||||
text={String(t('dashboard:add-secret'))}
|
||||
onButtonPressed={createNewFile}
|
||||
size="md"
|
||||
/>
|
||||
|
@ -1,95 +1,106 @@
|
||||
import { Fragment,useState } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faShuffle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
|
||||
/**
|
||||
* This is the menu that is used to (re)generate secrets (currently we only have ranom hex, in future we will have more options)
|
||||
* @returns the popup-menu for randomly generating secrets
|
||||
*/
|
||||
const GenerateSecretMenu = ({ modifyValue, position }: { modifyValue: (value: string, position: number) => void; position: number; }) => {
|
||||
const GenerateSecretMenu = ({
|
||||
modifyValue,
|
||||
position
|
||||
}: {
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
position: number;
|
||||
}) => {
|
||||
const [randomStringLength, setRandomStringLength] = useState(32);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className='py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500'>
|
||||
<FontAwesomeIcon icon={faShuffle} className='text-bunker-300'/>
|
||||
</div>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||
<div
|
||||
onClick={() => {
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
position
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={faShuffle}
|
||||
/>
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>{t("dashboard:sidebar.generate-random-hex")}</p>
|
||||
<p>{t("dashboard:sidebar.digits")}</p>
|
||||
return (
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<div className="py-1 px-2 rounded-md bg-bunker-800 hover:bg-bunker-500">
|
||||
<FontAwesomeIcon icon={faShuffle} className="text-bunker-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-50 drop-shadow-xl right-0 mt-0.5 w-[20rem] origin-top-right rounded-md bg-bunker border border-mineshaft-500 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none px-1 py-1">
|
||||
<div
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
if (randomStringLength > 32) {
|
||||
setRandomStringLength(32);
|
||||
} else if (randomStringLength < 2) {
|
||||
setRandomStringLength(2);
|
||||
} else {
|
||||
modifyValue(
|
||||
[...Array(randomStringLength)]
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join(''),
|
||||
position
|
||||
);
|
||||
}
|
||||
}}
|
||||
className="relative flex flex-row justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full"
|
||||
>
|
||||
-
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faShuffle} />
|
||||
<div className="text-sm justify-between flex flex-row w-full">
|
||||
<p>{t('dashboard:sidebar.generate-random-hex')}</p>
|
||||
<p>{t('dashboard:sidebar.digits')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
setRandomStringLength(parseInt(e.target.value))
|
||||
}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) => setRandomStringLength(parseInt(e.target.value, 10))}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateSecretMenu;
|
||||
|
@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
@ -7,9 +6,9 @@ import DashboardInputField from './DashboardInputField';
|
||||
|
||||
interface KeyPairProps {
|
||||
keyPair: SecretDataProps;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string, position: number) => void;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string, position: number) => void;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
@ -28,7 +27,7 @@ interface KeyPairProps {
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
||||
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
@ -41,48 +40,61 @@ const KeyPair = ({
|
||||
toggleSidebar,
|
||||
sidebarSecretId,
|
||||
isSnapshot
|
||||
}: KeyPairProps) => {
|
||||
return (
|
||||
<div className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && "pointer-events-none"} ${keyPair.id == sidebarSecretId && "bg-mineshaft-500 duration-200"} rounded-md`}>
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||
{keyPair.valueOverride && <div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<div className='w-1 h-1 rounded-full bg-primary z-40'></div>
|
||||
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||
This secret is overriden
|
||||
</span>
|
||||
</div>}
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
/>
|
||||
</div>
|
||||
}: KeyPairProps) => (
|
||||
<div
|
||||
className={`mx-1 flex flex-col items-center ml-1 ${isSnapshot && 'pointer-events-none'} ${
|
||||
keyPair.id === sidebarSecretId && 'bg-mineshaft-500 duration-200'
|
||||
} rounded-md`}
|
||||
>
|
||||
<div className="relative flex flex-row justify-between w-full max-w-5xl mr-auto max-h-14 my-1 items-start px-1">
|
||||
{keyPair.valueOverride && (
|
||||
<div className="group font-normal group absolute top-[1rem] left-[0.2rem] z-40 inline-block text-gray-300 underline hover:text-primary duration-200">
|
||||
<div className="w-1 h-1 rounded-full bg-primary z-40" />
|
||||
<span className="absolute z-50 hidden group-hover:flex group-hover:animate-popdown duration-200 w-[10.5rem] -left-[0.4rem] -top-[1.7rem] translate-y-full px-2 py-2 bg-mineshaft-500 rounded-b-md rounded-r-md text-center text-gray-100 text-sm after:content-[''] after:absolute after:left-0 after:bottom-[100%] after:-translate-x-0 after:border-8 after:border-x-transparent after:border-t-transparent after:border-b-mineshaft-500">
|
||||
This secret is overriden
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full min-w-xl">
|
||||
<div className={`flex min-w-xl items-center ${!isSnapshot && "pr-1.5"} rounded-lg mt-4 md:mt-0 max-h-10`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
|
||||
blurred={isBlurred}
|
||||
override={Boolean(keyPair.valueOverride)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isSnapshot && <div onClick={() => toggleSidebar(keyPair.id)} className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200">
|
||||
<FontAwesomeIcon
|
||||
className="text-gray-300 px-2.5 text-lg mt-0.5"
|
||||
icon={faEllipsis}
|
||||
)}
|
||||
<div className="min-w-xl w-96">
|
||||
<div className="flex pr-1.5 items-center rounded-lg mt-4 md:mt-0 max-h-16">
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.key}
|
||||
isDuplicate={isDuplicate}
|
||||
/>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full min-w-xl">
|
||||
<div
|
||||
className={`flex min-w-xl items-center ${
|
||||
!isSnapshot && 'pr-1.5'
|
||||
} rounded-lg mt-4 md:mt-0 max-h-10`}
|
||||
>
|
||||
<DashboardInputField
|
||||
onChangeHandler={keyPair.valueOverride ? modifyValueOverride : modifyValue}
|
||||
type="value"
|
||||
position={keyPair.pos}
|
||||
value={keyPair.valueOverride ? keyPair.valueOverride : keyPair.value}
|
||||
blurred={isBlurred}
|
||||
override={Boolean(keyPair.valueOverride)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isSnapshot && (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleSidebar(keyPair.id)}
|
||||
className="cursor-pointer w-[2.35rem] h-[2.35rem] bg-mineshaft-700 hover:bg-chicago-700 rounded-md flex flex-row justify-center items-center duration-200"
|
||||
>
|
||||
<FontAwesomeIcon className="text-gray-300 px-2.5 text-lg mt-0.5" icon={faEllipsis} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
);
|
||||
|
||||
export default KeyPair;
|
||||
export default KeyPair;
|
||||
|
@ -1,10 +1,12 @@
|
||||
/* eslint-disable react/no-unused-prop-types */
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import SecretVersionList from '@app/ee/components/SecretVersionList';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import SecretVersionList from '@app/ee/components/SecretVersionList';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import CommentField from './CommentField';
|
||||
@ -12,7 +14,6 @@ import DashboardInputField from './DashboardInputField';
|
||||
import { DeleteActionButton } from './DeleteActionButton';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
@ -22,22 +23,18 @@ interface SecretProps {
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
valueOverride: string;
|
||||
}
|
||||
export interface DeleteRowFunctionProps {
|
||||
ids: string[];
|
||||
secretName: string;
|
||||
export interface DeleteRowFunctionProps {
|
||||
ids: string[];
|
||||
secretName: string;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
toggleSidebar: (value: string) => void;
|
||||
data: SecretProps[];
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string | undefined, position: number) => void;
|
||||
modifyComment: (value: string, position: number) => void;
|
||||
modifyKey: (value: string, position: number) => void;
|
||||
modifyValue: (value: string, position: number) => void;
|
||||
modifyValueOverride: (value: string | undefined, position: number) => void;
|
||||
modifyComment: (value: string, position: number) => void;
|
||||
buttonReady: boolean;
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
@ -57,112 +54,140 @@ interface SideBarProps {
|
||||
* @param {function} obj.deleteRow - a function to delete a certain keyPair
|
||||
* @returns the sidebar with 'secret's settings'
|
||||
*/
|
||||
const SideBar = ({
|
||||
toggleSidebar,
|
||||
data,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyValueOverride,
|
||||
const SideBar = ({
|
||||
toggleSidebar,
|
||||
data,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyValueOverride,
|
||||
modifyComment,
|
||||
buttonReady,
|
||||
buttonReady,
|
||||
savePush,
|
||||
deleteRow
|
||||
}: SideBarProps) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride != undefined);
|
||||
const [overrideEnabled, setOverrideEnabled] = useState(data[0].valueOverride !== undefined);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return <div className='absolute border-l border-mineshaft-500 bg-bunker fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between'>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
return (
|
||||
<div className="absolute border-l border-mineshaft-500 bg-bunker h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("dashboard:sidebar.secret")}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("None")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-4 px-4 pointer-events-none'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.key")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
{data[0]?.value
|
||||
? <div className={`relative mt-2 px-4 ${overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.value")}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data[0].pos}
|
||||
value={data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
: <div className='px-4 text-sm text-bunker-300 pt-4'>
|
||||
<span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1'>{t("common:note")}:</span>
|
||||
{t("dashboard:sidebar.personal-explanation")}
|
||||
</div>}
|
||||
<div className='mt-4 px-4'>
|
||||
{data[0]?.value &&
|
||||
<div className='flex flex-row items-center justify-between my-2 pl-1 pr-2'>
|
||||
<p className='text-sm text-bunker-300'>{t("dashboard:sidebar.override")}</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={modifyValueOverride}
|
||||
pos={data[0]?.pos}
|
||||
/>
|
||||
</div>}
|
||||
<div className={`relative ${!overrideEnabled && "opacity-40 pointer-events-none"} duration-200`}>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValueOverride}
|
||||
type="value"
|
||||
position={data[0]?.pos}
|
||||
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred={true}
|
||||
/>
|
||||
<div className='absolute right-[0.57rem] top-[0.3rem] z-50'>
|
||||
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
|
||||
<div className="h-min overflow-y-auto">
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t('dashboard:sidebar.secret')}</p>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="p-1"
|
||||
onClick={() => toggleSidebar('None')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 px-4 pointer-events-none">
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.key')}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyKey}
|
||||
type="varName"
|
||||
position={data[0]?.pos}
|
||||
value={data[0]?.key}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
{data[0]?.value ? (
|
||||
<div
|
||||
className={`relative mt-2 px-4 ${
|
||||
overrideEnabled && 'opacity-40 pointer-events-none'
|
||||
} duration-200`}
|
||||
>
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.value')}</p>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValue}
|
||||
type="value"
|
||||
position={data[0].pos}
|
||||
value={data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
|
||||
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 text-sm text-bunker-300 pt-4">
|
||||
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1">
|
||||
{t('common:note')}:
|
||||
</span>
|
||||
{t('dashboard:sidebar.personal-explanation')}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 px-4">
|
||||
{data[0]?.value && (
|
||||
<div className="flex flex-row items-center justify-between my-2 pl-1 pr-2">
|
||||
<p className="text-sm text-bunker-300">{t('dashboard:sidebar.override')}</p>
|
||||
<Toggle
|
||||
enabled={overrideEnabled}
|
||||
setEnabled={setOverrideEnabled}
|
||||
addOverride={modifyValueOverride}
|
||||
pos={data[0]?.pos}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`relative ${
|
||||
!overrideEnabled && 'opacity-40 pointer-events-none'
|
||||
} duration-200`}
|
||||
>
|
||||
<DashboardInputField
|
||||
onChangeHandler={modifyValueOverride}
|
||||
type="value"
|
||||
position={data[0]?.pos}
|
||||
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
|
||||
isDuplicate={false}
|
||||
blurred
|
||||
/>
|
||||
<div className="absolute right-[0.57rem] top-[0.3rem] z-50">
|
||||
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField
|
||||
comment={data[0]?.comment}
|
||||
modifyComment={modifyComment}
|
||||
position={data[0]?.pos}
|
||||
/>
|
||||
</div>
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
||||
)}
|
||||
<div className="flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]">
|
||||
<Button
|
||||
text={String(t('common:save-changes'))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
<DeleteActionButton
|
||||
onSubmit={() =>
|
||||
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`flex justify-start max-w-sm mt-4 px-4 mt-full mb-[4.7rem]`}>
|
||||
<Button
|
||||
text={String(t("common:save-changes"))}
|
||||
onButtonPressed={savePush}
|
||||
color="primary"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
textDisabled="Saved"
|
||||
/>
|
||||
<DeleteActionButton
|
||||
onSubmit={() => deleteRow({ ids: data.map(secret => secret.id), secretName: data[0]?.key })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideBar;
|
||||
|
@ -21,12 +21,8 @@ interface IntegrationAuth {
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOption: CloudIntegrationOption;
|
||||
setSelectedIntegrationOption: (
|
||||
cloudIntegration: CloudIntegrationOption
|
||||
) => void;
|
||||
integrationOptionPress: (
|
||||
cloudIntegrationOption: CloudIntegrationOption
|
||||
) => void;
|
||||
setSelectedIntegrationOption: (cloudIntegration: CloudIntegrationOption) => void;
|
||||
integrationOptionPress: (cloudIntegrationOption: CloudIntegrationOption) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
}
|
||||
|
||||
@ -34,11 +30,14 @@ const CloudIntegration = ({
|
||||
cloudIntegrationOption,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths,
|
||||
integrationAuths
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
return integrationAuths ? (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`relative ${
|
||||
cloudIntegrationOption.isAvailable
|
||||
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
||||
@ -55,18 +54,17 @@ const CloudIntegration = ({
|
||||
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
||||
height={70}
|
||||
width={70}
|
||||
alt='integration logo'
|
||||
alt="integration logo"
|
||||
/>
|
||||
{cloudIntegrationOption.name.split(' ').length > 2 ? (
|
||||
<div className='font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs'>
|
||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-3xl ml-4 max-w-xs">
|
||||
<div>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
||||
<div className='text-base'>
|
||||
{cloudIntegrationOption.name.split(' ')[1]}{' '}
|
||||
{cloudIntegrationOption.name.split(' ')[2]}
|
||||
<div className="text-base">
|
||||
{cloudIntegrationOption.name.split(' ')[1]} {cloudIntegrationOption.name.split(' ')[2]}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs'>
|
||||
<div className="font-semibold text-gray-300 group-hover:text-gray-200 duration-200 text-xl ml-4 max-w-xs">
|
||||
{cloudIntegrationOption.name}
|
||||
</div>
|
||||
)}
|
||||
@ -74,43 +72,45 @@ const CloudIntegration = ({
|
||||
integrationAuths
|
||||
.map((authorization) => authorization.integration)
|
||||
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
||||
<div className='absolute group z-40 top-0 right-0 flex flex-row'>
|
||||
<div className="absolute group z-40 top-0 right-0 flex flex-row">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteIntegrationAuth({
|
||||
integrationAuthId: integrationAuths
|
||||
.filter(
|
||||
(authorization) =>
|
||||
authorization.integration ==
|
||||
cloudIntegrationOption.name.toLowerCase()
|
||||
authorization.integration === cloudIntegrationOption.name.toLowerCase()
|
||||
)
|
||||
.map((authorization) => authorization._id)[0],
|
||||
.map((authorization) => authorization._id)[0]
|
||||
});
|
||||
|
||||
router.reload();
|
||||
}}
|
||||
className='cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200'
|
||||
className="cursor-pointer w-max bg-red py-0.5 px-2 rounded-b-md text-xs flex flex-row items-center opacity-0 group-hover:opacity-100 duration-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className='text-xs mr-2 py-px' />
|
||||
<FontAwesomeIcon icon={faX} className="text-xs mr-2 py-px" />
|
||||
Revoke
|
||||
</div>
|
||||
<div className='w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200'>
|
||||
<FontAwesomeIcon icon={faCheck} className='text-xs mr-2' />
|
||||
<div className="w-max bg-primary py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90 group-hover:opacity-100 duration-200">
|
||||
<FontAwesomeIcon icon={faCheck} className="text-xs mr-2" />
|
||||
Authorized
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!cloudIntegrationOption.isAvailable && (
|
||||
<div className='absolute group z-50 top-0 right-0 flex flex-row'>
|
||||
<div className='w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90'>
|
||||
<div className="absolute group z-50 top-0 right-0 flex flex-row">
|
||||
<div className="w-max bg-yellow py-0.5 px-2 rounded-bl-md rounded-tr-md text-xs flex flex-row items-center text-black opacity-90">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -29,7 +29,7 @@ const CloudIntegrationSection = ({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2`}
|
||||
className="flex flex-col justify-between items-start m-4 mt-7 text-xl max-w-5xl px-2"
|
||||
>
|
||||
<h1 className='font-semibold text-3xl'>
|
||||
{t('integrations:cloud-integrations')}
|
||||
|
@ -7,33 +7,29 @@ interface Framework {
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
const FrameworkIntegration = ({ framework }: { framework: Framework }) => {
|
||||
return (
|
||||
<a
|
||||
href={framework.docsLink}
|
||||
rel='noopener'
|
||||
className={`relative flex flex-row items-center justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer`}
|
||||
const FrameworkIntegration = ({ framework }: { framework: Framework }) => (
|
||||
<a
|
||||
href={framework.docsLink}
|
||||
rel="noopener"
|
||||
className="relative flex flex-row justify-center bg-bunker-500 hover:bg-gradient-to-tr duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={`hover:bg-white/10 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
||||
framework?.name?.split(' ').length > 1 ? 'text-sm px-1' : 'text-xl px-2'
|
||||
} text-center w-full max-w-xs`}
|
||||
>
|
||||
<div
|
||||
className={`hover:bg-white/10 duration-200 cursor-pointer font-semibold bg-bunker-500 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
||||
framework?.name?.split(' ').length > 1
|
||||
? 'text-sm px-1'
|
||||
: 'text-xl px-2'
|
||||
} text-center w-full max-w-xs`}
|
||||
>
|
||||
{framework?.image && (
|
||||
<Image
|
||||
src={`/images/integrations/${framework.image}.png`}
|
||||
height={framework?.name ? 60 : 90}
|
||||
width={framework?.name ? 60 : 90}
|
||||
alt='integration logo'
|
||||
></Image>
|
||||
)}
|
||||
{framework?.name && framework?.image && <div className='h-2'></div>}
|
||||
{framework?.name && framework.name}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
{framework?.image && (
|
||||
<Image
|
||||
src={`/images/integrations/${framework.image}.png`}
|
||||
height={framework?.name ? 60 : 90}
|
||||
width={framework?.name ? 60 : 90}
|
||||
alt="integration logo"
|
||||
/>
|
||||
)}
|
||||
{framework?.name && framework?.image && <div className="h-2" />}
|
||||
{framework?.name && framework.name}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
export default FrameworkIntegration;
|
||||
|
@ -1,33 +1,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowRight,
|
||||
faRotate,
|
||||
faX,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
// TODO: This needs to be moved from public folder
|
||||
import {
|
||||
contextNetlifyMapping,
|
||||
reverseContextNetlifyMapping,
|
||||
} from "public/data/frequentConstants";
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faArrowRight, faRotate, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// TODO: This needs to be moved from public folder
|
||||
import { contextNetlifyMapping, reverseContextNetlifyMapping } from 'public/data/frequentConstants';
|
||||
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import ListBox from "~/components/basic/Listbox";
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import ListBox from '@app/components/basic/Listbox';
|
||||
|
||||
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration"
|
||||
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps";
|
||||
import updateIntegration from "../../pages/api/integrations/updateIntegration"
|
||||
import deleteIntegration from '../../pages/api/integrations/DeleteIntegration';
|
||||
import getIntegrationApps from '../../pages/api/integrations/GetIntegrationApps';
|
||||
import updateIntegration from '../../pages/api/integrations/updateIntegration';
|
||||
|
||||
interface Integration {
|
||||
_id: string;
|
||||
app?: string;
|
||||
target?: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
integrationAuth: string;
|
||||
isActive: boolean;
|
||||
context: string;
|
||||
interface TIntegration {
|
||||
_id: string;
|
||||
app?: string;
|
||||
target?: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
integrationAuth: string;
|
||||
isActive: boolean;
|
||||
context: string;
|
||||
}
|
||||
|
||||
interface IntegrationApp {
|
||||
@ -36,219 +30,190 @@ interface IntegrationApp {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
integration: Integration;
|
||||
integration: TIntegration;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
};
|
||||
|
||||
const Integration = ({
|
||||
integration,
|
||||
environments = []
|
||||
}:Props ) => {
|
||||
// set initial environment. This find will only execute when component is mounting
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<
|
||||
Props['environments'][0]
|
||||
>(
|
||||
environments.find(({ slug }) => slug === integration.environment) || {
|
||||
name: '',
|
||||
slug: '',
|
||||
}
|
||||
);
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||
const [integrationApp, setIntegrationApp] = useState(""); // integration app name
|
||||
const [integrationTarget, setIntegrationTarget] = useState(""); // vercel-specific integration param
|
||||
const [integrationContext, setIntegrationContext] = useState(""); // netlify-specific integration param
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const loadIntegration = async () => {
|
||||
interface App {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||
integrationAuthId: integration.integrationAuth,
|
||||
});
|
||||
|
||||
setApps(tempApps);
|
||||
setIntegrationApp(
|
||||
integration.app ? integration.app : tempApps[0].name
|
||||
);
|
||||
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
setIntegrationTarget(
|
||||
integration?.target
|
||||
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
||||
: "Development"
|
||||
);
|
||||
break;
|
||||
case "netlify":
|
||||
setIntegrationContext(integration?.context ? contextNetlifyMapping[integration.context] : "Local development");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
loadIntegration();
|
||||
}, []);
|
||||
|
||||
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
return (
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">
|
||||
ENVIRONMENT
|
||||
</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? [
|
||||
"Development",
|
||||
"Preview",
|
||||
"Production"
|
||||
] : null}
|
||||
selected={integrationTarget}
|
||||
onChange={setIntegrationTarget}
|
||||
isFull={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "netlify":
|
||||
return (
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2">
|
||||
CONTEXT
|
||||
</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? [
|
||||
"Production",
|
||||
"Deploy previews",
|
||||
"Branch deploys",
|
||||
"Local development"
|
||||
] : null}
|
||||
selected={integrationContext}
|
||||
onChange={setIntegrationContext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div></div>;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
const Integration = ({ integration, environments = [] }: Props) => {
|
||||
// set initial environment. This find will only execute when component is mounting
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props['environments'][0]>(
|
||||
environments.find(({ slug }) => slug === integration.environment) || {
|
||||
name: '',
|
||||
slug: ''
|
||||
}
|
||||
|
||||
if (!integrationApp || apps.length === 0) return <div></div>
|
||||
|
||||
return (
|
||||
<div className='max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between'>
|
||||
<div className='flex'>
|
||||
<div>
|
||||
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
||||
ENVIRONMENT
|
||||
</p>
|
||||
<ListBox
|
||||
data={
|
||||
!integration.isActive
|
||||
? environments.map(({ name }) => name)
|
||||
: null
|
||||
}
|
||||
selected={integrationEnvironment.name}
|
||||
onChange={(envName) =>
|
||||
setIntegrationEnvironment(
|
||||
environments.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull={true}
|
||||
/>
|
||||
</div>
|
||||
<div className='pt-2'>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className='mx-4 text-gray-400 mt-8'
|
||||
/>
|
||||
</div>
|
||||
<div className='mr-2'>
|
||||
<p className='text-gray-400 text-xs font-semibold mb-2'>
|
||||
INTEGRATION
|
||||
</p>
|
||||
<div className='py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300'>
|
||||
{integration.integration.charAt(0).toUpperCase() +
|
||||
integration.integration.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mr-2'>
|
||||
<div className='text-gray-400 text-xs font-semibold mb-2'>APP</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
selected={integrationApp}
|
||||
onChange={(app) => {
|
||||
setIntegrationApp(app);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className='flex items-end'>
|
||||
{integration.isActive ? (
|
||||
<div className='max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4'>
|
||||
<FontAwesomeIcon
|
||||
icon={faRotate}
|
||||
className='text-lg mr-2.5 text-primary animate-spin'
|
||||
);
|
||||
const [fileState, setFileState] = useState([]);
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||
const [integrationApp, setIntegrationApp] = useState(''); // integration app name
|
||||
const [integrationTarget, setIntegrationTarget] = useState(''); // vercel-specific integration param
|
||||
const [integrationContext, setIntegrationContext] = useState(''); // netlify-specific integration param
|
||||
|
||||
useEffect(() => {
|
||||
const loadIntegration = async () => {
|
||||
interface App {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||
integrationAuthId: integration.integrationAuth
|
||||
});
|
||||
|
||||
setApps(tempApps);
|
||||
setIntegrationApp(integration.app ? integration.app : tempApps[0].name);
|
||||
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
setIntegrationTarget(
|
||||
integration?.target
|
||||
? integration.target.charAt(0).toUpperCase() + integration.target.substring(1)
|
||||
: 'Development'
|
||||
);
|
||||
break;
|
||||
case 'netlify':
|
||||
setIntegrationContext(
|
||||
integration?.context ? contextNetlifyMapping[integration.context] : 'Local development'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
loadIntegration();
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const renderIntegrationSpecificParams = (integration: TIntegration) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case 'vercel':
|
||||
return (
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2 w-60">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? ['Development', 'Preview', 'Production'] : null}
|
||||
isSelected={integrationTarget}
|
||||
onChange={setIntegrationTarget}
|
||||
isFull
|
||||
/>
|
||||
<div className='text-gray-300 font-semibold'>In Sync</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
text='Start Integration'
|
||||
onButtonPressed={async () => {
|
||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
||||
);
|
||||
case 'netlify':
|
||||
return (
|
||||
<div>
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2">CONTEXT</div>
|
||||
<ListBox
|
||||
data={
|
||||
!integration.isActive
|
||||
? ['Production', 'Deploy previews', 'Branch deploys', 'Local development']
|
||||
: null
|
||||
}
|
||||
isSelected={integrationContext}
|
||||
onChange={setIntegrationContext}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div />;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
await updateIntegration({
|
||||
integrationId: integration._id,
|
||||
environment: integrationEnvironment.slug,
|
||||
app: integrationApp,
|
||||
isActive: true,
|
||||
target: integrationTarget
|
||||
? integrationTarget.toLowerCase()
|
||||
: null,
|
||||
context: integrationContext
|
||||
? reverseContextNetlifyMapping[integrationContext]
|
||||
: null,
|
||||
siteId,
|
||||
});
|
||||
|
||||
router.reload();
|
||||
}}
|
||||
color='mineshaft'
|
||||
size='md'
|
||||
/>
|
||||
)}
|
||||
<div className='opacity-50 hover:opacity-100 duration-200 ml-2'>
|
||||
<Button
|
||||
onButtonPressed={async () => {
|
||||
await deleteIntegration({
|
||||
integrationId: integration._id,
|
||||
});
|
||||
router.reload();
|
||||
}}
|
||||
color='red'
|
||||
size='icon-md'
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default Integration;
|
||||
if (!integrationApp || apps.length === 0) return <div />;
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl p-6 mx-6 mb-8 rounded-md bg-white/5 flex justify-between">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">ENVIRONMENT</p>
|
||||
<ListBox
|
||||
data={!integration.isActive ? environments.map(({ name }) => name) : null}
|
||||
isSelected={integrationEnvironment.name}
|
||||
onChange={(envName) =>
|
||||
setIntegrationEnvironment(
|
||||
environments.find(({ name }) => envName === name) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown'
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<FontAwesomeIcon icon={faArrowRight} className="mx-4 text-gray-400 mt-8" />
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<p className="text-gray-400 text-xs font-semibold mb-2">INTEGRATION</p>
|
||||
<div className="py-2.5 bg-white/[.07] rounded-md pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||
{integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<div className="text-gray-400 text-xs font-semibold mb-2">APP</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
isSelected={integrationApp}
|
||||
onChange={(app) => {
|
||||
setIntegrationApp(app);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
{integration.isActive ? (
|
||||
<div className="max-w-5xl flex flex-row items-center bg-white/5 p-2 rounded-md px-4">
|
||||
<FontAwesomeIcon icon={faRotate} className="text-lg mr-2.5 text-primary animate-spin" />
|
||||
<div className="text-gray-300 font-semibold">In Sync</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
text="Start Integration"
|
||||
onButtonPressed={async () => {
|
||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||
const siteId = siteApp?.siteId ? siteApp.siteId : null;
|
||||
|
||||
await updateIntegration({
|
||||
integrationId: integration._id,
|
||||
environment: integrationEnvironment.slug,
|
||||
app: integrationApp,
|
||||
isActive: true,
|
||||
target: integrationTarget ? integrationTarget.toLowerCase() : null,
|
||||
context: integrationContext
|
||||
? reverseContextNetlifyMapping[integrationContext]
|
||||
: null,
|
||||
siteId
|
||||
});
|
||||
|
||||
router.reload();
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<div className="opacity-50 hover:opacity-100 duration-200 ml-2">
|
||||
<Button
|
||||
onButtonPressed={async () => {
|
||||
await deleteIntegration({
|
||||
integrationId: integration._id
|
||||
});
|
||||
router.reload();
|
||||
}}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Integration;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import guidGenerator from '~/utilities/randomId';
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
|
||||
import Integration from './Integration';
|
||||
|
||||
@ -17,29 +17,21 @@ interface IntegrationType {
|
||||
context: string;
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({
|
||||
integrations,
|
||||
environments = [],
|
||||
}: Props) => {
|
||||
return integrations.length > 0 ? (
|
||||
<div className='mb-12'>
|
||||
<div className='flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2'>
|
||||
<h1 className='font-semibold text-3xl'>Current Integrations</h1>
|
||||
<p className='text-base text-gray-400'>
|
||||
const ProjectIntegrationSection = ({ integrations, environments = [] }: Props) =>
|
||||
integrations.length > 0 ? (
|
||||
<div className="mb-12">
|
||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 mt-6 text-xl max-w-5xl px-2">
|
||||
<h1 className="font-semibold text-3xl">Current Integrations</h1>
|
||||
<p className="text-base text-gray-400">
|
||||
Manage your integrations of Infisical with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
{integrations.map((integration: IntegrationType) => (
|
||||
<Integration
|
||||
key={guidGenerator()}
|
||||
integration={integration}
|
||||
environments={environments}
|
||||
/>
|
||||
<Integration key={guidGenerator()} integration={integration} environments={environments} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectIntegrationSection;
|
||||
|
@ -1,10 +1,11 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { TFunction, useTranslation } from "next-i18next";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { TFunction, useTranslation } from 'next-i18next';
|
||||
import { faGithub, faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||
import { faCircleQuestion } from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
faAngleDown,
|
||||
faBook,
|
||||
@ -12,40 +13,39 @@ import {
|
||||
faEnvelope,
|
||||
faGear,
|
||||
faPlus,
|
||||
faRightFromBracket,
|
||||
faUpRightFromSquare,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
faRightFromBracket
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
|
||||
import logout from "~/pages/api/auth/Logout";
|
||||
import logout from '@app/pages/api/auth/Logout';
|
||||
|
||||
import getOrganization from "../../pages/api/organization/GetOrg";
|
||||
import getOrganizations from "../../pages/api/organization/getOrgs";
|
||||
import getUser from "../../pages/api/user/getUser";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
import getOrganization from '../../pages/api/organization/GetOrg';
|
||||
import getOrganizations from '../../pages/api/organization/getOrgs';
|
||||
import getUser from '../../pages/api/user/getUser';
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
|
||||
t("nav:support.slack"),
|
||||
"https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA",
|
||||
t('nav:support.slack'),
|
||||
'https://join.slack.com/t/infisical/shared_invite/zt-1dgg63ln8-G7PCNJdCymAT9YF3j1ewVA'
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
||||
t("nav:support.docs"),
|
||||
"https://infisical.com/docs/getting-started/introduction",
|
||||
t('nav:support.docs'),
|
||||
'https://infisical.com/docs/getting-started/introduction'
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
|
||||
t("nav:support.issue"),
|
||||
"https://github.com/Infisical/infisical-cli/issues",
|
||||
t('nav:support.issue'),
|
||||
'https://github.com/Infisical/infisical-cli/issues'
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
||||
t("nav:support.email"),
|
||||
"mailto:support@infisical.com",
|
||||
],
|
||||
t('nav:support.email'),
|
||||
'mailto:support@infisical.com'
|
||||
]
|
||||
];
|
||||
|
||||
export interface ICurrentOrg {
|
||||
@ -79,17 +79,17 @@ export default function Navbar() {
|
||||
setUser(userData);
|
||||
const orgsData = await getOrganizations();
|
||||
setOrgs(orgsData);
|
||||
const currentOrg = await getOrganization({
|
||||
orgId: String(localStorage.getItem("orgData.id")),
|
||||
const currentUserOrg = await getOrganization({
|
||||
orgId: String(localStorage.getItem('orgData.id'))
|
||||
});
|
||||
setCurrentOrg(currentOrg);
|
||||
setCurrentOrg(currentUserOrg);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const closeApp = async () => {
|
||||
console.log("Logging out...");
|
||||
console.log('Logging out...');
|
||||
await logout();
|
||||
router.push("/login");
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
@ -97,28 +97,26 @@ export default function Navbar() {
|
||||
<div className="m-auto flex justify-start items-center mx-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
<Image
|
||||
src="/images/logotransparent.png"
|
||||
height={23}
|
||||
width={57}
|
||||
alt="logo"
|
||||
/>
|
||||
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
|
||||
</div>
|
||||
<a className="text-2xl text-white font-semibold mx-2">Infisical</a>
|
||||
<a href="#" className="text-2xl text-white font-semibold mx-2">
|
||||
Infisical
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex justify-start items-center mx-2 z-40">
|
||||
<a
|
||||
<a
|
||||
href="https://infisical.com/docs/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center">
|
||||
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
|
||||
Docs
|
||||
Docs
|
||||
</a>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div className="mr-4">
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<Menu.Button className="inline-flex w-full justify-center px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
@ -151,7 +149,7 @@ export default function Navbar() {
|
||||
</Menu>
|
||||
<Menu as="div" className="relative inline-block text-left mr-4">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center rounded-md pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<Menu.Button className="inline-flex w-full justify-center pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
{user?.firstName} {user?.lastName}
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleDown}
|
||||
@ -171,12 +169,13 @@ export default function Navbar() {
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 ">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav:user.signed-in-as")}
|
||||
{t('nav:user.signed-in-as')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push("/settings/personal/" + router.query.id)
|
||||
}
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
|
||||
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300">
|
||||
@ -185,13 +184,10 @@ export default function Navbar() {
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<p className="text-gray-300 px-2 pt-1 text-sm">
|
||||
{" "}
|
||||
{' '}
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="text-gray-400 px-2 pb-1 text-xs">
|
||||
{" "}
|
||||
{user?.email}
|
||||
</p>
|
||||
<p className="text-gray-400 px-2 pb-1 text-xs"> {user?.email}</p>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
@ -202,21 +198,20 @@ export default function Navbar() {
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav:user.current-organization")}
|
||||
{t('nav:user.current-organization')}
|
||||
</div>
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push("/settings/org/" + router.query.id)
|
||||
}
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<p className="text-gray-300 px-2 text-sm">
|
||||
{currentOrg?.name}
|
||||
</p>
|
||||
<p className="text-gray-300 px-2 text-sm">{currentOrg?.name}</p>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
|
||||
@ -225,56 +220,57 @@ export default function Navbar() {
|
||||
</div>
|
||||
<button
|
||||
// onClick={buttonAction}
|
||||
type="button"
|
||||
className="cursor-pointer w-full"
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push("/settings/billing/" + router.query.id)
|
||||
}
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
|
||||
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-lg pl-1.5 pr-3"
|
||||
icon={faCoins}
|
||||
/>
|
||||
<div className="text-sm">{t("nav:user.usage-billing")}</div>
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faCoins} />
|
||||
<div className="text-sm">{t('nav:user.usage-billing')}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
// onClick={buttonAction}
|
||||
className="cursor-pointer w-full mb-2"
|
||||
>
|
||||
<div
|
||||
onClick={() =>
|
||||
router.push(
|
||||
"/settings/org/" + router.query.id + "?invite"
|
||||
)
|
||||
}
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
|
||||
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
|
||||
>
|
||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
||||
</span>
|
||||
<div className="text-sm ml-1">{t("nav:user.invite")}</div>
|
||||
<div className="text-sm ml-1">{t('nav:user.invite')}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{orgs?.length > 1 && (
|
||||
<div className="px-1 pt-1">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav:user.other-organizations")}
|
||||
{t('nav:user.other-organizations')}
|
||||
</div>
|
||||
<div className="flex flex-col items-start px-1 mt-3 mb-2">
|
||||
{orgs
|
||||
.filter(
|
||||
(org: { _id: string }) =>
|
||||
org._id != localStorage.getItem("orgData.id")
|
||||
(org: { _id: string }) => org._id !== localStorage.getItem('orgData.id')
|
||||
)
|
||||
.map((org: { _id: string; name: string }) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={guidGenerator()}
|
||||
onClick={() => {
|
||||
localStorage.setItem("orgData.id", org._id);
|
||||
localStorage.setItem('orgData.id', org._id);
|
||||
router.reload();
|
||||
}}
|
||||
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
|
||||
@ -283,9 +279,7 @@ export default function Navbar() {
|
||||
{org.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<p className="text-gray-300 px-2 text-sm">
|
||||
{org.name}
|
||||
</p>
|
||||
<p className="text-gray-300 px-2 text-sm">{org.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -296,11 +290,10 @@ export default function Navbar() {
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeApp}
|
||||
className={`${
|
||||
active
|
||||
? "bg-red font-semibold text-white"
|
||||
: "text-gray-400"
|
||||
active ? 'bg-red font-semibold text-white' : 'text-gray-400'
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none">
|
||||
@ -308,7 +301,7 @@ export default function Navbar() {
|
||||
className="text-lg ml-1.5 mr-3"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
{t("common:logout")}
|
||||
{t('common:logout')}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
|
@ -3,8 +3,8 @@ import { useRouter } from 'next/router';
|
||||
import { faAngleRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import getOrganization from '~/pages/api/organization/GetOrg';
|
||||
import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
|
||||
import getOrganization from '@app/pages/api/organization/GetOrg';
|
||||
import getProjectInfo from '@app/pages/api/workspace/getProjectInfo';
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
@ -17,7 +17,7 @@ import getProjectInfo from '~/pages/api/workspace/getProjectInfo';
|
||||
*/
|
||||
export default function NavHeader({
|
||||
pageName,
|
||||
isProjectRelated,
|
||||
isProjectRelated
|
||||
}: {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
@ -30,12 +30,12 @@ export default function NavHeader({
|
||||
(async () => {
|
||||
const orgId = localStorage.getItem('orgData.id');
|
||||
const org = await getOrganization({
|
||||
orgId: orgId ? orgId : '',
|
||||
orgId: orgId || ''
|
||||
});
|
||||
setOrgName(org.name);
|
||||
|
||||
const workspace = await getProjectInfo({
|
||||
projectId: String(router.query.id),
|
||||
projectId: String(router.query.id)
|
||||
});
|
||||
setWorkspaceName(workspace.name);
|
||||
})();
|
||||
@ -43,27 +43,19 @@ export default function NavHeader({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='pt-20 ml-6 flex flex-row items-center'>
|
||||
<div className='bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2'>
|
||||
<div className="pt-20 ml-6 flex flex-row items-center">
|
||||
<div className="bg-primary-900 h-6 w-6 rounded-md flex items-center justify-center text-mineshaft-100 mr-2">
|
||||
{orgName?.charAt(0)}
|
||||
</div>
|
||||
<div className='text-primary text-sm font-semibold'>{orgName}</div>
|
||||
<div className="text-primary text-sm font-semibold">{orgName}</div>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleRight}
|
||||
className='ml-3 text-sm text-gray-400 mr-3'
|
||||
/>
|
||||
<div className='font-semibold text-primary text-sm'>
|
||||
{workspaceName}
|
||||
</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||
<div className="font-semibold text-primary text-sm">{workspaceName}</div>
|
||||
</>
|
||||
)}
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleRight}
|
||||
className='ml-3 text-sm text-gray-400 mr-3'
|
||||
/>
|
||||
<div className='text-gray-400 text-sm'>{pageName}</div>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 text-sm text-gray-400 mr-3" />
|
||||
<div className="text-gray-400 text-sm">{pageName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,49 +1,49 @@
|
||||
import React, { useState } from "react";
|
||||
import ReactCodeInput from "react-code-input";
|
||||
import { useTranslation } from "next-i18next";
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import React, { useState } from 'react';
|
||||
import ReactCodeInput from 'react-code-input';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
|
||||
|
||||
import Button from "../basic/buttons/Button";
|
||||
import Error from "../basic/Error";
|
||||
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Error from '../basic/Error';
|
||||
|
||||
// The style for the verification code input
|
||||
const props = {
|
||||
inputStyle: {
|
||||
fontFamily: "monospace",
|
||||
margin: "4px",
|
||||
MozAppearance: "textfield",
|
||||
width: "55px",
|
||||
borderRadius: "5px",
|
||||
fontSize: "24px",
|
||||
height: "55px",
|
||||
paddingLeft: "7",
|
||||
backgroundColor: "#0d1117",
|
||||
color: "white",
|
||||
border: "1px solid #2d2f33",
|
||||
textAlign: "center",
|
||||
outlineColor: "#8ca542",
|
||||
borderColor: "#2d2f33"
|
||||
},
|
||||
fontFamily: 'monospace',
|
||||
margin: '4px',
|
||||
MozAppearance: 'textfield',
|
||||
width: '55px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '24px',
|
||||
height: '55px',
|
||||
paddingLeft: '7',
|
||||
backgroundColor: '#0d1117',
|
||||
color: 'white',
|
||||
border: '1px solid #2d2f33',
|
||||
textAlign: 'center',
|
||||
outlineColor: '#8ca542',
|
||||
borderColor: '#2d2f33'
|
||||
}
|
||||
} as const;
|
||||
const propsPhone = {
|
||||
inputStyle: {
|
||||
fontFamily: "monospace",
|
||||
margin: "4px",
|
||||
MozAppearance: "textfield",
|
||||
width: "40px",
|
||||
borderRadius: "5px",
|
||||
fontSize: "24px",
|
||||
height: "40px",
|
||||
paddingLeft: "7",
|
||||
backgroundColor: "#0d1117",
|
||||
color: "white",
|
||||
border: "1px solid #2d2f33",
|
||||
textAlign: "center",
|
||||
outlineColor: "#8ca542",
|
||||
borderColor: "#2d2f33"
|
||||
},
|
||||
fontFamily: 'monospace',
|
||||
margin: '4px',
|
||||
MozAppearance: 'textfield',
|
||||
width: '40px',
|
||||
borderRadius: '5px',
|
||||
fontSize: '24px',
|
||||
height: '40px',
|
||||
paddingLeft: '7',
|
||||
backgroundColor: '#0d1117',
|
||||
color: 'white',
|
||||
border: '1px solid #2d2f33',
|
||||
textAlign: 'center',
|
||||
outlineColor: '#8ca542',
|
||||
borderColor: '#2d2f33'
|
||||
}
|
||||
} as const;
|
||||
|
||||
interface CodeInputStepProps {
|
||||
@ -55,17 +55,21 @@ interface CodeInputStepProps {
|
||||
|
||||
/**
|
||||
* This is the second step of sign up where users need to verify their email
|
||||
* @param {object} obj
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - user's email to which we just sent a verification email
|
||||
* @param {function} obj.incrementStep - goes to the next step of signup
|
||||
* @param {function} obj.setCode - state updating function that set the current value of the emai verification code
|
||||
* @param {boolean} obj.codeError - whether the code was inputted wrong or now
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export default function CodeInputStep({ email, incrementStep, setCode, codeError }: CodeInputStepProps): JSX.Element {
|
||||
export default function CodeInputStep({
|
||||
email,
|
||||
incrementStep,
|
||||
setCode,
|
||||
codeError
|
||||
}: CodeInputStepProps): JSX.Element {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResendingVerificationEmail, setIsResendingVerificationEmail] =
|
||||
useState(false);
|
||||
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resendVerificationEmail = async () => {
|
||||
@ -80,12 +84,8 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
|
||||
|
||||
return (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-10 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-16">
|
||||
<p className="text-l flex justify-center text-bunker-300">
|
||||
{t("signup:step2-message")}
|
||||
</p>
|
||||
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">
|
||||
{email}{" "}
|
||||
</p>
|
||||
<p className="text-l flex justify-center text-bunker-300">{t('signup:step2-message')}</p>
|
||||
<p className="text-l flex justify-center font-semibold my-2 text-bunker-300">{email} </p>
|
||||
<div className="hidden md:block">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
@ -108,28 +108,28 @@ export default function CodeInputStep({ email, incrementStep, setCode, codeError
|
||||
className="mt-2 mb-6"
|
||||
/>
|
||||
</div>
|
||||
{codeError && <Error text={t("signup:step2-code-error")} />}
|
||||
{codeError && <Error text={t('signup:step2-code-error')} />}
|
||||
<div className="flex max-w-max min-w-28 flex-col items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<Button
|
||||
text={t("signup:verify") ?? ""}
|
||||
onButtonPressed={incrementStep}
|
||||
size="lg"
|
||||
/>
|
||||
<Button text={t('signup:verify') ?? ''} onButtonPressed={incrementStep} size="lg" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full max-h-24 max-w-md mx-auto pt-2">
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">
|
||||
{t("signup:step2-resend-alert")}
|
||||
</span>
|
||||
<u className={`font-normal ${isResendingVerificationEmail ? 'text-bunker-400' : 'text-primary-700 hover:text-primary duration-200'}`}>
|
||||
<button disabled={isLoading} onClick={resendVerificationEmail}>
|
||||
{isResendingVerificationEmail ? t("signup:step2-resend-progress") : t("signup:step2-resend-submit")}
|
||||
<span className="text-bunker-400">{t('signup:step2-resend-alert')}</span>
|
||||
<u
|
||||
className={`font-normal ${
|
||||
isResendingVerificationEmail
|
||||
? 'text-bunker-400'
|
||||
: 'text-primary-700 hover:text-primary duration-200'
|
||||
}`}
|
||||
>
|
||||
<button disabled={isLoading} onClick={resendVerificationEmail} type="button">
|
||||
{isResendingVerificationEmail
|
||||
? t('signup:step2-resend-progress')
|
||||
: t('signup:step2-resend-submit')}
|
||||
</button>
|
||||
</u>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-400 pb-2">
|
||||
{t("signup:step2-spam-alert")}
|
||||
</p>
|
||||
<p className="text-sm text-bunker-400 pb-2">{t('signup:step2-spam-alert')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -25,37 +25,37 @@ export default function DonwloadBackupPDFStep({
|
||||
incrementStep,
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
name
|
||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg mx-auto h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl'>
|
||||
<p className='text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary'>
|
||||
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
{t('signup:step4-message')}
|
||||
</p>
|
||||
<div className='flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2'>
|
||||
<div className="flex flex-col items-center justify-center w-full mt-4 md:mt-8 max-w-md text-gray-400 text-md rounded-md px-2">
|
||||
<div>{t('signup:step4-description1')}</div>
|
||||
<div className='mt-3'>{t('signup:step4-description2')}</div>
|
||||
<div className="mt-3">{t('signup:step4-description2')}</div>
|
||||
</div>
|
||||
<div className='w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4'>
|
||||
<FontAwesomeIcon icon={faWarning} className='ml-2 mr-4 text-4xl' />
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-400 rounded-md max-w-xs md:max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
|
||||
{t('signup:step4-description3')}
|
||||
</div>
|
||||
<div className='flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg'>
|
||||
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
|
||||
<Button
|
||||
text='Download PDF'
|
||||
text="Download PDF"
|
||||
onButtonPressed={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: name,
|
||||
setBackupKeyError: (value: boolean) => {},
|
||||
setBackupKeyIssued: (value: boolean) => {},
|
||||
setBackupKeyError: () => {},
|
||||
setBackupKeyIssued: () => {}
|
||||
});
|
||||
incrementStep();
|
||||
}}
|
||||
size='lg'
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,30 +1,33 @@
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import sendVerificationEmail from "~/pages/api/auth/SendVerificationEmail";
|
||||
|
||||
import Button from "../basic/buttons/Button";
|
||||
import InputField from "../basic/InputField";
|
||||
import sendVerificationEmail from '@app/pages/api/auth/SendVerificationEmail';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import InputField from '../basic/InputField';
|
||||
|
||||
interface DownloadBackupPDFStepProps {
|
||||
incrementStep: () => void;
|
||||
email: string;
|
||||
email: string;
|
||||
setEmail: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the first step of the sign up process - users need to enter their email
|
||||
* @param {object} obj
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of a user signing up
|
||||
* @param {function} obj.setEmail - funciton that manages the state of the email variable
|
||||
* @param {function} obj.incrementStep - function to go to the next step of the signup flow
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
export default function EnterEmailStep({ email, setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element {
|
||||
export default function EnterEmailStep({
|
||||
email,
|
||||
setEmail,
|
||||
incrementStep
|
||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [emailErrorMessage, setEmailErrorMessage] = useState("");
|
||||
const [emailErrorMessage, setEmailErrorMessage] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
@ -34,15 +37,11 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
||||
let emailCheckBool = false;
|
||||
if (!email) {
|
||||
setEmailError(true);
|
||||
setEmailErrorMessage("Please enter your email.");
|
||||
setEmailErrorMessage('Please enter your email.');
|
||||
emailCheckBool = true;
|
||||
} else if (
|
||||
!email.includes("@") ||
|
||||
!email.includes(".") ||
|
||||
!/[a-z]/.test(email)
|
||||
) {
|
||||
} else if (!email.includes('@') || !email.includes('.') || !/[a-z]/.test(email)) {
|
||||
setEmailError(true);
|
||||
setEmailErrorMessage("Please enter a valid email.");
|
||||
setEmailErrorMessage('Please enter a valid email.');
|
||||
emailCheckBool = true;
|
||||
} else {
|
||||
setEmailError(false);
|
||||
@ -57,13 +56,13 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||
<div className="bg-bunker w-full max-w-md h-7/12 py-8 md:px-6 mx-1 rounded-xl drop-shadow-xl">
|
||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||
{t("signup:step1-start")}
|
||||
{t('signup:step1-start')}
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-5/6 md:w-full m-auto md:p-2 rounded-lg max-h-24 mt-4">
|
||||
<InputField
|
||||
label={t("common:email") ?? ""}
|
||||
label={t('common:email') ?? ''}
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
@ -75,11 +74,14 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-5/6 md:w-full md:p-2 max-h-28 max-w-xs md:max-w-md mx-auto mt-4 md:mt-4 text-sm text-center md:text-left">
|
||||
<p className="text-gray-400 mt-2 md:mx-0.5">
|
||||
{t("signup:step1-privacy")}
|
||||
</p>
|
||||
<p className="text-gray-400 mt-2 md:mx-0.5">{t('signup:step1-privacy')}</p>
|
||||
<div className="text-l mt-6 m-2 md:m-8 px-8 py-1 text-lg">
|
||||
<Button text={t("signup:step1-submit") ?? ""} type="submit" onButtonPressed={emailCheck} size="lg" />
|
||||
<Button
|
||||
text={t('signup:step1-submit') ?? ''}
|
||||
type="submit"
|
||||
onButtonPressed={emailCheck}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -87,7 +89,7 @@ export default function EnterEmailStep({ email, setEmail, incrementStep }: Downl
|
||||
<Link href="/login">
|
||||
<button type="button" className="w-max pb-3 hover:opacity-90 duration-200">
|
||||
<u className="font-normal text-sm text-primary-500">
|
||||
{t("signup:already-have-account")}
|
||||
{t('signup:already-have-account')}
|
||||
</u>
|
||||
</button>
|
||||
</Link>
|
||||
|
@ -1,18 +1,17 @@
|
||||
import React, { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import addUserToOrg from "~/pages/api/organization/addUserToOrg";
|
||||
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
||||
|
||||
import Button from "../basic/buttons/Button";
|
||||
import addUserToOrg from '@app/pages/api/organization/addUserToOrg';
|
||||
import getWorkspaces from '@app/pages/api/workspace/getWorkspaces';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
|
||||
/**
|
||||
* This is the last step of the signup flow. People can optionally invite their teammates here.
|
||||
*/
|
||||
export default function TeamInviteStep(): JSX.Element {
|
||||
const [emails, setEmails] = useState("");
|
||||
const [emails, setEmails] = useState('');
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
@ -20,34 +19,31 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
const redirectToHome = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push("/home/" + userWorkspace);
|
||||
router.push(`/home/${userWorkspace}`);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const inviteUsers = async ({ emails }: { emails: string; }) => {
|
||||
emails
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
inviteEmails
|
||||
.split(',')
|
||||
.map(email => email.trim())
|
||||
.map(async (email) => await addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
|
||||
.map((email) => email.trim())
|
||||
.map(async (email) => addUserToOrg(email, String(localStorage.getItem('orgData.id'))));
|
||||
|
||||
await redirectToHome();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 pt-6 pb-4 px-8 rounded-xl drop-shadow-xl mb-64 md:mb-32">
|
||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||
{t("signup:step5-invite-team")}
|
||||
{t('signup:step5-invite-team')}
|
||||
</p>
|
||||
<p className="text-center flex justify-center text-bunker-300 max-w-xs md:max-w-sm md:mx-8 mb-6 mt-4">
|
||||
{t("signup:step5-subtitle")}
|
||||
{t('signup:step5-subtitle')}
|
||||
</p>
|
||||
<div>
|
||||
<div className="overflow-auto bg-bunker-800">
|
||||
<div className="whitespace-pre-wrap break-words bg-transparent">
|
||||
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-words bg-transparent" />
|
||||
</div>
|
||||
<textarea
|
||||
<textarea
|
||||
className="bg-bunker-800 h-20 w-full placeholder:text-bunker-400 py-1 px-2 rounded-md border border-mineshaft-500 text-sm text-bunker-300 outline-none focus:ring-2 ring-primary-800 ring-opacity-70"
|
||||
value={emails}
|
||||
onChange={(e) => setEmails(e.target.value)}
|
||||
@ -55,15 +51,18 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<div
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="text-md md:text-sm mx-3 text-bunker-300 bg-mineshaft-700 py-3 md:py-3.5 px-5 rounded-md cursor-pointer hover:bg-mineshaft-500 duration-200"
|
||||
onClick={redirectToHome}
|
||||
>
|
||||
{t("signup:step5-skip")}
|
||||
{t('signup:step5-skip')}
|
||||
</div>
|
||||
<Button
|
||||
text={t("signup:step5-send-invites") ?? ""}
|
||||
onButtonPressed={() => inviteUsers({ emails})}
|
||||
text={t('signup:step5-send-invites') ?? ''}
|
||||
onButtonPressed={() => inviteUsers({ emails })}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
|
@ -3,8 +3,11 @@ import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import jsrp from 'jsrp';
|
||||
import nacl from 'tweetnacl';
|
||||
import { encodeBase64 } from 'tweetnacl-util';
|
||||
|
||||
import completeAccountInformationSignup from '~/pages/api/auth/CompleteAccountInformationSignup';
|
||||
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import InputField from '../basic/InputField';
|
||||
@ -12,9 +15,7 @@ import attemptLogin from '../utilities/attemptLogin';
|
||||
import passwordCheck from '../utilities/checks/PasswordCheck';
|
||||
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
|
||||
|
||||
const nacl = require('tweetnacl');
|
||||
const jsrp = require('jsrp');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
interface UserInfoStepProps {
|
||||
@ -51,7 +52,7 @@ export default function UserInfoStep({
|
||||
firstName,
|
||||
setFirstName,
|
||||
lastName,
|
||||
setLastName,
|
||||
setLastName
|
||||
}: UserInfoStepProps): JSX.Element {
|
||||
const [firstNameError, setFirstNameError] = useState(false);
|
||||
const [lastNameError, setLastNameError] = useState(false);
|
||||
@ -85,7 +86,7 @@ export default function UserInfoStep({
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck,
|
||||
errorCheck
|
||||
});
|
||||
|
||||
if (!errorCheck) {
|
||||
@ -93,17 +94,14 @@ export default function UserInfoStep({
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const PRIVATE_KEY = nacl.util.encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = nacl.util.encodeBase64(publicKeyUint8Array);
|
||||
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: PRIVATE_KEY,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
||||
'0'
|
||||
),
|
||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||
}) as { ciphertext: string; iv: string; tag: string };
|
||||
|
||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
||||
@ -111,50 +109,41 @@ export default function UserInfoStep({
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password,
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(
|
||||
async (err: any, result: { salt: string; verifier: string }) => {
|
||||
const response = await completeAccountInformationSignup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: firstName + "'s organization",
|
||||
publicKey: PUBLIC_KEY,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token: verificationToken,
|
||||
});
|
||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||
const response = await completeAccountInformationSignup({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: `${firstName}'s organization`,
|
||||
publicKey: PUBLIC_KEY,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token: verificationToken
|
||||
});
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
if (response.status === 200) {
|
||||
// response = await response.json();
|
||||
// if everything works, go the main dashboard page.
|
||||
if (response.status === 200) {
|
||||
// response = await response.json();
|
||||
|
||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
|
||||
try {
|
||||
await attemptLogin(
|
||||
email,
|
||||
password,
|
||||
(value: boolean) => {},
|
||||
router,
|
||||
true,
|
||||
false
|
||||
);
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
try {
|
||||
await attemptLogin(email, password, () => {}, router, true, false);
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
@ -163,142 +152,108 @@ export default function UserInfoStep({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16'>
|
||||
<p className='text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary'>
|
||||
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
|
||||
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
|
||||
{t('signup:step3-message')}
|
||||
</p>
|
||||
<div className='relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24'>
|
||||
<div className="relative z-0 flex items-center justify-end w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label={t('common:first-name')}
|
||||
onChangeHandler={setFirstName}
|
||||
type='name'
|
||||
type="name"
|
||||
value={firstName}
|
||||
isRequired
|
||||
errorText={
|
||||
t('common:validate-required', {
|
||||
name: t('common:first-name'),
|
||||
name: t('common:first-name')
|
||||
}) as string
|
||||
}
|
||||
error={firstNameError}
|
||||
autoComplete='given-name'
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24'>
|
||||
<div className="mt-2 flex items-center justify-center w-full md:p-2 rounded-lg max-h-24">
|
||||
<InputField
|
||||
label={t('common:last-name')}
|
||||
onChangeHandler={setLastName}
|
||||
type='name'
|
||||
type="name"
|
||||
value={lastName}
|
||||
isRequired
|
||||
errorText={
|
||||
t('common:validate-required', {
|
||||
name: t('common:last-name'),
|
||||
name: t('common:last-name')
|
||||
}) as string
|
||||
}
|
||||
error={lastNameError}
|
||||
autoComplete='family-name'
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60'>
|
||||
<div className="mt-2 flex flex-col items-center justify-center w-full md:p-2 rounded-lg max-h-60">
|
||||
<InputField
|
||||
label={t('section-password:password')}
|
||||
onChangeHandler={(password: string) => {
|
||||
setPassword(password);
|
||||
onChangeHandler={(pass: string) => {
|
||||
setPassword(pass);
|
||||
passwordCheck({
|
||||
password,
|
||||
password: pass,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck: false,
|
||||
errorCheck: false
|
||||
});
|
||||
}}
|
||||
type='password'
|
||||
type="password"
|
||||
value={password}
|
||||
isRequired
|
||||
error={
|
||||
passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase
|
||||
}
|
||||
autoComplete='new-password'
|
||||
id='new-password'
|
||||
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase}
|
||||
autoComplete="new-password"
|
||||
id="new-password"
|
||||
/>
|
||||
{passwordErrorLength ||
|
||||
passwordErrorLowerCase ||
|
||||
passwordErrorNumber ? (
|
||||
<div className='w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md'>
|
||||
<div className={`text-gray-400 text-sm mb-1`}>
|
||||
{t('section-password:validate-base')}
|
||||
</div>
|
||||
<div className='flex flex-row justify-start items-center ml-1'>
|
||||
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? (
|
||||
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md">
|
||||
<div className="text-gray-400 text-sm mb-1">{t('section-password:validate-base')}</div>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLength ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className='text-md text-red mr-2.5'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='text-md text-primary mr-2'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
passwordErrorLength ? 'text-gray-400' : 'text-gray-600'
|
||||
} text-sm`}
|
||||
>
|
||||
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
||||
{t('section-password:validate-length')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row justify-start items-center ml-1'>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorLowerCase ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className='text-md text-red mr-2.5'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='text-md text-primary mr-2'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
|
||||
} text-sm`}
|
||||
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
|
||||
>
|
||||
{t('section-password:validate-case')}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-row justify-start items-center ml-1'>
|
||||
<div className="flex flex-row justify-start items-center ml-1">
|
||||
{passwordErrorNumber ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className='text-md text-red mr-2.5'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='text-md text-primary mr-2'
|
||||
/>
|
||||
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
|
||||
} text-sm`}
|
||||
>
|
||||
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
|
||||
{t('section-password:validate-number')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className='py-2'></div>
|
||||
<div className="py-2" />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3'>
|
||||
<div className="flex flex-col items-center justify-center md:p-2 max-h-48 max-w-max mx-auto text-lg px-2 py-3">
|
||||
<Button
|
||||
text={t('signup:signup') ?? ''}
|
||||
loading={isLoading}
|
||||
onButtonPressed={signupErrorCheck}
|
||||
size='lg'
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,32 +1,27 @@
|
||||
import token from '~/pages/api/auth/Token';
|
||||
import token from '@app/pages/api/auth/Token';
|
||||
|
||||
export default class SecurityClient {
|
||||
static #token = '';
|
||||
|
||||
constructor() {}
|
||||
|
||||
static setToken(token: string) {
|
||||
this.#token = token;
|
||||
static setToken(tokenStr: string) {
|
||||
this.#token = tokenStr;
|
||||
}
|
||||
|
||||
static async fetchCall(
|
||||
resource: RequestInfo,
|
||||
options?: RequestInit | undefined
|
||||
) {
|
||||
static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
|
||||
const req = new Request(resource, options);
|
||||
|
||||
if (this.#token == '') {
|
||||
if (this.#token === '') {
|
||||
try {
|
||||
// TODO: This should be moved to a context to do it only once when app loads
|
||||
// this try catch saves route guard from a stuck state
|
||||
this.setToken(await token());
|
||||
} catch (error) {
|
||||
console.error("Unauthorized access");
|
||||
console.error('Unauthorized access');
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#token) {
|
||||
req.headers.set('Authorization', 'Bearer ' + this.#token);
|
||||
req.headers.set('Authorization', `Bearer ${this.#token}`);
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
|
@ -1,13 +1,17 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import crypto from 'crypto';
|
||||
|
||||
import jsrp from 'jsrp';
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
|
||||
import Aes256Gcm from '~/components/utilities/cryptography/aes-256-gcm';
|
||||
import login1 from '~/pages/api/auth/Login1';
|
||||
import login2 from '~/pages/api/auth/Login2';
|
||||
import addSecrets from '~/pages/api/files/AddSecrets';
|
||||
import getOrganizations from '~/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '~/pages/api/organization/GetOrgUserProjects';
|
||||
import getUser from '~/pages/api/user/getUser';
|
||||
import uploadKeys from '~/pages/api/workspace/uploadKeys';
|
||||
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||
import login1 from '@app/pages/api/auth/Login1';
|
||||
import login2 from '@app/pages/api/auth/Login2';
|
||||
import addSecrets from '@app/pages/api/files/AddSecrets';
|
||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||
import getUser from '@app/pages/api/user/getUser';
|
||||
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
|
||||
|
||||
import { encryptAssymmetric } from './cryptography/crypto';
|
||||
import encryptSecrets from './secrets/encryptSecrets';
|
||||
@ -15,11 +19,7 @@ import Telemetry from './telemetry/Telemetry';
|
||||
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
|
||||
import SecurityClient from './SecurityClient';
|
||||
|
||||
|
||||
const crypto = require("crypto");
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
const jsrp = require('jsrp');
|
||||
// eslint-disable-next-line new-cap
|
||||
const client = new jsrp.client();
|
||||
|
||||
/**
|
||||
@ -46,7 +46,7 @@ const attemptLogin = async (
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
@ -59,8 +59,10 @@ const attemptLogin = async (
|
||||
const clientProof = client.getProof(); // called M1
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
||||
await login2(email, clientProof);
|
||||
const { token, publicKey, encryptedPrivateKey, iv, tag } = await login2(
|
||||
email,
|
||||
clientProof
|
||||
);
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
@ -70,10 +72,7 @@ const attemptLogin = async (
|
||||
tag,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
||||
'0'
|
||||
)
|
||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
@ -83,9 +82,9 @@ const attemptLogin = async (
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
|
||||
const userOrgsData = userOrgs.map((org: { _id: string }) => org._id);
|
||||
|
||||
let orgToLogin;
|
||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||
@ -99,11 +98,9 @@ const attemptLogin = async (
|
||||
orgId: orgToLogin
|
||||
});
|
||||
|
||||
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
|
||||
orgUserProjects = orgUserProjects?.map((project: { _id: string }) => project._id);
|
||||
let projectToLogin;
|
||||
if (
|
||||
orgUserProjects.includes(localStorage.getItem('projectData.id'))
|
||||
) {
|
||||
if (orgUserProjects.includes(localStorage.getItem('projectData.id'))) {
|
||||
projectToLogin = localStorage.getItem('projectData.id');
|
||||
} else {
|
||||
try {
|
||||
@ -113,90 +110,108 @@ const attemptLogin = async (
|
||||
console.log('ERROR: User likely has no projects. ', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (email) {
|
||||
telemetry.identify(email);
|
||||
telemetry.capture('User Logged In');
|
||||
}
|
||||
|
||||
if (isSignUp) {
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const PRIVATE_KEY = String(localStorage.getItem('PRIVATE_KEY'));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
privateKey: PRIVATE_KEY
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
await uploadKeys(
|
||||
projectToLogin,
|
||||
myUser._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
await uploadKeys(projectToLogin, myUser._id, ciphertext, nonce);
|
||||
|
||||
const secretsToBeAdded: SecretDataProps[] = [{
|
||||
pos: 0,
|
||||
key: "DATABASE_URL",
|
||||
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
|
||||
valueOverride: undefined,
|
||||
comment: "This is an example of secret referencing.",
|
||||
id: ''
|
||||
}, {
|
||||
pos: 1,
|
||||
key: "DB_USERNAME",
|
||||
value: "OVERRIDE_THIS",
|
||||
valueOverride: undefined,
|
||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
||||
id: ''
|
||||
}, {
|
||||
pos: 2,
|
||||
key: "DB_PASSWORD",
|
||||
value: "OVERRIDE_THIS",
|
||||
valueOverride: undefined,
|
||||
comment: "This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need",
|
||||
id: ''
|
||||
}, {
|
||||
pos: 3,
|
||||
key: "DB_USERNAME",
|
||||
value: "user1234",
|
||||
valueOverride: "user1234",
|
||||
comment: "",
|
||||
id: ''
|
||||
}, {
|
||||
pos: 4,
|
||||
key: "DB_PASSWORD",
|
||||
value: "example_password",
|
||||
valueOverride: "example_password",
|
||||
comment: "",
|
||||
id: ''
|
||||
}, {
|
||||
pos: 5,
|
||||
key: "TWILIO_AUTH_TOKEN",
|
||||
value: "example_twillio_token",
|
||||
valueOverride: undefined,
|
||||
comment: "",
|
||||
id: ''
|
||||
}, {
|
||||
pos: 6,
|
||||
key: "WEBSITE_URL",
|
||||
value: "http://localhost:3000",
|
||||
valueOverride: undefined,
|
||||
comment: "",
|
||||
id: ''
|
||||
}]
|
||||
const secrets = await encryptSecrets({ secretsToEncrypt: secretsToBeAdded, workspaceId: String(localStorage.getItem('projectData.id')), env: 'dev' })
|
||||
await addSecrets({ secrets: secrets ?? [], env: "dev", workspaceId: String(localStorage.getItem('projectData.id')) });
|
||||
const secretsToBeAdded: SecretDataProps[] = [
|
||||
{
|
||||
pos: 0,
|
||||
key: 'DATABASE_URL',
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
|
||||
valueOverride: undefined,
|
||||
comment: 'This is an example of secret referencing.',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
pos: 1,
|
||||
key: 'DB_USERNAME',
|
||||
value: 'OVERRIDE_THIS',
|
||||
valueOverride: undefined,
|
||||
comment:
|
||||
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
pos: 2,
|
||||
key: 'DB_PASSWORD',
|
||||
value: 'OVERRIDE_THIS',
|
||||
valueOverride: undefined,
|
||||
comment:
|
||||
'This is an example of secret overriding. Your team can have a shared value of a secret, while you can override it to whatever value you need',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
pos: 3,
|
||||
key: 'DB_USERNAME',
|
||||
value: 'user1234',
|
||||
valueOverride: 'user1234',
|
||||
comment: '',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
pos: 4,
|
||||
key: 'DB_PASSWORD',
|
||||
value: 'example_password',
|
||||
valueOverride: 'example_password',
|
||||
comment: '',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
pos: 5,
|
||||
key: 'TWILIO_AUTH_TOKEN',
|
||||
value: 'example_twillio_token',
|
||||
valueOverride: undefined,
|
||||
comment: '',
|
||||
id: ''
|
||||
},
|
||||
{
|
||||
pos: 6,
|
||||
key: 'WEBSITE_URL',
|
||||
value: 'http://localhost:3000',
|
||||
valueOverride: undefined,
|
||||
comment: '',
|
||||
id: ''
|
||||
}
|
||||
];
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeAdded,
|
||||
workspaceId: String(localStorage.getItem('projectData.id')),
|
||||
env: 'dev'
|
||||
});
|
||||
await addSecrets({
|
||||
secrets: secrets ?? [],
|
||||
env: 'dev',
|
||||
workspaceId: String(localStorage.getItem('projectData.id'))
|
||||
});
|
||||
}
|
||||
|
||||
if (isLogin) {
|
||||
router.push('/dashboard/' + localStorage.getItem('projectData.id'));
|
||||
if (localStorage.getItem('projectData.id') !== "undefined") {
|
||||
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
|
||||
} else {
|
||||
router.push("/noprojects");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log(error);
|
||||
setErrorLogin(true);
|
||||
console.log('Login response not available');
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers';
|
||||
import checkUserAction from '~/pages/api/userActions/checkUserAction';
|
||||
import getOrganizationUsers from '@app/pages/api/organization/GetOrgUsers';
|
||||
import checkUserAction from '@app/pages/api/userActions/checkUserAction';
|
||||
|
||||
interface OnboardingCheckProps {
|
||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||
@ -26,46 +26,43 @@ const onboardingCheck = async ({
|
||||
action: 'slack_cta_clicked'
|
||||
});
|
||||
if (userActionSlack) {
|
||||
countActions = countActions + 1;
|
||||
countActions += 1;
|
||||
}
|
||||
setHasUserClickedSlack &&
|
||||
setHasUserClickedSlack(userActionSlack ? true : false);
|
||||
if (setHasUserClickedSlack) setHasUserClickedSlack(!!userActionSlack);
|
||||
|
||||
const userActionSecrets = await checkUserAction({
|
||||
action: 'first_time_secrets_pushed'
|
||||
});
|
||||
if (userActionSecrets) {
|
||||
countActions = countActions + 1;
|
||||
countActions += 1;
|
||||
}
|
||||
setHasUserPushedSecrets &&
|
||||
setHasUserPushedSecrets(userActionSecrets ? true : false);
|
||||
if (setHasUserPushedSecrets) setHasUserPushedSecrets(!!userActionSecrets);
|
||||
|
||||
const userActionIntro = await checkUserAction({
|
||||
action: 'intro_cta_clicked'
|
||||
});
|
||||
if (userActionIntro) {
|
||||
countActions = countActions + 1;
|
||||
countActions += 1;
|
||||
}
|
||||
setHasUserClickedIntro &&
|
||||
setHasUserClickedIntro(userActionIntro ? true : false);
|
||||
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||
|
||||
const userActionStar = await checkUserAction({
|
||||
action: 'star_cta_clicked'
|
||||
});
|
||||
if (userActionStar) {
|
||||
countActions = countActions + 1;
|
||||
countActions += 1;
|
||||
}
|
||||
setHasUserStarred && setHasUserStarred(userActionStar ? true : false);
|
||||
if (setHasUserStarred) setHasUserStarred(!!userActionStar);
|
||||
|
||||
const orgId = localStorage.getItem('orgData.id');
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: orgId ? orgId : ''
|
||||
orgId: orgId || ''
|
||||
});
|
||||
if (orgUsers.length > 1) {
|
||||
countActions = countActions + 1;
|
||||
countActions += 1;
|
||||
}
|
||||
setUsersInOrg && setUsersInOrg(orgUsers.length > 1);
|
||||
setTotalOnboardingActionsDone && setTotalOnboardingActionsDone(countActions);
|
||||
if (setUsersInOrg) setUsersInOrg(orgUsers.length > 1);
|
||||
if (setTotalOnboardingActionsDone) setTotalOnboardingActionsDone(countActions);
|
||||
};
|
||||
|
||||
export default onboardingCheck;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
interface PasswordCheckProps {
|
||||
password: string;
|
||||
errorCheck: boolean;
|
||||
@ -14,7 +15,7 @@ const passwordCheck = ({
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck,
|
||||
errorCheck
|
||||
}: PasswordCheckProps) => {
|
||||
if (!password || password.length < 14) {
|
||||
setPasswordErrorLength(true);
|
||||
|
@ -32,7 +32,6 @@ class Aes256Gcm {
|
||||
/**
|
||||
* No need to run the constructor. The class only has static methods.
|
||||
*/
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Encrypts text with AES 256 GCM.
|
||||
@ -65,11 +64,7 @@ class Aes256Gcm {
|
||||
* @returns {string}
|
||||
*/
|
||||
static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, 'base64')
|
||||
);
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, secret, Buffer.from(iv, 'base64'));
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
|
@ -1,7 +1,8 @@
|
||||
/* eslint-disable new-cap */
|
||||
import jsrp from 'jsrp';
|
||||
|
||||
import changePassword2 from '~/pages/api/auth/ChangePassword2';
|
||||
import SRP1 from '~/pages/api/auth/SRP1';
|
||||
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
|
||||
import SRP1 from '@app/pages/api/auth/SRP1';
|
||||
|
||||
import Aes256Gcm from './aes-256-gcm';
|
||||
|
||||
@ -33,15 +34,16 @@ const changePassword = async (
|
||||
clientOldPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: currentPassword,
|
||||
password: currentPassword
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||
|
||||
let serverPublicKey, salt;
|
||||
let serverPublicKey;
|
||||
let salt;
|
||||
try {
|
||||
const res = await SRP1({
|
||||
clientPublicKey: clientPublicKey,
|
||||
clientPublicKey
|
||||
});
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
@ -57,7 +59,7 @@ const changePassword = async (
|
||||
clientNewPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: newPassword,
|
||||
password: newPassword
|
||||
},
|
||||
async () => {
|
||||
clientNewPassword.createVerifier(async (err, result) => {
|
||||
@ -67,11 +69,9 @@ const changePassword = async (
|
||||
secret: newPassword
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 +
|
||||
(newPassword.slice(0, 32).length -
|
||||
new Blob([newPassword]).size),
|
||||
32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size),
|
||||
'0'
|
||||
),
|
||||
)
|
||||
});
|
||||
|
||||
if (ciphertext) {
|
||||
@ -87,18 +87,18 @@ const changePassword = async (
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
clientProof,
|
||||
clientProof
|
||||
});
|
||||
if (res && res.status == 400) {
|
||||
if (res && res.status === 400) {
|
||||
setCurrentPasswordError(true);
|
||||
} else if (res && res.status == 200) {
|
||||
} else if (res && res.status === 200) {
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (error) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log(err);
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1,8 +1,9 @@
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
import aes from './aes-256-gcm';
|
||||
|
||||
type encryptAsymmetricProps = {
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
type EncryptAsymmetricProps = {
|
||||
plaintext: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
@ -22,8 +23,8 @@ type encryptAsymmetricProps = {
|
||||
const encryptAssymmetric = ({
|
||||
plaintext,
|
||||
publicKey,
|
||||
privateKey,
|
||||
}: encryptAsymmetricProps): {
|
||||
privateKey
|
||||
}: EncryptAsymmetricProps): {
|
||||
ciphertext: string;
|
||||
nonce: string;
|
||||
} => {
|
||||
@ -37,11 +38,11 @@ const encryptAssymmetric = ({
|
||||
|
||||
return {
|
||||
ciphertext: nacl.util.encodeBase64(ciphertext),
|
||||
nonce: nacl.util.encodeBase64(nonce),
|
||||
nonce: nacl.util.encodeBase64(nonce)
|
||||
};
|
||||
};
|
||||
|
||||
type decryptAsymmetricProps = {
|
||||
type DecryptAsymmetricProps = {
|
||||
ciphertext: string;
|
||||
nonce: string;
|
||||
publicKey: string;
|
||||
@ -61,8 +62,8 @@ const decryptAssymmetric = ({
|
||||
ciphertext,
|
||||
nonce,
|
||||
publicKey,
|
||||
privateKey,
|
||||
}: decryptAsymmetricProps): string => {
|
||||
privateKey
|
||||
}: DecryptAsymmetricProps): string => {
|
||||
const plaintext = nacl.box.open(
|
||||
nacl.util.decodeBase64(ciphertext),
|
||||
nacl.util.decodeBase64(nonce),
|
||||
@ -73,24 +74,23 @@ const decryptAssymmetric = ({
|
||||
return nacl.util.encodeUTF8(plaintext);
|
||||
};
|
||||
|
||||
type encryptSymmetricProps = {
|
||||
type EncryptSymmetricProps = {
|
||||
plaintext: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
type encryptSymmetricReturn = {
|
||||
ciphertext:string;
|
||||
iv:string;
|
||||
tag:string;
|
||||
type EncryptSymmetricReturn = {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
};
|
||||
/**
|
||||
* Return symmetrically encrypted [plaintext] using [key].
|
||||
*/
|
||||
const encryptSymmetric = ({
|
||||
plaintext,
|
||||
key,
|
||||
}: encryptSymmetricProps): encryptSymmetricReturn => {
|
||||
let ciphertext, iv, tag;
|
||||
const encryptSymmetric = ({ plaintext, key }: EncryptSymmetricProps): EncryptSymmetricReturn => {
|
||||
let ciphertext;
|
||||
let iv;
|
||||
let tag;
|
||||
try {
|
||||
const obj = aes.encrypt({ text: plaintext, secret: key });
|
||||
ciphertext = obj.ciphertext;
|
||||
@ -105,11 +105,11 @@ const encryptSymmetric = ({
|
||||
return {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
tag
|
||||
};
|
||||
};
|
||||
|
||||
type decryptSymmetricProps = {
|
||||
type DecryptSymmetricProps = {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
@ -126,12 +126,7 @@ type decryptSymmetricProps = {
|
||||
* @param {String} obj.key - 32-byte hex key
|
||||
*
|
||||
*/
|
||||
const decryptSymmetric = ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key,
|
||||
}: decryptSymmetricProps): string => {
|
||||
const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
|
||||
let plaintext;
|
||||
try {
|
||||
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
||||
@ -143,9 +138,4 @@ const decryptSymmetric = ({
|
||||
return plaintext;
|
||||
};
|
||||
|
||||
export {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
};
|
||||
export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
|
||||
|
@ -1,15 +1,16 @@
|
||||
import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey';
|
||||
import SRP1 from '~/pages/api/auth/SRP1';
|
||||
/* eslint-disable new-cap */
|
||||
import crypto from 'crypto';
|
||||
|
||||
import jsrp from 'jsrp';
|
||||
|
||||
import issueBackupPrivateKey from '@app/pages/api/auth/IssueBackupPrivateKey';
|
||||
import SRP1 from '@app/pages/api/auth/SRP1';
|
||||
|
||||
import generateBackupPDF from '../generateBackupPDF';
|
||||
import Aes256Gcm from './aes-256-gcm';
|
||||
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
const jsrp = require('jsrp');
|
||||
const clientPassword = new jsrp.client();
|
||||
const clientKey = new jsrp.client();
|
||||
const crypto = require('crypto');
|
||||
|
||||
interface BackupKeyProps {
|
||||
email: string;
|
||||
@ -42,15 +43,16 @@ const issueBackupKey = async ({
|
||||
clientPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: password
|
||||
password
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = clientPassword.getPublicKey();
|
||||
|
||||
let serverPublicKey, salt;
|
||||
let serverPublicKey;
|
||||
let salt;
|
||||
try {
|
||||
const res = await SRP1({
|
||||
clientPublicKey: clientPublicKey
|
||||
clientPublicKey
|
||||
});
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
@ -87,9 +89,9 @@ const issueBackupKey = async ({
|
||||
clientProof
|
||||
});
|
||||
|
||||
if (res?.status == 400) {
|
||||
if (res?.status === 400) {
|
||||
setBackupKeyError(true);
|
||||
} else if (res?.status == 200) {
|
||||
} else if (res?.status === 200) {
|
||||
generateBackupPDF({
|
||||
personalName,
|
||||
personalEmail: email,
|
||||
|
File diff suppressed because one or more lines are too long
@ -29,6 +29,7 @@ export function parseDotEnv(src: ArrayBuffer) {
|
||||
let match;
|
||||
let item: [string, string, string[]] | [] = [];
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = LINE.exec(line)) !== null) {
|
||||
const key = match[1];
|
||||
|
||||
@ -58,8 +59,8 @@ export function parseDotEnv(src: ArrayBuffer) {
|
||||
})
|
||||
.filter((line) => line.length > 1)
|
||||
.forEach((line) => {
|
||||
const [key, value, comments] = line;
|
||||
object[key as string] = { value, comments };
|
||||
const [key, value, cmnts] = line;
|
||||
object[key as string] = { value, comments: cmnts };
|
||||
});
|
||||
|
||||
return object;
|
||||
|
@ -3,23 +3,11 @@
|
||||
* @returns
|
||||
*/
|
||||
const guidGenerator = () => {
|
||||
const S4 = function () {
|
||||
const S4 = () => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
};
|
||||
return (
|
||||
S4() +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
S4() +
|
||||
S4()
|
||||
);
|
||||
return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
|
||||
};
|
||||
|
||||
export default guidGenerator;
|
||||
|
@ -22,7 +22,7 @@ export const saveTokenToLocalStorage = ({
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new Error(
|
||||
"Unable to send the tokens in local storage:" + err.message
|
||||
`Unable to send the tokens in local storage:${ err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,32 @@
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .env file
|
||||
* @param {object} obj
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const checkOverrides = async ({ data }: { data: SecretDataProps[]; }) => {
|
||||
let secrets : SecretDataProps[] = data!.map((secret) => Object.create(secret));
|
||||
const overridenSecrets = data!.filter(
|
||||
(secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal'
|
||||
const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => {
|
||||
let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret));
|
||||
const overridenSecrets = data!.filter((secret) =>
|
||||
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||
? 'shared'
|
||||
: 'personal'
|
||||
);
|
||||
if (overridenSecrets.length) {
|
||||
overridenSecrets.forEach((secret) => {
|
||||
const index = secrets!.findIndex(
|
||||
(_secret) => _secret.key === secret.key && (secret.valueOverride == undefined || secret?.value != secret?.valueOverride)
|
||||
(_secret) =>
|
||||
_secret.key === secret.key &&
|
||||
(secret.valueOverride === undefined || secret?.value !== secret?.valueOverride)
|
||||
);
|
||||
secrets![index].value = secret.value;
|
||||
});
|
||||
secrets = secrets!.filter((secret) => (secret.valueOverride == undefined || secret?.value != secret?.valueOverride));
|
||||
secrets = secrets!.filter(
|
||||
(secret) => secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||
);
|
||||
}
|
||||
return secrets;
|
||||
}
|
||||
};
|
||||
|
||||
export default checkOverrides;
|
||||
|
@ -1,16 +1,14 @@
|
||||
import { envMapping } from "public/data/frequentConstants";
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
|
||||
import checkOverrides from './checkOverrides';
|
||||
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .env file
|
||||
* @param {object} obj
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||
* @param {string} obj.env - the environment which we're downloading (used for naming the file)
|
||||
*/
|
||||
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
||||
const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||
if (!data) return;
|
||||
const secrets = await checkOverrides({ data });
|
||||
|
||||
@ -19,21 +17,21 @@ const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: str
|
||||
(item: SecretDataProps) =>
|
||||
`${
|
||||
item.comment
|
||||
? item.comment
|
||||
? `${item.comment
|
||||
.split('\n')
|
||||
.map((comment) => '# '.concat(comment))
|
||||
.join('\n') + '\n'
|
||||
.join('\n')}\n`
|
||||
: ''
|
||||
}` + [item.key, item.value].join('=')
|
||||
}${[item.key, item.value].join('=')}`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = env + '.env';
|
||||
alink.click();
|
||||
}
|
||||
const blob = new Blob([file]);
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
const alink = document.createElement('a');
|
||||
alink.href = fileDownloadUrl;
|
||||
alink.download = `${env}.env`;
|
||||
alink.click();
|
||||
};
|
||||
|
||||
export default downloadDotEnv;
|
||||
|
@ -1,20 +1,20 @@
|
||||
// import YAML from 'yaml';
|
||||
// import { YAMLSeq } from 'yaml/types';
|
||||
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
|
||||
// import { envMapping } from "../../../public/data/frequentConstants";
|
||||
// import checkOverrides from './checkOverrides';
|
||||
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .yml file
|
||||
* @param {object} obj
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to download
|
||||
* @param {string} obj.env - used for naming the file
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
|
||||
// if (!data) return;
|
||||
// const doc = new YAML.Document();
|
||||
// doc.contents = new YAMLSeq();
|
||||
@ -27,20 +27,17 @@ const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: strin
|
||||
// .join('\n');
|
||||
// doc.add(pair);
|
||||
// });
|
||||
|
||||
// const file = doc
|
||||
// .toString()
|
||||
// .split('\n')
|
||||
// .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
|
||||
// .join('\n');
|
||||
|
||||
// const blob = new Blob([file]);
|
||||
// const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
// const alink = document.createElement('a');
|
||||
// alink.href = fileDownloadUrl;
|
||||
// alink.download = envMapping[env] + '.yml';
|
||||
// alink.click();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export default downloadYaml;
|
||||
|
@ -1,15 +1,10 @@
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
import crypto from 'crypto';
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
|
||||
const crypto = require("crypto");
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
} = require("../cryptography/crypto");
|
||||
const nacl = require("tweetnacl");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
import { decryptAssymmetric, encryptSymmetric } from '../cryptography/crypto';
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
id: string;
|
||||
@ -24,22 +19,30 @@ interface EncryptedSecretProps {
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
type: 'personal' | 'shared';
|
||||
}
|
||||
|
||||
/**
|
||||
* Encypt secrets before pushing the to the DB
|
||||
* @param {object} obj
|
||||
* @param {object} obj
|
||||
* @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
|
||||
* @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsToEncrypt: SecretDataProps[]; workspaceId: string; env: string; }) => {
|
||||
const encryptSecrets = async ({
|
||||
secretsToEncrypt,
|
||||
workspaceId,
|
||||
env
|
||||
}: {
|
||||
secretsToEncrypt: SecretDataProps[];
|
||||
workspaceId: string;
|
||||
env: string;
|
||||
}) => {
|
||||
let secrets;
|
||||
try {
|
||||
const sharedKey = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
let randomBytes: string;
|
||||
if (Object.keys(sharedKey).length > 0) {
|
||||
@ -48,42 +51,42 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
|
||||
ciphertext: sharedKey.latestKey.encryptedKey,
|
||||
nonce: sharedKey.latestKey.nonce,
|
||||
publicKey: sharedKey.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
} else {
|
||||
// case: a (shared) key does not exist for the workspace
|
||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
|
||||
secrets = secretsToEncrypt.map((secret) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag,
|
||||
tag: secretKeyTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.key,
|
||||
key: randomBytes,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag,
|
||||
tag: secretValueTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.value,
|
||||
key: randomBytes,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
// encrypt comment
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag,
|
||||
tag: secretCommentTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.comment ?? '',
|
||||
key: randomBytes,
|
||||
key: randomBytes
|
||||
});
|
||||
|
||||
const result: EncryptedSecretProps = {
|
||||
@ -99,17 +102,19 @@ const encryptSecrets = async ({ secretsToEncrypt, workspaceId, env }: { secretsT
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal',
|
||||
type:
|
||||
secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
|
||||
? 'shared'
|
||||
: 'personal'
|
||||
};
|
||||
|
||||
return result;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error while encrypting secrets");
|
||||
console.log('Error while encrypting secrets');
|
||||
}
|
||||
|
||||
return secrets;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export default encryptSecrets;
|
||||
|
@ -1,12 +1,7 @@
|
||||
import getSecrets from '~/pages/api/files/GetSecrets';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
import getSecrets from '@app/pages/api/files/GetSecrets';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
import { decryptAssymmetric, decryptSymmetric } from '../cryptography/crypto';
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
_id: string;
|
||||
@ -21,14 +16,14 @@ interface EncryptedSecretProps {
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
type: 'personal' | 'shared';
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
type: 'personal' | 'shared';
|
||||
comment: string;
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
type: 'personal' | 'shared';
|
||||
comment: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
@ -61,11 +56,11 @@ const getSecretsForProject = async ({
|
||||
console.log('ERROR: Not able to access the latest version of secrets');
|
||||
}
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId })
|
||||
const latestKey = await getLatestFileKey({ workspaceId });
|
||||
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
|
||||
setIsKeyAvailable(!latestKey ? encryptedSecrets.length == 0 : true);
|
||||
setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
const tempDecryptedSecrets: SecretProps[] = [];
|
||||
if (latestKey) {
|
||||
@ -78,7 +73,7 @@ const getSecretsForProject = async ({
|
||||
});
|
||||
|
||||
// decrypt secret keys, values, and comments
|
||||
encryptedSecrets.map((secret: EncryptedSecretProps) => {
|
||||
encryptedSecrets.forEach((secret: EncryptedSecretProps) => {
|
||||
const plainTextKey = decryptSymmetric({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
@ -102,7 +97,7 @@ const getSecretsForProject = async ({
|
||||
key
|
||||
});
|
||||
} else {
|
||||
plainTextComment = "";
|
||||
plainTextComment = '';
|
||||
}
|
||||
|
||||
tempDecryptedSecrets.push({
|
||||
@ -115,20 +110,26 @@ const getSecretsForProject = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const secretKeys = [...new Set(tempDecryptedSecrets.map(secret => secret.key))];
|
||||
const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))];
|
||||
|
||||
|
||||
const result = secretKeys.map((key, index) => {
|
||||
return {
|
||||
id: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.id,
|
||||
idOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.id,
|
||||
pos: index,
|
||||
key: key,
|
||||
value: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.value,
|
||||
valueOverride: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'personal')[0]?.value,
|
||||
comment: tempDecryptedSecrets.filter(secret => secret.key == key && secret.type == 'shared')[0]?.comment,
|
||||
}
|
||||
});
|
||||
const result = secretKeys.map((key, index) => ({
|
||||
id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
|
||||
?.id,
|
||||
idOverride: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.id,
|
||||
pos: index,
|
||||
key,
|
||||
value: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.value,
|
||||
valueOverride: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.value,
|
||||
comment: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.comment
|
||||
}));
|
||||
|
||||
setData(result);
|
||||
return result;
|
||||
|
@ -1,7 +1,7 @@
|
||||
/* eslint-disable */
|
||||
import { PostHog } from 'posthog-js';
|
||||
import { initPostHog } from '~/components/analytics/posthog';
|
||||
import { ENV } from '~/components/utilities/config';
|
||||
import { initPostHog } from '@app/components/analytics/posthog';
|
||||
import { ENV } from '@app/components/utilities/config';
|
||||
|
||||
declare let TELEMETRY_CAPTURING_ENABLED: any;
|
||||
|
||||
@ -13,7 +13,7 @@ class Capturer {
|
||||
}
|
||||
|
||||
capture(item: string) {
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
try {
|
||||
this.api.capture(item);
|
||||
} catch (error) {
|
||||
@ -23,7 +23,7 @@ class Capturer {
|
||||
}
|
||||
|
||||
identify(id: string) {
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
try {
|
||||
this.api.identify(id);
|
||||
} catch (error) {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
actionId: string;
|
||||
interface WorkspaceProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -11,21 +10,18 @@ interface workspaceProps {
|
||||
* @param {string} obj.actionId - id of an action for which we are trying to get data
|
||||
* @returns
|
||||
*/
|
||||
const getActionData = async ({ actionId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/action/' + actionId, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const getActionData = async ({ actionId }: WorkspaceProps) =>
|
||||
SecurityClient.fetchCall(`/api/v1/action/${actionId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).action;
|
||||
} else {
|
||||
console.log('Failed to get the info about an action');
|
||||
}
|
||||
console.log('Failed to get the info about an action');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default getActionData;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
interface WorkspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
userId: string;
|
||||
@ -12,48 +11,53 @@ interface workspaceProps {
|
||||
/**
|
||||
* This function fetches the activity logs for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
|
||||
* @param {string} obj.workspaceId - workspace id for which we are trying to get project log
|
||||
* @param {object} obj.offset - teh starting point of logs that we want to pull
|
||||
* @param {object} obj.limit - how many logs will we output
|
||||
* @param {object} obj.userId - optional userId filter - will only query logs for that user
|
||||
* @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions
|
||||
* @returns
|
||||
*/
|
||||
const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => {
|
||||
const getProjectLogs = async ({
|
||||
workspaceId,
|
||||
offset,
|
||||
limit,
|
||||
userId,
|
||||
actionNames
|
||||
}: WorkspaceProps) => {
|
||||
let payload;
|
||||
if (userId != "" && actionNames != '') {
|
||||
if (userId !== '' && actionNames !== '') {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId),
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else if (userId != "") {
|
||||
actionNames
|
||||
};
|
||||
} else if (userId !== '') {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId)
|
||||
}
|
||||
} else if (actionNames != "") {
|
||||
};
|
||||
} else if (actionNames !== '') {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
actionNames: actionNames
|
||||
}
|
||||
actionNames
|
||||
};
|
||||
} else {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent'
|
||||
}
|
||||
sortBy: 'recent'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/logs?' +
|
||||
new URLSearchParams(payload),
|
||||
`/api/v1/workspace/${workspaceId}/logs?${new URLSearchParams(payload)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -61,11 +65,11 @@ const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).logs;
|
||||
} else {
|
||||
console.log('Failed to get project logs');
|
||||
}
|
||||
console.log('Failed to get project logs');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
interface WorkspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
@ -10,30 +9,29 @@ interface workspaceProps {
|
||||
/**
|
||||
* This function fetches the secret snapshots for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @param {object} obj.offset - teh starting point of snapshots that we want to pull
|
||||
* @param {object} obj.limit - how many snapshots will we output
|
||||
* @returns
|
||||
*/
|
||||
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots?' +
|
||||
new URLSearchParams({
|
||||
const getProjectSecretShanpshots = async ({ workspaceId, offset, limit }: WorkspaceProps) =>
|
||||
SecurityClient.fetchCall(
|
||||
`/api/v1/workspace/${workspaceId}/secret-snapshots?${new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}), {
|
||||
})}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).secretSnapshots;
|
||||
} else {
|
||||
console.log('Failed to get project secret snapshots');
|
||||
}
|
||||
console.log('Failed to get project secret snapshots');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSecretShanpshots;
|
||||
|
@ -1,31 +1,27 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
interface WorkspaceProps {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the count of secret snapshots for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @param {string} obj.workspaceId - project id for which we are trying to get project secret snapshots
|
||||
* @returns
|
||||
*/
|
||||
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const getProjectSercetSnapshotsCount = async ({ workspaceId }: WorkspaceProps) =>
|
||||
SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/count`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).count;
|
||||
} else {
|
||||
console.log('Failed to get the count of project secret snapshots');
|
||||
}
|
||||
console.log('Failed to get the count of project secret snapshots');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSercetSnapshotsCount;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface SnapshotProps {
|
||||
secretSnapshotId: string;
|
||||
secretSnapshotId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -11,21 +10,18 @@ interface SnapshotProps {
|
||||
* @param {string} obj.secretSnapshotId - snapshot id for which we are trying to get secrets
|
||||
* @returns
|
||||
*/
|
||||
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret-snapshot/' + secretSnapshotId, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const getSecretSnapshotData = async ({ secretSnapshotId }: SnapshotProps) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-snapshot/${secretSnapshotId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).secretSnapshot;
|
||||
} else {
|
||||
console.log('Failed to get the secrets of a certain snapshot');
|
||||
}
|
||||
console.log('Failed to get the secrets of a certain snapshot');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretSnapshotData;
|
||||
|
@ -1,9 +1,8 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface secretVersionProps {
|
||||
secretId: string;
|
||||
offset: number;
|
||||
interface SecretVersionProps {
|
||||
secretId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
@ -15,13 +14,12 @@ interface secretVersionProps {
|
||||
* @param {number} obj.limit - how far our query goes
|
||||
* @returns
|
||||
*/
|
||||
const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/secret/' + secretId + '/secret-versions?' +
|
||||
new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}),
|
||||
const getSecretVersions = async ({ secretId, offset, limit }: SecretVersionProps) =>
|
||||
SecurityClient.fetchCall(
|
||||
`/api/v1/secret/${secretId}/secret-versions?${new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
})}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@ -29,12 +27,11 @@ const getSecretVersions = async ({ secretId, offset, limit }: secretVersionProps
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to get secret version history');
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log('Failed to get secret version history');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretVersions;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function performs a rollback of secrets in a certain project
|
||||
@ -7,24 +7,27 @@ import SecurityClient from '~/utilities/SecurityClient';
|
||||
* @param {number} obj.version - version to which we are rolling back
|
||||
* @returns
|
||||
*/
|
||||
const performSecretRollback = async ({ workspaceId, version }: { workspaceId: string; version: number; }) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + "/secret-snapshots/rollback", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version
|
||||
})
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to perform the secret rollback');
|
||||
const performSecretRollback = async ({
|
||||
workspaceId,
|
||||
version
|
||||
}: {
|
||||
workspaceId: string;
|
||||
version: number;
|
||||
}) =>
|
||||
SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log('Failed to perform the secret rollback');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default performSecretRollback;
|
||||
|
@ -1,27 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getActionData from "@app/ee/api/secrets/GetActionData";
|
||||
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
import getActionData from '@app/ee/api/secrets/GetActionData';
|
||||
import patienceDiff from '@app/ee/utilities/findTextDifferences';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
||||
|
||||
|
||||
const {
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../../components/utilities/cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
} from '../../components/utilities/cryptography/crypto';
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
toggleSidebar: (value: string) => void;
|
||||
currentAction: string;
|
||||
}
|
||||
|
||||
@ -41,11 +36,11 @@ interface DecryptedSecretProps {
|
||||
newSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
};
|
||||
oldSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
@ -58,10 +53,7 @@ interface ActionProps {
|
||||
* @param {string} obj.currentAction - the action id for which a sidebar is being displayed
|
||||
* @returns the sidebar with the payload of user activity logs
|
||||
*/
|
||||
const ActivitySideBar = ({
|
||||
toggleSidebar,
|
||||
currentAction
|
||||
}: SideBarProps) => {
|
||||
const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||
@ -72,7 +64,7 @@ const ActivitySideBar = ({
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempActionData = await getActionData({ actionId: currentAction });
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
// #TODO: make this a separate function and reuse across the app
|
||||
@ -86,12 +78,12 @@ const ActivitySideBar = ({
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
|
||||
newSecretVersion?: SecretProps;
|
||||
oldSecretVersion?: SecretProps;
|
||||
}) => {
|
||||
return {
|
||||
|
||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map(
|
||||
(encryptedSecretVersion: {
|
||||
newSecretVersion?: SecretProps;
|
||||
oldSecretVersion?: SecretProps;
|
||||
}) => ({
|
||||
newSecretVersion: {
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
|
||||
@ -107,79 +99,134 @@ const ActivitySideBar = ({
|
||||
})
|
||||
},
|
||||
oldSecretVersion: {
|
||||
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined,
|
||||
key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
: undefined,
|
||||
value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
}): undefined
|
||||
? decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV,
|
||||
tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
setActionData(decryptedSecretVersions);
|
||||
setActionMetaData({name: tempActionData.name});
|
||||
setActionMetaData({ name: tempActionData.name });
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
getLogData();
|
||||
}, [currentAction]);
|
||||
|
||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min overflow-y-auto'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("activity:event." + actionMetaData?.name)}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar("")}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
return (
|
||||
<div
|
||||
className={`absolute border-l border-mineshaft-500 ${
|
||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-min overflow-y-auto">
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">
|
||||
{t(`activity:event.${actionMetaData?.name}`)}
|
||||
</p>
|
||||
<div
|
||||
className="p-1"
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleSidebar('')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col px-4">
|
||||
{(actionMetaData?.name === 'readSecrets' ||
|
||||
actionMetaData?.name === 'addSecrets' ||
|
||||
actionMetaData?.name === 'deleteSecrets') &&
|
||||
actionData?.map((item, id) => (
|
||||
<div key={`secret.${id + 1}`}>
|
||||
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||
{item.newSecretVersion.key}
|
||||
</div>
|
||||
<DashboardInputField
|
||||
onChangeHandler={() => {}}
|
||||
type="value"
|
||||
position={1}
|
||||
value={item.newSecretVersion.value}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{actionMetaData?.name === 'updateSecrets' &&
|
||||
actionData?.map((item, id) => (
|
||||
<>
|
||||
<div className="text-xs text-bunker-200 mt-4 pl-1">
|
||||
{item.newSecretVersion.key}
|
||||
</div>
|
||||
<div className="text-bunker-100 font-mono rounded-md overflow-hidden">
|
||||
<div className="bg-red/30 px-2">
|
||||
-{' '}
|
||||
{patienceDiff(
|
||||
item.oldSecretVersion.value.split(''),
|
||||
item.newSecretVersion.value.split(''),
|
||||
false
|
||||
).lines.map(
|
||||
(character, lineId) =>
|
||||
character.bIndex !== -1 && (
|
||||
<span
|
||||
key={`actionData.${id + 1}.line.${lineId + 1}`}
|
||||
className={`${character.aIndex === -1 && 'bg-red-700/80'}`}
|
||||
>
|
||||
{character.line}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-green-500/30 px-2">
|
||||
+{' '}
|
||||
{patienceDiff(
|
||||
item.oldSecretVersion.value.split(''),
|
||||
item.newSecretVersion.value.split(''),
|
||||
false
|
||||
).lines.map(
|
||||
(character, lineId) =>
|
||||
character.aIndex !== -1 && (
|
||||
<span
|
||||
key={`actionData.${id + 1}.linev2.${lineId + 1}`}
|
||||
className={`${character.bIndex === -1 && 'bg-green-700/80'}`}
|
||||
>
|
||||
{character.line}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col px-4'>
|
||||
{(actionMetaData?.name == 'readSecrets'
|
||||
|| actionMetaData?.name == 'addSecrets'
|
||||
|| actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) =>
|
||||
<div key={id}>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<DashboardInputField
|
||||
key={id}
|
||||
onChangeHandler={() => {}}
|
||||
type="value"
|
||||
position={1}
|
||||
value={item.newSecretVersion.value}
|
||||
isDuplicate={false}
|
||||
blurred={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) =>
|
||||
<>
|
||||
<div className='text-xs text-bunker-200 mt-4 pl-1'>{item.newSecretVersion.key}</div>
|
||||
<div className='text-bunker-100 font-mono rounded-md overflow-hidden'>
|
||||
<div className='bg-red/30 px-2'>- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && <span key={id} className={`${character.aIndex == -1 && "bg-red-700/80"}`}>{character.line}</span>)}</div>
|
||||
<div className='bg-green-500/30 px-2'>+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && <span key={id} className={`${character.bIndex == -1 && "bg-green-700/80"}`}>{character.line}</span>)}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivitySideBar;
|
||||
|
@ -1,24 +1,21 @@
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import timeSince from '@app/ee/utilities/timeSince';
|
||||
import {
|
||||
faAngleDown,
|
||||
faAngleRight,
|
||||
faUpRightFromSquare
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faAngleDown, faAngleRight, faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import timeSince from '@app/ee/utilities/timeSince';
|
||||
|
||||
import guidGenerator from '../../components/utilities/randomId';
|
||||
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logData {
|
||||
interface LogData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
@ -27,15 +24,20 @@ interface logData {
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a single row of the activity table
|
||||
* @param obj
|
||||
* @param {logData} obj.row - data for a certain event
|
||||
* @param obj
|
||||
* @param {LogData} obj.row - data for a certain event
|
||||
* @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
|
||||
const ActivityLogsRow = ({
|
||||
row,
|
||||
toggleSidebar
|
||||
}: {
|
||||
row: LogData;
|
||||
toggleSidebar: (value: string) => void;
|
||||
}) => {
|
||||
const [payloadOpened, setPayloadOpened] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -43,6 +45,7 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
||||
<>
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
||||
<td
|
||||
onKeyDown={() => null}
|
||||
onClick={() => setPayloadOpened(!payloadOpened)}
|
||||
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
||||
>
|
||||
@ -54,40 +57,58 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
||||
/>
|
||||
</td>
|
||||
<td className="py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.payload?.map(action => String(action.secretVersions.length) + " " + t("activity:event." + action.name)).join(" and ")}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.user}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{row.channel}
|
||||
{row.payload
|
||||
?.map(
|
||||
(action) =>
|
||||
`${String(action.secretVersions.length)} ${t(`activity:event.${action.name}`)}`
|
||||
)
|
||||
.join(' and ')}
|
||||
</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.user}</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">{row.channel}</td>
|
||||
<td className="pl-6 py-3 border-mineshaft-700 border-t text-gray-300">
|
||||
{timeSince(new Date(row.createdAt))}
|
||||
</td>
|
||||
</tr>
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>{String(t("common:timestamp"))}</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>}
|
||||
{payloadOpened && row.payload?.map((action, index) => {
|
||||
return action.secretVersions.length > 0 && <tr key={index} className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td></td>
|
||||
<td className="">{t("activity:event." + action.name)}</td>
|
||||
<td className="text-primary-300 cursor-pointer hover:text-primary duration-200" onClick={() => toggleSidebar(action._id)}>
|
||||
{action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")}
|
||||
<FontAwesomeIcon icon={faUpRightFromSquare} className="ml-2 mb-0.5 font-light w-3 h-3"/>
|
||||
</td>
|
||||
{payloadOpened && (
|
||||
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td />
|
||||
<td>{String(t('common:timestamp'))}</td>
|
||||
<td>{row.createdAt}</td>
|
||||
</tr>
|
||||
})}
|
||||
)}
|
||||
{payloadOpened &&
|
||||
<tr className='h-9 text-bunker-200 border-mineshaft-700 border-t text-sm'>
|
||||
<td></td>
|
||||
<td>{String(t("activity:ip-address"))}</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
</tr>}
|
||||
row.payload?.map(
|
||||
(action) =>
|
||||
action.secretVersions.length > 0 && (
|
||||
<tr
|
||||
key={action._id}
|
||||
className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm"
|
||||
>
|
||||
<td />
|
||||
<td className="">{t(`activity:event.${action.name}`)}</td>
|
||||
<td
|
||||
onKeyDown={() => null}
|
||||
className="text-primary-300 cursor-pointer hover:text-primary duration-200"
|
||||
onClick={() => toggleSidebar(action._id)}
|
||||
>
|
||||
{action.secretVersions.length +
|
||||
(action.secretVersions.length !== 1 ? ' secrets' : ' secret')}
|
||||
<FontAwesomeIcon
|
||||
icon={faUpRightFromSquare}
|
||||
className="ml-2 mb-0.5 font-light w-3 h-3"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
{payloadOpened && (
|
||||
<tr className="h-9 text-bunker-200 border-mineshaft-700 border-t text-sm">
|
||||
<td />
|
||||
<td>{String(t('activity:ip-address'))}</td>
|
||||
<td>{row.ipAddress}</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -100,37 +121,61 @@ const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar:
|
||||
* @param {boolean} obj.isLoading - whether the log data has been loaded yet or not
|
||||
* @returns
|
||||
*/
|
||||
const ActivityTable = ({ data, toggleSidebar, isLoading }: { data: logData[], toggleSidebar: (value: string) => void; isLoading: boolean; }) => {
|
||||
const ActivityTable = ({
|
||||
data,
|
||||
toggleSidebar,
|
||||
isLoading
|
||||
}: {
|
||||
data: LogData[];
|
||||
toggleSidebar: (value: string) => void;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 mt-8">
|
||||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative">
|
||||
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5"></div>
|
||||
<div className="absolute rounded-t-md w-full h-[3rem] bg-white/5" />
|
||||
<table className="w-full my-1">
|
||||
<thead className="text-bunker-300">
|
||||
<tr className='text-sm'>
|
||||
<th className="text-left pl-6 pt-2.5 pb-3"></th>
|
||||
<th className="text-left font-semibold pt-2.5 pb-3">{String(t("common:event")).toUpperCase()}</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:user")).toUpperCase()}</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:source")).toUpperCase()}</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">{String(t("common:time")).toUpperCase()}</th>
|
||||
<th></th>
|
||||
<tr className="text-sm">
|
||||
<th aria-label="actions" className="text-left pl-6 pt-2.5 pb-3" />
|
||||
<th className="text-left font-semibold pt-2.5 pb-3">
|
||||
{String(t('common:event')).toUpperCase()}
|
||||
</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||
{String(t('common:user')).toUpperCase()}
|
||||
</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||
{String(t('common:source')).toUpperCase()}
|
||||
</th>
|
||||
<th className="text-left font-semibold pl-6 pt-2.5 pb-3">
|
||||
{String(t('common:time')).toUpperCase()}
|
||||
</th>
|
||||
<th aria-label="action" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, index) => {
|
||||
return <ActivityLogsRow key={index} row={row} toggleSidebar={toggleSidebar} />;
|
||||
})}
|
||||
{data?.map((row, index) => (
|
||||
<ActivityLogsRow
|
||||
key={`activity.${index + 1}.${row._id}`}
|
||||
row={row}
|
||||
toggleSidebar={toggleSidebar}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{isLoading && <div className='w-full flex justify-center mb-8 mt-4'><Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="loading animation"
|
||||
></Image></div>}
|
||||
{isLoading && (
|
||||
<div className="w-full flex justify-center mb-8 mt-4">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="loading animation"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getProjectSecretShanpshots from "@app/ee/api/secrets/GetProjectSercetShanpshots";
|
||||
import getSecretSnapshotData from "@app/ee/api/secrets/GetSecretSnapshotData";
|
||||
import timeSince from "@app/ee/utilities/timeSince";
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import { decryptAssymmetric, decryptSymmetric } from "~/components/utilities/cryptography/crypto";
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from '@app/components/utilities/cryptography/crypto';
|
||||
import getProjectSecretShanpshots from '@app/ee/api/secrets/GetProjectSercetShanpshots';
|
||||
import getSecretSnapshotData from '@app/ee/api/secrets/GetSecretSnapshotData';
|
||||
import timeSince from '@app/ee/utilities/timeSince';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
export interface SecretDataProps {
|
||||
pos: number;
|
||||
@ -23,7 +26,7 @@ export interface SecretDataProps {
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: boolean) => void;
|
||||
toggleSidebar: (value: boolean) => void;
|
||||
setSnapshotData: (value: any) => void;
|
||||
chosenSnapshot: string;
|
||||
}
|
||||
@ -44,7 +47,7 @@ interface EncrypetedSecretVersionListProps {
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
environment: string;
|
||||
type: "personal" | "shared";
|
||||
type: 'personal' | 'shared';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -54,11 +57,7 @@ interface EncrypetedSecretVersionListProps {
|
||||
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
|
||||
* @returns the sidebar with the options for point-in-time recovery (commits)
|
||||
*/
|
||||
const PITRecoverySidebar = ({
|
||||
toggleSidebar,
|
||||
setSnapshotData,
|
||||
chosenSnapshot
|
||||
}: SideBarProps) => {
|
||||
const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }: SideBarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -68,22 +67,26 @@ const PITRecoverySidebar = ({
|
||||
|
||||
const loadMoreSnapshots = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const results = await getProjectSecretShanpshots({ workspaceId: String(router.query.id), limit: currentLimit, offset: currentOffset })
|
||||
const results = await getProjectSecretShanpshots({
|
||||
workspaceId: String(router.query.id),
|
||||
limit: currentLimit,
|
||||
offset: currentOffset
|
||||
});
|
||||
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
getLogData();
|
||||
}, [currentOffset]);
|
||||
|
||||
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string; }) => {
|
||||
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string }) => {
|
||||
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
@ -97,10 +100,10 @@ const PITRecoverySidebar = ({
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => {
|
||||
return {
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.map(
|
||||
(encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
|
||||
id: encryptedSecretVersion._id,
|
||||
pos: pos,
|
||||
pos,
|
||||
type: encryptedSecretVersion.type,
|
||||
environment: encryptedSecretVersion.environment,
|
||||
key: decryptSymmetric({
|
||||
@ -115,69 +118,132 @@ const PITRecoverySidebar = ({
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
const secretKeys = [
|
||||
...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))
|
||||
];
|
||||
|
||||
const secretKeys = [...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))];
|
||||
|
||||
const result = secretKeys.map((key, index) => {
|
||||
return {
|
||||
id: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].id,
|
||||
pos: index,
|
||||
key: key,
|
||||
environment: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0].environment,
|
||||
value: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'shared')[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter((secret: SecretDataProps) => secret.key == key && secret.type == 'personal')[0]?.value,
|
||||
}
|
||||
const result = secretKeys.map((key, index) => ({
|
||||
id: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].id,
|
||||
pos: index,
|
||||
key,
|
||||
environment: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0].environment,
|
||||
value: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.value
|
||||
}));
|
||||
|
||||
setSnapshotData({
|
||||
id: secretSnapshotData._id,
|
||||
version: secretSnapshotData.version,
|
||||
createdAt: secretSnapshotData.createdAt,
|
||||
secretVersions: result,
|
||||
comment: ''
|
||||
});
|
||||
};
|
||||
|
||||
setSnapshotData({ id: secretSnapshotData._id, version: secretSnapshotData.version, createdAt: secretSnapshotData.createdAt, secretVersions: result, comment: '' })
|
||||
}
|
||||
|
||||
return <div className={`absolute border-l border-mineshaft-500 ${isLoading ? "bg-bunker-800" : "bg-bunker"} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-min'>
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t("Point-in-time Recovery")}</p>
|
||||
<div className='p-1' onClick={() => toggleSidebar(false)}>
|
||||
<FontAwesomeIcon icon={faX} className='w-4 h-4 text-bunker-300 cursor-pointer'/>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={`absolute border-l border-mineshaft-500 ${
|
||||
isLoading ? 'bg-bunker-800' : 'bg-bunker'
|
||||
} fixed h-full w-96 top-14 right-0 z-40 shadow-xl flex flex-col justify-between`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full mb-8">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col px-2 py-2 overflow-y-auto h-[92vh]'>
|
||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) =>
|
||||
<div
|
||||
key={snapshot._id}
|
||||
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
||||
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "bg-primary text-black pointer-events-none" : "bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer"} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
|
||||
) : (
|
||||
<div className="h-min">
|
||||
<div className="flex flex-row px-4 py-3 border-b border-mineshaft-500 justify-between items-center">
|
||||
<p className="font-semibold text-lg text-bunker-200">{t('Point-in-time Recovery')}</p>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="p-1"
|
||||
onClick={() => toggleSidebar(false)}
|
||||
>
|
||||
<div className="flex flex-row items-start">
|
||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800" : "text-bunker-200"} text-sm mr-1.5`}>{timeSince(new Date(snapshot.createdAt))}</div>
|
||||
<div className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-900" : "text-bunker-300"} text-sm `}>{" - " + snapshot.secretVersions.length + " Secrets"}</div>
|
||||
<FontAwesomeIcon icon={faX} className="w-4 h-4 text-bunker-300 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col px-2 py-2 overflow-y-auto h-[92vh]">
|
||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={snapshot._id}
|
||||
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||
? 'bg-primary text-black pointer-events-none'
|
||||
: 'bg-mineshaft-700 hover:bg-mineshaft-500 duration-200 cursor-pointer'
|
||||
} py-3 px-4 mb-2 rounded-md flex flex-row justify-between items-center`}
|
||||
>
|
||||
<div className="flex flex-row items-start">
|
||||
<div
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||
? 'text-bunker-800'
|
||||
: 'text-bunker-200'
|
||||
} text-sm mr-1.5`}
|
||||
>
|
||||
{timeSince(new Date(snapshot.createdAt))}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||
? 'text-bunker-900'
|
||||
: 'text-bunker-300'
|
||||
} text-sm `}
|
||||
>{` - ${snapshot.secretVersions.length} Secrets`}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === '')
|
||||
? 'text-bunker-800 pointer-events-none'
|
||||
: 'text-bunker-200 hover:text-primary duration-200 cursor-pointer'
|
||||
} text-sm`}
|
||||
>
|
||||
{id === 0
|
||||
? 'Current Version'
|
||||
: chosenSnapshot === snapshot._id
|
||||
? 'Currently Viewing'
|
||||
: 'Explore'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${chosenSnapshot == snapshot._id || (id == 0 && chosenSnapshot === "") ? "text-bunker-800 pointer-events-none" : "text-bunker-200 hover:text-primary duration-200 cursor-pointer"} text-sm`}>
|
||||
{id == 0 ? "Current Version" : chosenSnapshot == snapshot._id ? "Currently Viewing" : "Explore"}
|
||||
))}
|
||||
<div className="flex justify-center w-full mb-14">
|
||||
<div className="items-center w-40">
|
||||
<Button
|
||||
text="View More"
|
||||
textDisabled="End of History"
|
||||
active={secretSnapshotsMetadata.length % 15 === 0}
|
||||
onButtonPressed={loadMoreSnapshots}
|
||||
size="md"
|
||||
color="mineshaft"
|
||||
/>
|
||||
</div>
|
||||
</div>)}
|
||||
<div className='flex justify-center w-full mb-14'>
|
||||
<div className='items-center w-40'>
|
||||
<Button text="View More" textDisabled="End of History" active={secretSnapshotsMetadata.length % 15 == 0 ? true : false} onButtonPressed={loadMoreSnapshots} size="md" color="mineshaft"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PITRecoverySidebar;
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCircle, faDotCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { decryptAssymmetric, decryptSymmetric } from '~/components/utilities/cryptography/crypto';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from '@app/components/utilities/cryptography/crypto';
|
||||
import getSecretVersions from '@app/ee/api/secrets/GetSecretVersions';
|
||||
import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
interface DecryptedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
@ -21,24 +24,23 @@ interface EncrypetedSecretVersionListProps {
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} secretId - the id of a secret for which are querying version history
|
||||
* @returns a list of versions for a specific secret
|
||||
*/
|
||||
const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const getSecretVersionHistory = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10});
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
|
||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10 });
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
@ -52,8 +54,8 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
|
||||
return {
|
||||
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map(
|
||||
(encryptedSecretVersion: EncrypetedSecretVersionListProps) => ({
|
||||
createdAt: encryptedSecretVersion.createdAt,
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
@ -61,63 +63,76 @@ const SecretVersionList = ({ secretId }: { secretId: string; }) => {
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
setSecretVersions(decryptedSecretVersions);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
getSecretVersionHistory();
|
||||
}, [secretId]);
|
||||
|
||||
return <div className='w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none'>
|
||||
<p className=''>{t("dashboard:sidebar.version-history")}</p>
|
||||
<div className='p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full'>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
></Image>
|
||||
</div>
|
||||
) : (
|
||||
<div className='h-48 overflow-y-auto overflow-x-none'>
|
||||
{secretVersions
|
||||
? secretVersions?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) =>
|
||||
<div key={index} className='flex flex-row'>
|
||||
<div className='pr-1 flex flex-col items-center'>
|
||||
<div className='p-1'><FontAwesomeIcon icon={index == 0 ? faDotCircle : faCircle} /></div>
|
||||
<div className='w-0 h-full border-l mt-1'></div>
|
||||
</div>
|
||||
<div className='flex flex-col w-full max-w-[calc(100%-2.3rem)]'>
|
||||
<div className='pr-2 pt-1'>
|
||||
{(new Date(version.createdAt)).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
return (
|
||||
<div className="w-full h-52 px-4 mt-4 text-sm text-bunker-300 overflow-x-none">
|
||||
<p className="">{t('dashboard:sidebar.version-history')}</p>
|
||||
<div className="p-1 rounded-md bg-bunker-800 border border-mineshaft-500 overflow-x-none h-full">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-48 overflow-y-auto overflow-x-none">
|
||||
{secretVersions ? (
|
||||
secretVersions
|
||||
?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) => (
|
||||
<div key={`${version.createdAt}.${index + 1}`} className="flex flex-row">
|
||||
<div className="pr-1 flex flex-col items-center">
|
||||
<div className="p-1">
|
||||
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
|
||||
</div>
|
||||
<div className="w-0 h-full border-l mt-1" />
|
||||
</div>
|
||||
<div className="flex flex-col w-full max-w-[calc(100%-2.3rem)]">
|
||||
<div className="pr-2 pt-1">
|
||||
{new Date(version.createdAt).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
<div className="">
|
||||
<p className="break-words">
|
||||
<span className="py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5">
|
||||
Value:
|
||||
</span>
|
||||
{version.value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className=''><p className='break-words'><span className='py-0.5 px-1 rounded-md bg-primary-200/10 mr-1.5'>Value:</span>{version.value}</p></div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-bunker-400">
|
||||
No version history yet.
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
|
@ -1,270 +1,284 @@
|
||||
/* eslint-disable */
|
||||
// TODO(akhilmhdh): Danger. This file functions needs to simplified.
|
||||
// Disabled eslinting as below algos uses unnary operator and need to be careful on handling it
|
||||
// Will revisit this and change to simplier non-recursive to dynamic programming with space allocation strategy
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param textOld - old secret
|
||||
* @param textNew - new (updated) secret
|
||||
* @param diffPlusFlag - a flag for whether we want to detect moving segments
|
||||
* @param diffPlusFlag - a flag for whether we want to detect moving segments
|
||||
* - doesn't work in some examples (e.g., when we have a full reverse ordering of the text)
|
||||
* @returns
|
||||
* @returns
|
||||
*/
|
||||
function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boolean) {
|
||||
|
||||
/**
|
||||
* findUnique finds all unique values in arr[lo..hi], inclusive. This
|
||||
* function is used in preparation for determining the longest common
|
||||
* subsequence. Specifically, it first reduces the array range in question
|
||||
* to unique values.
|
||||
* @param chars - an array of characters
|
||||
* @param lo
|
||||
* @param hi
|
||||
* @param lo
|
||||
* @param hi
|
||||
* @returns - an ordered Map, with the arr[i] value as the Map key and the
|
||||
* array index i as the Map value.
|
||||
* array index i as the Map value.
|
||||
*/
|
||||
function findUnique(chars: string[], lo: number, hi: number) {
|
||||
const characterMap = new Map();
|
||||
function findUnique(chars: string[], lo: number, hi: number) {
|
||||
const characterMap = new Map();
|
||||
|
||||
for (let i=lo; i<=hi; i++) {
|
||||
const character = chars[i];
|
||||
|
||||
if (characterMap.has(character)) {
|
||||
characterMap.get(character).count++;
|
||||
characterMap.get(character).index = i;
|
||||
} else {
|
||||
characterMap.set(character, { count: 1, index: i });
|
||||
}
|
||||
}
|
||||
for (let i = lo; i <= hi; i += 1) {
|
||||
const character = chars[i];
|
||||
|
||||
characterMap.forEach((val, key, map) => {
|
||||
if (val.count !== 1) {
|
||||
map.delete(key);
|
||||
} else {
|
||||
map.set(key, val.index);
|
||||
}
|
||||
});
|
||||
if (characterMap.has(character)) {
|
||||
characterMap.get(character).count += 1;
|
||||
characterMap.get(character).index = i;
|
||||
} else {
|
||||
characterMap.set(character, { count: 1, index: i });
|
||||
}
|
||||
}
|
||||
|
||||
return characterMap;
|
||||
}
|
||||
characterMap.forEach((val, key, map) => {
|
||||
if (val.count !== 1) {
|
||||
map.delete(key);
|
||||
} else {
|
||||
map.set(key, val.index);
|
||||
}
|
||||
});
|
||||
|
||||
return characterMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param aArray
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bArray
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @param aArray
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bArray
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @returns an ordered Map, with the Map key as the common line between aArray
|
||||
* and bArray, with the Map value as an object containing the array indexes of
|
||||
* the matching unique lines.
|
||||
*
|
||||
*
|
||||
*/
|
||||
function uniqueCommon(aArray: string[], aLo: number, aHi: number, bArray: string[], bLo: number, bHi: number) {
|
||||
const ma = findUnique(aArray, aLo, aHi);
|
||||
const mb = findUnique(bArray, bLo, bHi);
|
||||
function uniqueCommon(
|
||||
aArray: string[],
|
||||
aLo: number,
|
||||
aHi: number,
|
||||
bArray: string[],
|
||||
bLo: number,
|
||||
bHi: number
|
||||
) {
|
||||
const ma = findUnique(aArray, aLo, aHi);
|
||||
const mb = findUnique(bArray, bLo, bHi);
|
||||
|
||||
ma.forEach((val, key, map) => {
|
||||
if (mb.has(key)) {
|
||||
map.set(key, {
|
||||
indexA: val,
|
||||
indexB: mb.get(key)
|
||||
});
|
||||
} else {
|
||||
map.delete(key);
|
||||
}
|
||||
});
|
||||
ma.forEach((val, key, map) => {
|
||||
if (mb.has(key)) {
|
||||
map.set(key, {
|
||||
indexA: val,
|
||||
indexB: mb.get(key)
|
||||
});
|
||||
} else {
|
||||
map.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
return ma;
|
||||
}
|
||||
return ma;
|
||||
}
|
||||
|
||||
/**
|
||||
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
|
||||
* and determines the Longest Common Subsequence (LCS).
|
||||
* @param abMap
|
||||
* @param abMap
|
||||
* @returns an ordered array of objects containing the array indexes of the
|
||||
* matching lines for a LCS.
|
||||
*/
|
||||
function longestCommonSubsequence(abMap: Map<number, { indexA: number, indexB: number, prev?: number }>) {
|
||||
const ja: any = [];
|
||||
function longestCommonSubsequence(
|
||||
abMap: Map<number, { indexA: number; indexB: number; prev?: number }>
|
||||
) {
|
||||
const ja: any = [];
|
||||
|
||||
// First, walk the list creating the jagged array.
|
||||
abMap.forEach((val, key, map) => {
|
||||
let i = 0;
|
||||
|
||||
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
|
||||
i++;
|
||||
}
|
||||
// First, walk the list creating the jagged array.
|
||||
abMap.forEach((val, key, map) => {
|
||||
let i = 0;
|
||||
|
||||
if (!ja[i]) {
|
||||
ja[i] = [];
|
||||
}
|
||||
while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (0 < i) {
|
||||
val.prev = ja[i-1][ja[i - 1].length - 1];
|
||||
}
|
||||
ja[i].push(val);
|
||||
});
|
||||
if (!ja[i]) {
|
||||
ja[i] = [];
|
||||
}
|
||||
|
||||
// Now, pull out the longest common subsequence.
|
||||
let lcs: any[] = [];
|
||||
|
||||
if (0 < ja.length) {
|
||||
const n = ja.length - 1;
|
||||
lcs = [ja[n][ja[n].length - 1]];
|
||||
|
||||
while (lcs[lcs.length - 1].prev) {
|
||||
lcs.push(lcs[lcs.length - 1].prev);
|
||||
}
|
||||
}
|
||||
if (i > 0) {
|
||||
val.prev = ja[i - 1][ja[i - 1].length - 1];
|
||||
}
|
||||
ja[i].push(val);
|
||||
});
|
||||
|
||||
return lcs.reverse();
|
||||
}
|
||||
// Now, pull out the longest common subsequence.
|
||||
let lcs: any[] = [];
|
||||
|
||||
// "result" is the array used to accumulate the textOld that are deleted, the
|
||||
// lines that are shared between textOld and textNew, and the textNew that were
|
||||
// inserted.
|
||||
|
||||
const result: any[] = [];
|
||||
let deleted = 0;
|
||||
let inserted = 0;
|
||||
if (ja.length > 0) {
|
||||
const n = ja.length - 1;
|
||||
lcs = [ja[n][ja[n].length - 1]];
|
||||
|
||||
// aMove and bMove will contain the lines that don't match, and will be returned
|
||||
// for possible searching of lines that moved.
|
||||
|
||||
const aMove: any[] = [];
|
||||
const aMoveIndex: any[] = [];
|
||||
const bMove: any[] = [];
|
||||
const bMoveIndex: any[] = [];
|
||||
while (lcs[lcs.length - 1].prev) {
|
||||
lcs.push(lcs[lcs.length - 1].prev);
|
||||
}
|
||||
}
|
||||
|
||||
return lcs.reverse();
|
||||
}
|
||||
|
||||
// "result" is the array used to accumulate the textOld that are deleted, the
|
||||
// lines that are shared between textOld and textNew, and the textNew that were
|
||||
// inserted.
|
||||
|
||||
const result: any[] = [];
|
||||
let deleted = 0;
|
||||
let inserted = 0;
|
||||
|
||||
// aMove and bMove will contain the lines that don't match, and will be returned
|
||||
// for possible searching of lines that moved.
|
||||
|
||||
const aMove: any[] = [];
|
||||
const aMoveIndex: any[] = [];
|
||||
const bMove: any[] = [];
|
||||
const bMoveIndex: any[] = [];
|
||||
|
||||
/**
|
||||
* addToResult simply pushes the latest value onto the "result" array. This
|
||||
* array captures the diff of the line, aIndex, and bIndex from the textOld
|
||||
* and textNew array.
|
||||
* @param aIndex
|
||||
* @param bIndex
|
||||
* @param aIndex
|
||||
* @param bIndex
|
||||
*/
|
||||
function addToResult(aIndex: number, bIndex: number) {
|
||||
if (bIndex < 0) {
|
||||
aMove.push(textOld[aIndex]);
|
||||
aMoveIndex.push(result.length);
|
||||
deleted++;
|
||||
} else if (aIndex < 0) {
|
||||
bMove.push(textNew[bIndex]);
|
||||
bMoveIndex.push(result.length);
|
||||
inserted++;
|
||||
}
|
||||
function addToResult(aIndex: number, bIndex: number) {
|
||||
if (bIndex < 0) {
|
||||
aMove.push(textOld[aIndex]);
|
||||
aMoveIndex.push(result.length);
|
||||
deleted += 1;
|
||||
} else if (aIndex < 0) {
|
||||
bMove.push(textNew[bIndex]);
|
||||
bMoveIndex.push(result.length);
|
||||
inserted += 1;
|
||||
}
|
||||
|
||||
result.push({
|
||||
line: aIndex >= 0 ? textOld[aIndex] : textNew[bIndex],
|
||||
aIndex,
|
||||
bIndex
|
||||
});
|
||||
}
|
||||
|
||||
result.push({
|
||||
line: 0 <= aIndex ? textOld[aIndex] : textNew[bIndex],
|
||||
aIndex: aIndex,
|
||||
bIndex: bIndex,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* addSubMatch handles the lines between a pair of entries in the LCS. Thus,
|
||||
* this function might recursively call recurseLCS to further match the lines
|
||||
* between textOld and textNew.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
*/
|
||||
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
|
||||
// Match any lines at the beginning of textOld and textNew.
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
|
||||
addToResult(aLo++, bLo++);
|
||||
}
|
||||
function addSubMatch(aLo: number, aHi: number, bLo: number, bHi: number) {
|
||||
// Match any lines at the beginning of textOld and textNew.
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aLo] === textNew[bLo]) {
|
||||
addToResult(aLo++, bLo++);
|
||||
}
|
||||
|
||||
// Match any lines at the end of textOld and textNew, but don't place them
|
||||
// in the "result" array just yet, as the lines between these matches at
|
||||
// the beginning and the end need to be analyzed first.
|
||||
|
||||
const aHiTemp = aHi;
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
|
||||
aHi--;
|
||||
bHi--;
|
||||
}
|
||||
// Match any lines at the end of textOld and textNew, but don't place them
|
||||
// in the "result" array just yet, as the lines between these matches at
|
||||
// the beginning and the end need to be analyzed first.
|
||||
|
||||
// Now, check to determine with the remaining lines in the subsequence
|
||||
// whether there are any unique common lines between textOld and textNew.
|
||||
//
|
||||
// If not, add the subsequence to the result (all textOld having been
|
||||
// deleted, and all textNew having been inserted).
|
||||
//
|
||||
// If there are unique common lines between textOld and textNew, then let's
|
||||
// recursively perform the patience diff on the subsequence.
|
||||
|
||||
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
|
||||
|
||||
if (uniqueCommonMap.size === 0) {
|
||||
while (aLo <= aHi) {
|
||||
addToResult(aLo++, -1);
|
||||
}
|
||||
const aHiTemp = aHi;
|
||||
while (aLo <= aHi && bLo <= bHi && textOld[aHi] === textNew[bHi]) {
|
||||
aHi -= 1;
|
||||
bHi -= 1;
|
||||
}
|
||||
|
||||
while (bLo <= bHi) {
|
||||
addToResult(-1, bLo++);
|
||||
}
|
||||
} else {
|
||||
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
|
||||
}
|
||||
// Now, check to determine with the remaining lines in the subsequence
|
||||
// whether there are any unique common lines between textOld and textNew.
|
||||
//
|
||||
// If not, add the subsequence to the result (all textOld having been
|
||||
// deleted, and all textNew having been inserted).
|
||||
//
|
||||
// If there are unique common lines between textOld and textNew, then let's
|
||||
// recursively perform the patience diff on the subsequence.
|
||||
|
||||
const uniqueCommonMap = uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi);
|
||||
|
||||
if (uniqueCommonMap.size === 0) {
|
||||
while (aLo <= aHi) {
|
||||
addToResult(aLo++, -1);
|
||||
}
|
||||
|
||||
while (bLo <= bHi) {
|
||||
addToResult(-1, bLo++);
|
||||
}
|
||||
} else {
|
||||
recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);
|
||||
}
|
||||
|
||||
// Finally, let's add the matches at the end to the result.
|
||||
while (aHi < aHiTemp) {
|
||||
addToResult(++aHi, ++bHi);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, let's add the matches at the end to the result.
|
||||
while (aHi < aHiTemp) {
|
||||
addToResult(++aHi, ++bHi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* recurseLCS finds the longest common subsequence (LCS) between the arrays
|
||||
* textOld[aLo..aHi] and textNew[bLo..bHi] inclusive. Then for each subsequence
|
||||
* recursively performs another LCS search (via addSubMatch), until there are
|
||||
* none found, at which point the subsequence is dumped to the result.
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @param uniqueCommonMap
|
||||
* @param aLo
|
||||
* @param aHi
|
||||
* @param bLo
|
||||
* @param bHi
|
||||
* @param uniqueCommonMap
|
||||
*/
|
||||
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
|
||||
const x = longestCommonSubsequence(uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi));
|
||||
|
||||
if (x.length === 0) {
|
||||
addSubMatch(aLo, aHi, bLo, bHi);
|
||||
} else {
|
||||
if (aLo < x[0].indexA || bLo < x[0].indexB) {
|
||||
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
|
||||
}
|
||||
function recurseLCS(aLo: number, aHi: number, bLo: number, bHi: number, uniqueCommonMap?: any) {
|
||||
const x = longestCommonSubsequence(
|
||||
uniqueCommonMap || uniqueCommon(textOld, aLo, aHi, textNew, bLo, bHi)
|
||||
);
|
||||
|
||||
let i;
|
||||
for (i = 0; i < x.length - 1; i++) {
|
||||
addSubMatch(x[i].indexA, x[i+1].indexA - 1, x[i].indexB, x[i+1].indexB - 1);
|
||||
}
|
||||
if (x.length === 0) {
|
||||
addSubMatch(aLo, aHi, bLo, bHi);
|
||||
} else {
|
||||
if (aLo < x[0].indexA || bLo < x[0].indexB) {
|
||||
addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);
|
||||
}
|
||||
|
||||
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
|
||||
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
||||
}
|
||||
}
|
||||
}
|
||||
let i;
|
||||
for (i = 0; i < x.length - 1; i += 1) {
|
||||
addSubMatch(x[i].indexA, x[i + 1].indexA - 1, x[i].indexB, x[i + 1].indexB - 1);
|
||||
}
|
||||
|
||||
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
||||
if (x[i].indexA <= aHi || x[i].indexB <= bHi) {
|
||||
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (diffPlusFlag) {
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
aMove: aMove,
|
||||
aMoveIndex: aMoveIndex,
|
||||
bMove: bMove,
|
||||
bMoveIndex: bMoveIndex,
|
||||
};
|
||||
}
|
||||
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
||||
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
};
|
||||
if (diffPlusFlag) {
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
aMove,
|
||||
aMoveIndex,
|
||||
bMove,
|
||||
bMoveIndex
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -289,58 +303,54 @@ function patienceDiff(textOld: string[], textNew: string[], diffPlusFlag?: boole
|
||||
*/
|
||||
|
||||
function patienceDiffPlus(textOld: string[], textNew: string[]) {
|
||||
const difference = patienceDiff(textOld, textNew, true);
|
||||
|
||||
const difference = patienceDiff(textOld, textNew, true);
|
||||
let aMoveNext = difference.aMove;
|
||||
let aMoveIndexNext = difference.aMoveIndex;
|
||||
let bMoveNext = difference.bMove;
|
||||
let bMoveIndexNext = difference.bMoveIndex;
|
||||
|
||||
let aMoveNext = difference.aMove;
|
||||
let aMoveIndexNext = difference.aMoveIndex;
|
||||
let bMoveNext = difference.bMove;
|
||||
let bMoveIndexNext = difference.bMoveIndex;
|
||||
delete difference.aMove;
|
||||
delete difference.aMoveIndex;
|
||||
delete difference.bMove;
|
||||
delete difference.bMoveIndex;
|
||||
|
||||
delete difference.aMove;
|
||||
delete difference.aMoveIndex;
|
||||
delete difference.bMove;
|
||||
delete difference.bMoveIndex;
|
||||
|
||||
let lastLineCountMoved;
|
||||
let lastLineCountMoved;
|
||||
|
||||
do {
|
||||
const aMove = aMoveNext;
|
||||
const aMoveIndex = aMoveIndexNext;
|
||||
const bMove = bMoveNext;
|
||||
const bMoveIndex = bMoveIndexNext;
|
||||
do {
|
||||
const aMove = aMoveNext;
|
||||
const aMoveIndex = aMoveIndexNext;
|
||||
const bMove = bMoveNext;
|
||||
const bMoveIndex = bMoveIndexNext;
|
||||
|
||||
aMoveNext = [];
|
||||
aMoveIndexNext = [];
|
||||
bMoveNext = [];
|
||||
bMoveIndexNext = [];
|
||||
aMoveNext = [];
|
||||
aMoveIndexNext = [];
|
||||
bMoveNext = [];
|
||||
bMoveIndexNext = [];
|
||||
|
||||
const subDiff = patienceDiff(aMove!, bMove!);
|
||||
|
||||
lastLineCountMoved = difference.lineCountMoved;
|
||||
const subDiff = patienceDiff(aMove!, bMove!);
|
||||
|
||||
subDiff.lines.forEach((v, i) => {
|
||||
lastLineCountMoved = difference.lineCountMoved;
|
||||
|
||||
if (0 <= v.aIndex && 0 <= v.bIndex) {
|
||||
|
||||
difference.lines[aMoveIndex![v.aIndex]].moved = true;
|
||||
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
|
||||
difference.lines[bMoveIndex![v.bIndex]].moved = true;
|
||||
difference.lineCountInserted--;
|
||||
difference.lineCountDeleted--;
|
||||
difference.lineCountMoved++;
|
||||
} else if (v.bIndex < 0) {
|
||||
aMoveNext!.push(aMove![v.aIndex]);
|
||||
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
|
||||
} else {
|
||||
bMoveNext!.push(bMove![v.bIndex]);
|
||||
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
|
||||
}
|
||||
});
|
||||
} while (0 < difference.lineCountMoved - lastLineCountMoved);
|
||||
|
||||
return difference;
|
||||
subDiff.lines.forEach((v, i) => {
|
||||
if (v.aIndex >= 0 && v.bIndex >= 0) {
|
||||
difference.lines[aMoveIndex![v.aIndex]].moved = true;
|
||||
difference.lines[bMoveIndex![v.bIndex]].aIndex = aMoveIndex![v.aIndex];
|
||||
difference.lines[bMoveIndex![v.bIndex]].moved = true;
|
||||
difference.lineCountInserted -= 1;
|
||||
difference.lineCountDeleted -= 1;
|
||||
difference.lineCountMoved += 1;
|
||||
} else if (v.bIndex < 0) {
|
||||
aMoveNext!.push(aMove![v.aIndex]);
|
||||
aMoveIndexNext!.push(aMoveIndex![v.aIndex]);
|
||||
} else {
|
||||
bMoveNext!.push(bMove![v.bIndex]);
|
||||
bMoveIndexNext!.push(bMoveIndex![v.bIndex]);
|
||||
}
|
||||
});
|
||||
} while (difference.lineCountMoved - lastLineCountMoved > 0);
|
||||
|
||||
return difference;
|
||||
}
|
||||
|
||||
export default patienceDiff;
|
||||
|
@ -11,25 +11,25 @@ function timeSince(date: Date) {
|
||||
let interval = seconds / 31536000;
|
||||
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' years ago';
|
||||
return `${Math.floor(interval) } years ago`;
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' months ago';
|
||||
return `${Math.floor(interval) } months ago`;
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' days ago';
|
||||
return `${Math.floor(interval) } days ago`;
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' hours ago';
|
||||
return `${Math.floor(interval) } hours ago`;
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' minutes ago';
|
||||
return `${Math.floor(interval) } minutes ago`;
|
||||
}
|
||||
return Math.floor(seconds) + ' seconds ago';
|
||||
return `${Math.floor(seconds) } seconds ago`;
|
||||
}
|
||||
|
||||
export default timeSince;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
interface usePopUpProps {
|
||||
interface UsePopUpProps {
|
||||
name: Readonly<string>;
|
||||
isOpen: boolean;
|
||||
}
|
||||
@ -10,18 +10,18 @@ interface usePopUpProps {
|
||||
* checks which type of inputProps were given and converts them into key-names
|
||||
* SIDENOTE: On inputting give it as const and not string with (as const)
|
||||
*/
|
||||
type usePopUpState<T extends Readonly<string[]> | usePopUpProps[]> = {
|
||||
[P in T extends usePopUpProps[] ? T[number]['name'] : T[number]]: {
|
||||
type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
|
||||
[P in T extends UsePopUpProps[] ? T[number]['name'] : T[number]]: {
|
||||
isOpen: boolean;
|
||||
data?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
|
||||
popUp: usePopUpState<T>;
|
||||
handlePopUpOpen: (popUpName: keyof usePopUpState<T>, data?: unknown) => void;
|
||||
handlePopUpClose: (popUpName: keyof usePopUpState<T>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof usePopUpState<T>) => void;
|
||||
interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
|
||||
popUp: UsePopUpState<T>;
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
|
||||
handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
|
||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<T>) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -29,34 +29,31 @@ interface usePopUpReturn<T extends Readonly<string[]> | usePopUpProps[]> {
|
||||
* Provides api to open,close,toggle and also store temporary data for the popUp
|
||||
* @param popUpNames: the names of popUp containers eg: ["popUp1","second"] or [{name:"popUp2",isOpen:bool}]
|
||||
*/
|
||||
export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>(
|
||||
export const usePopUp = <T extends Readonly<string[]> | UsePopUpProps[]>(
|
||||
popUpNames: T
|
||||
): usePopUpReturn<T> => {
|
||||
const [popUp, setPopUp] = useState<usePopUpState<T>>(
|
||||
): UsePopUpReturn<T> => {
|
||||
const [popUp, setPopUp] = useState<UsePopUpState<T>>(
|
||||
Object.fromEntries(
|
||||
popUpNames.map((popUpName) =>
|
||||
typeof popUpName === 'string'
|
||||
? [popUpName, { isOpen: false }]
|
||||
: [popUpName.name, { isOpen: popUpName.isOpen }]
|
||||
) // convert into an array of [[popUpName,state]] then into Object
|
||||
) as usePopUpState<T> // to override generic string return type of the function
|
||||
) as UsePopUpState<T> // to override generic string return type of the function
|
||||
);
|
||||
|
||||
const handlePopUpOpen = useCallback(
|
||||
(popUpName: keyof usePopUpState<T>, data?: unknown) => {
|
||||
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: true, data } }));
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handlePopUpClose = useCallback((popUpName: keyof usePopUpState<T>) => {
|
||||
setPopUp((popUp) => ({ ...popUp, [popUpName]: { isOpen: false } }));
|
||||
const handlePopUpOpen = useCallback((popUpName: keyof UsePopUpState<T>, data?: unknown) => {
|
||||
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: true, data } }));
|
||||
}, []);
|
||||
|
||||
const handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => {
|
||||
setPopUp((popUp) => ({
|
||||
...popUp,
|
||||
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
|
||||
const handlePopUpClose = useCallback((popUpName: keyof UsePopUpState<T>) => {
|
||||
setPopUp((oldState) => ({ ...oldState, [popUpName]: { isOpen: false } }));
|
||||
}, []);
|
||||
|
||||
const handlePopUpToggle = useCallback((popUpName: keyof UsePopUpState<T>) => {
|
||||
setPopUp((oldState) => ({
|
||||
...oldState,
|
||||
[popUpName]: { isOpen: !oldState[popUpName].isOpen }
|
||||
}));
|
||||
}, []);
|
||||
|
||||
@ -64,6 +61,6 @@ export const usePopUp = <T extends Readonly<string[]> | usePopUpProps[]>(
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
handlePopUpClose,
|
||||
handlePopUpToggle,
|
||||
handlePopUpToggle
|
||||
};
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ export default function Custom404() {
|
||||
height={554}
|
||||
width={942}
|
||||
alt='infisical dragon - page not found'
|
||||
></Image>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,14 +1,15 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { useEffect } from 'react';
|
||||
import { AppProps } from 'next/app';
|
||||
import { useRouter } from 'next/router';
|
||||
import { appWithTranslation } from 'next-i18next';
|
||||
import { config } from '@fortawesome/fontawesome-svg-core';
|
||||
|
||||
import Layout from '~/components/basic/Layout';
|
||||
import NotificationProvider from '~/components/context/Notifications/NotificationProvider';
|
||||
import RouteGuard from '~/components/RouteGuard';
|
||||
import { publicPaths } from '~/const';
|
||||
import Telemetry from '~/utilities/telemetry/Telemetry';
|
||||
import Layout from '@app/components/basic/Layout';
|
||||
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
|
||||
import RouteGuard from '@app/components/RouteGuard';
|
||||
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
|
||||
import { publicPaths } from '@app/const';
|
||||
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
import '../styles/globals.css';
|
||||
@ -19,18 +20,14 @@ type NextAppProp = AppProps & {
|
||||
Component: AppProps['Component'] & { requireAuth: boolean };
|
||||
};
|
||||
|
||||
const App = ({
|
||||
Component,
|
||||
pageProps,
|
||||
...appProps
|
||||
}: NextAppProp): JSX.Element => {
|
||||
const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const storedLang = localStorage.getItem('lang');
|
||||
if (router.locale ?? 'en' !== storedLang ?? 'en') {
|
||||
if (router.locale ?? storedLang !== 'en' ?? 'en') {
|
||||
router.push(router.asPath, router.asPath, {
|
||||
locale: storedLang ?? 'en',
|
||||
locale: storedLang ?? 'en'
|
||||
});
|
||||
}
|
||||
}, [router.locale, router.pathname]);
|
||||
@ -54,7 +51,7 @@ const App = ({
|
||||
|
||||
// If it's one of these routes, don't add the layout (e.g., these routes are external)
|
||||
if (
|
||||
publicPaths.includes('/' + appProps.router.pathname.split('/')[1]) ||
|
||||
publicPaths.includes(`/${appProps.router.pathname.split('/')[1]}`) ||
|
||||
!Component.requireAuth
|
||||
) {
|
||||
return <Component {...pageProps} />;
|
||||
@ -73,8 +70,7 @@ const App = ({
|
||||
|
||||
export default appWithTranslation(App);
|
||||
|
||||
{
|
||||
/* <Script
|
||||
/* <Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
@ -87,4 +83,3 @@ strategy="afterInteractive"
|
||||
gtag('config', 'G-DQ1XLJJGG1');
|
||||
`}
|
||||
</Script> */
|
||||
}
|
||||
|
@ -1,18 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import ActivitySideBar from '@app/ee/components/ActivitySideBar';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import EventFilter from '~/components/basic/EventFilter';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import EventFilter from '@app/components/basic/EventFilter';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
import ActivitySideBar from '@app/ee/components/ActivitySideBar';
|
||||
|
||||
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
|
||||
import ActivityTable from '../../ee/components/ActivityTable';
|
||||
|
||||
|
||||
interface logData {
|
||||
interface LogData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
@ -25,17 +24,17 @@ interface logData {
|
||||
name: string;
|
||||
payload: {
|
||||
secretVersions: string[];
|
||||
}
|
||||
}[]
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logDataPoint {
|
||||
interface LogDataPoint {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
@ -50,11 +49,11 @@ interface logDataPoint {
|
||||
export default function Activity() {
|
||||
const router = useRouter();
|
||||
const [eventChosen, setEventChosen] = useState('');
|
||||
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
|
||||
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 10;
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>()
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
@ -62,25 +61,29 @@ export default function Activity() {
|
||||
setCurrentOffset(0);
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen })
|
||||
setLogsData(tempLogsData.map((log: logData) => {
|
||||
return {
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
const tempLogsData = await getProjectLogs({
|
||||
workspaceId: String(router.query.id),
|
||||
offset: 0,
|
||||
limit: currentLimit,
|
||||
userId: '',
|
||||
actionNames: eventChosen
|
||||
});
|
||||
setLogsData(
|
||||
tempLogsData.map((log: LogData) => ({
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map((action) => ({
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}))
|
||||
}))
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
getLogData();
|
||||
}, [eventChosen]);
|
||||
|
||||
@ -88,58 +91,64 @@ export default function Activity() {
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen })
|
||||
setLogsData(logsData.concat(tempLogsData.map((log: logData) => {
|
||||
return {
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map(action => {
|
||||
return {
|
||||
const tempLogsData = await getProjectLogs({
|
||||
workspaceId: String(router.query.id),
|
||||
offset: currentOffset,
|
||||
limit: currentLimit,
|
||||
userId: '',
|
||||
actionNames: eventChosen
|
||||
});
|
||||
setLogsData(
|
||||
logsData.concat(
|
||||
tempLogsData.map((log: LogData) => ({
|
||||
_id: log._id,
|
||||
channel: log.channel,
|
||||
createdAt: log.createdAt,
|
||||
ipAddress: log.ipAddress,
|
||||
user: log.user.email,
|
||||
payload: log.actions.map((action) => ({
|
||||
_id: action._id,
|
||||
name: action.name,
|
||||
secretVersions: action.payload.secretVersions
|
||||
}
|
||||
})
|
||||
}
|
||||
})))
|
||||
}))
|
||||
}))
|
||||
)
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
getLogData();
|
||||
}, [currentLimit, currentOffset]);
|
||||
|
||||
const loadMoreLogs = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll h-screen">
|
||||
<NavHeader pageName="Project Activity" isProjectRelated={true} />
|
||||
{currentSidebarAction && <ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />}
|
||||
<NavHeader pageName="Project Activity" isProjectRelated />
|
||||
{currentSidebarAction && (
|
||||
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||
)}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<p className="font-semibold mr-4 text-bunker-100">{t("activity:title")}</p>
|
||||
<p className="font-semibold mr-4 text-bunker-100">{t('activity:title')}</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">
|
||||
{t("activity:subtitle")}
|
||||
</p>
|
||||
<p className="mr-4 text-base text-gray-400">{t('activity:subtitle')}</p>
|
||||
</div>
|
||||
<div className="px-6 h-8 mt-2">
|
||||
<EventFilter
|
||||
selected={eventChosen}
|
||||
select={setEventChosen}
|
||||
/>
|
||||
<EventFilter selected={eventChosen} select={setEventChosen} />
|
||||
</div>
|
||||
<ActivityTable
|
||||
data={logsData}
|
||||
toggleSidebar={toggleSidebar}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<div className='flex justify-center w-full mb-6'>
|
||||
<div className='items-center w-60'>
|
||||
<Button text={String(t("common:view-more"))} textDisabled={String(t("common:end-of-history"))} active={logsData.length % 10 == 0 ? true : false} onButtonPressed={loadMoreLogs} size="md" color="mineshaft"/>
|
||||
<ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
|
||||
<div className="flex justify-center w-full mb-6">
|
||||
<div className="items-center w-60">
|
||||
<Button
|
||||
text={String(t('common:view-more'))}
|
||||
textDisabled={String(t('common:end-of-history'))}
|
||||
active={logsData.length % 10 === 0}
|
||||
onButtonPressed={loadMoreLogs}
|
||||
size="md"
|
||||
color="mineshaft"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -148,4 +157,4 @@ export default function Activity() {
|
||||
|
||||
Activity.requireAuth = true;
|
||||
|
||||
export const getServerSideProps = getTranslatedServerSideProps(["activity", "common"]);
|
||||
export const getServerSideProps = getTranslatedServerSideProps(['activity', 'common']);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
@ -12,11 +12,8 @@ interface Props {
|
||||
* @param {string} obj.expiresIn - how soon the API key expires in ms
|
||||
* @returns
|
||||
*/
|
||||
const addAPIKey = ({
|
||||
name,
|
||||
expiresIn,
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v2/api-key/', {
|
||||
const addAPIKey = ({ name, expiresIn }: Props) =>
|
||||
SecurityClient.fetchCall('/api/v2/api-key/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
@ -26,12 +23,11 @@ const addAPIKey = ({
|
||||
expiresIn
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to add API key');
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log('Failed to add API key');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default addAPIKey;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
apiKeyId: string;
|
||||
@ -10,21 +10,18 @@ interface Props {
|
||||
* @param {string} obj.apiKeyId - id of the API key to delete
|
||||
* @returns
|
||||
*/
|
||||
const deleteAPIKey = ({
|
||||
apiKeyId
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v2/api-key/' + apiKeyId, {
|
||||
const deleteAPIKey = ({ apiKeyId }: Props) =>
|
||||
SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to delete API key');
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log('Failed to delete API key');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default deleteAPIKey;
|
||||
|
@ -1,26 +1,22 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This route gets API keys for the user
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const getAPIKeys = () => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v2/api-key',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
const getAPIKeys = () =>
|
||||
SecurityClient.fetchCall('/api/v2/api-key', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).apiKeyData;
|
||||
} else {
|
||||
console.log('Failed to get API keys');
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).apiKeyData;
|
||||
}
|
||||
console.log('Failed to get API keys');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default getAPIKeys;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
encryptedPrivateKey: string;
|
||||
@ -14,34 +14,26 @@ interface Props {
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const changePassword2 = ({
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
clientProof
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v1/password/change-password', {
|
||||
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
|
||||
SecurityClient.fetchCall('/api/v1/password/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientProof: clientProof,
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
iv: iv,
|
||||
tag: tag,
|
||||
salt: salt,
|
||||
verifier: verifier
|
||||
clientProof,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
if (res && res.status === 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to change the password');
|
||||
}
|
||||
console.log('Failed to change the password');
|
||||
return undefined;
|
||||
});
|
||||
};
|
||||
|
||||
export default changePassword2;
|
||||
|
@ -1,16 +1,15 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function is used to check if the user is authenticated.
|
||||
* To do that, we get their tokens from cookies, and verify if they are good.
|
||||
*/
|
||||
const checkAuth = async () => {
|
||||
return SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
|
||||
const checkAuth = async () =>
|
||||
SecurityClient.fetchCall('/api/v1/auth/checkAuth', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((res) => res);
|
||||
};
|
||||
|
||||
export default checkAuth;
|
||||
|
@ -10,17 +10,15 @@ interface Props {
|
||||
* @param {string} obj.code
|
||||
* @returns
|
||||
*/
|
||||
const checkEmailVerificationCode = ({ email, code }: Props) => {
|
||||
return fetch('/api/v1/signup/email/verify', {
|
||||
const checkEmailVerificationCode = ({ email, code }: Props) => fetch('/api/v1/signup/email/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
code: code
|
||||
email,
|
||||
code
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export default checkEmailVerificationCode;
|
||||
|
@ -41,12 +41,11 @@ const completeAccountInformationSignup = ({
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}: Props) => {
|
||||
return fetch('/api/v1/signup/complete-account/signup', {
|
||||
}: Props) => fetch('/api/v1/signup/complete-account/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token
|
||||
Authorization: `Bearer ${ token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
@ -61,6 +60,5 @@ const completeAccountInformationSignup = ({
|
||||
verifier
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export default completeAccountInformationSignup;
|
||||
|
@ -38,25 +38,23 @@ const completeAccountInformationSignupInvite = ({
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}: Props) => {
|
||||
return fetch('/api/v1/signup/complete-account/invite', {
|
||||
}: Props) => fetch('/api/v1/signup/complete-account/invite', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token
|
||||
Authorization: `Bearer ${ token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
publicKey: publicKey,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv: iv,
|
||||
tag: tag,
|
||||
salt: salt,
|
||||
verifier: verifier
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export default completeAccountInformationSignupInvite;
|
||||
|
@ -18,8 +18,8 @@ const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
code: code
|
||||
email,
|
||||
code
|
||||
})
|
||||
});
|
||||
if (response?.status === 200) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
encryptedPrivateKey: string;
|
||||
@ -27,19 +27,19 @@ const issueBackupPrivateKey = ({
|
||||
salt,
|
||||
verifier,
|
||||
clientProof
|
||||
}: Props) => {
|
||||
return SecurityClient.fetchCall('/api/v1/password/backup-private-key', {
|
||||
}: Props) =>
|
||||
SecurityClient.fetchCall('/api/v1/password/backup-private-key', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientProof: clientProof,
|
||||
encryptedPrivateKey: encryptedPrivateKey,
|
||||
iv: iv,
|
||||
tag: tag,
|
||||
salt: salt,
|
||||
verifier: verifier
|
||||
clientProof,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res?.status !== 200) {
|
||||
@ -47,6 +47,5 @@ const issueBackupPrivateKey = ({
|
||||
}
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
export default issueBackupPrivateKey;
|
||||
|
@ -16,7 +16,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
email,
|
||||
clientPublicKey,
|
||||
}),
|
||||
});
|
||||
|
@ -13,24 +13,24 @@ interface Login2Response {
|
||||
* @returns
|
||||
*/
|
||||
const login2 = async (email: string, clientProof: string) => {
|
||||
const response = await fetch("/api/v1/auth/login2", {
|
||||
method: "POST",
|
||||
const response = await fetch('/api/v1/auth/login2', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
clientProof,
|
||||
email,
|
||||
clientProof
|
||||
}),
|
||||
credentials: "include",
|
||||
credentials: 'include'
|
||||
});
|
||||
// need precise error handling about the status code
|
||||
if (response.status == 200) {
|
||||
if (response.status === 200) {
|
||||
const data = (await response.json()) as unknown as Login2Response;
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error("Password verification failed");
|
||||
throw new Error('Password verification failed');
|
||||
};
|
||||
|
||||
export default login2;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user