mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 23:28:31 +00:00
chore(frontend): changed source code to src folder from root
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { publicPaths } from '~/const';
|
||||
import checkAuth from '~/pages/api/auth/CheckAuth';
|
||||
|
||||
// #TODO: finish spinner only when the data loads fully
|
||||
// #TODO: Redirect somewhere if the page does not exist
|
||||
|
||||
type Prop = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function RouteGuard({ children }: Prop): JSX.Element {
|
||||
const router = useRouter();
|
||||
const [authorized, setAuthorized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// on initial load - run auth check
|
||||
(async () => {
|
||||
await authCheck(router.asPath);
|
||||
})();
|
||||
|
||||
// on route change start - hide page content by setting authorized to false
|
||||
// #TODO: add the loading page when not yet authorized.
|
||||
const hideContent = () => setAuthorized(false);
|
||||
// const onError = () => setAuthorized(true)
|
||||
router.events.on('routeChangeStart', hideContent);
|
||||
// router.events.on("routeChangeError", onError);
|
||||
|
||||
// on route change complete - run auth check
|
||||
router.events.on('routeChangeComplete', authCheck);
|
||||
|
||||
// unsubscribe from events in useEffect return function
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', hideContent);
|
||||
router.events.off('routeChangeComplete', authCheck);
|
||||
// router.events.off("routeChangeError", onError);
|
||||
};
|
||||
// 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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable no-undef */
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config';
|
||||
|
||||
export const initPostHog = () => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
// @ts-ignore
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
posthog.init(POSTHOG_API_KEY, {
|
||||
api_host: POSTHOG_HOST
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return posthog;
|
||||
} catch (e) {
|
||||
console.log("posthog err", e)
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
export default function 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import {
|
||||
faAngleDown,
|
||||
faEye,
|
||||
faPlus,
|
||||
faShuffle,
|
||||
faTrash,
|
||||
faX
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Listbox, Transition } from '@headlessui/react';
|
||||
|
||||
interface ListBoxProps {
|
||||
selected: string;
|
||||
select: (event: string) => void;
|
||||
}
|
||||
|
||||
const eventOptions = [
|
||||
{
|
||||
name: 'addSecrets',
|
||||
icon: faPlus
|
||||
},
|
||||
{
|
||||
name: 'readSecrets',
|
||||
icon: faEye
|
||||
},
|
||||
{
|
||||
name: 'updateSecrets',
|
||||
icon: faShuffle
|
||||
},
|
||||
{
|
||||
name: 'deleteSecrets',
|
||||
icon: faTrash
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* This is the component that we use for the event picker in the activity logs tab.
|
||||
* @param {object} obj
|
||||
* @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 { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<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>
|
||||
) : (
|
||||
<p className="select-none">{String(t("common:select-event"))}</p>
|
||||
)}
|
||||
{selected != '' ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className="pl-2 w-2 p-2"
|
||||
onClick={() => select('')}
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pl-4 pr-2" />
|
||||
)}
|
||||
</Listbox.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { memo, useState } from 'react';
|
||||
import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
|
||||
interface InputFieldProps {
|
||||
static?: boolean;
|
||||
label: string;
|
||||
type: string;
|
||||
value: string;
|
||||
placeholder?: string;
|
||||
isRequired: boolean;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
text?: string;
|
||||
name?: string;
|
||||
blurred?: boolean;
|
||||
errorText?: string;
|
||||
onChangeHandler: (value: string) => void;
|
||||
}
|
||||
|
||||
const InputField = (
|
||||
props: InputFieldProps &
|
||||
Pick<JSX.IntrinsicElements['input'], 'autoComplete' | 'id'>
|
||||
) => {
|
||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||
|
||||
if (props.static === 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>
|
||||
)}
|
||||
<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}
|
||||
readOnly
|
||||
autoComplete={props.autoComplete}
|
||||
id={props.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" && (
|
||||
<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"
|
||||
}`}
|
||||
/>
|
||||
<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">
|
||||
The password should contain at least 8
|
||||
characters including at least 1 lowercase
|
||||
character, uppercase character, number, and a
|
||||
special character.
|
||||
</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 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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(InputField);
|
||||
@@ -0,0 +1,440 @@
|
||||
/* 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 {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
faGear,
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus } 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 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 router = useRouter();
|
||||
const [workspaceList, setWorkspaceList] = useState([]);
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState([{ "1": "2" }]);
|
||||
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 { t } = useTranslation();
|
||||
|
||||
function closeModal() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
// TODO: what to do about the fact that 2ids can have the same name
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
setLoading(true);
|
||||
// timeout code.
|
||||
setTimeout(() => setLoading(false), 1500);
|
||||
|
||||
try {
|
||||
const workspaces = await getWorkspaces();
|
||||
const currentWorkspaces = workspaces.map((workspace) => workspace.name);
|
||||
if (!currentWorkspaces.includes(workspaceName)) {
|
||||
const newWorkspace = await createWorkspace({
|
||||
workspaceName,
|
||||
organizationId: tempLocalStorage("orgData.id"),
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
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 };
|
||||
|
||||
await uploadKeys(
|
||||
newWorkspaceId,
|
||||
myUser._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
|
||||
if (addAllUsers) {
|
||||
console.log('adding other users')
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
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 (result?.invitee && result?.latestKey) {
|
||||
const 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,
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: result.invitee.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
uploadKeys(
|
||||
newWorkspaceId,
|
||||
result.invitee._id,
|
||||
ciphertext,
|
||||
nonce
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
router.push("/dashboard/" + newWorkspaceId);
|
||||
setIsOpen(false);
|
||||
setNewWorkspaceName("");
|
||||
} else {
|
||||
console.error("A project with this name already exists.");
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
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: "/integrations/" + workspaceMapping[workspaceSelected as any],
|
||||
title: t("nav:menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />,
|
||||
},
|
||||
{
|
||||
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} />,
|
||||
},
|
||||
],
|
||||
[t, workspaceMapping, workspaceSelected]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage("orgData.id") === "") {
|
||||
const userOrgs = await getOrganizations();
|
||||
localStorage.setItem("orgData.id", userOrgs[0]._id);
|
||||
}
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: tempLocalStorage("orgData.id"),
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
userWorkspaces.length == 0 &&
|
||||
router.asPath != "/noprojects" &&
|
||||
!router.asPath.includes("home")&&
|
||||
!router.asPath.includes("settings")
|
||||
) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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) &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push("/dashboard/" + userWorkspaces[0]._id);
|
||||
} else {
|
||||
setWorkspaceList(
|
||||
userWorkspaces.map((workspace: any) => workspace.name)
|
||||
);
|
||||
setWorkspaceMapping(
|
||||
Object.fromEntries(
|
||||
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]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
putUserInWorkSpace();
|
||||
onboardingCheck({ setTotalOnboardingActionsDone });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (
|
||||
workspaceMapping[workspaceSelected as any] &&
|
||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||
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]
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [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>
|
||||
<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">
|
||||
<nav className="flex flex-col justify-between items-between h-full">
|
||||
{/* <div className="py-6"></div> */}
|
||||
<div>
|
||||
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
||||
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
||||
{t("nav:menu.project")}
|
||||
</div>
|
||||
{workspaceList.length > 0 ? (
|
||||
<Listbox
|
||||
selected={workspaceSelected}
|
||||
onChange={setWorkspaceSelected as any}
|
||||
data={workspaceList}
|
||||
buttonAction={openModal}
|
||||
text=""
|
||||
// workspaceMapping={workspaceMapping as any}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text="Add Project"
|
||||
onButtonPressed={openModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ul>
|
||||
{workspaceList.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("/")[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>
|
||||
<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>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<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>
|
||||
{title}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</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>
|
||||
<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`}
|
||||
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`}
|
||||
>
|
||||
<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`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
></img>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<AddWorkspaceDialog
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
submitModal={submitModal}
|
||||
workspaceName={newWorkspaceName}
|
||||
setWorkspaceName={setNewWorkspaceName}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<main className="flex-1 bg-bunker-800">{children}</main>
|
||||
</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"
|
||||
/>
|
||||
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
||||
{` ${t("common:no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { 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;
|
||||
onChange: (arg: string) => void;
|
||||
data: string[] | null;
|
||||
text?: string;
|
||||
buttonAction?: () => void;
|
||||
isFull?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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,
|
||||
onChange,
|
||||
data,
|
||||
text,
|
||||
buttonAction,
|
||||
isFull,
|
||||
}: ListBoxProps): JSX.Element {
|
||||
return (
|
||||
<Listbox value={selected} 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'>
|
||||
{text}
|
||||
<span className='ml-1 cursor-pointer block truncate font-semibold text-gray-300 capitalize'>
|
||||
{' '}
|
||||
{selected}
|
||||
</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>
|
||||
)}
|
||||
</Listbox.Button>
|
||||
{data && (
|
||||
<Transition
|
||||
as={Fragment}
|
||||
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'>
|
||||
{data.map((person, personIdx) => (
|
||||
<Listbox.Option
|
||||
key={personIdx}
|
||||
className={({ active, selected }) =>
|
||||
`my-0.5 relative cursor-default select-none py-2 pl-10 pr-4 rounded-md capitalize ${
|
||||
selected ? 'bg-white/10 text-gray-400 font-bold' : ''
|
||||
} ${
|
||||
active && !selected
|
||||
? 'bg-white/5 text-mineshaft-200 cursor-pointer'
|
||||
: 'text-gray-400'
|
||||
} `
|
||||
}
|
||||
value={person}
|
||||
>
|
||||
{({ selected }) => (
|
||||
<>
|
||||
<span
|
||||
className={`block truncate text-primary${
|
||||
selected ? 'font-medium' : 'font-normal'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</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' />
|
||||
</span>
|
||||
Add Project
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
)}
|
||||
</div>
|
||||
</Listbox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Switch } from '@headlessui/react';
|
||||
|
||||
interface ToggleProps {
|
||||
enabled: boolean;
|
||||
setEnabled: (value: boolean) => void;
|
||||
addOverride: (value: string | undefined, pos: number) => void;
|
||||
pos: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a typical 'iPhone' toggle (e.g., user for overriding secrets with personal values)
|
||||
* @param obj
|
||||
* @param {boolean} obj.enabled - whether the toggle is turned on or off
|
||||
* @param {function} obj.setEnabled - change the state of the toggle
|
||||
* @param {function} obj.addOverride - a function that adds an override to a certain secret
|
||||
* @param {number} obj.pos - position of a certain secret
|
||||
* @returns
|
||||
*/
|
||||
export default function Toggle({
|
||||
enabled,
|
||||
setEnabled,
|
||||
addOverride,
|
||||
pos,
|
||||
}: ToggleProps): JSX.Element {
|
||||
return (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
if (enabled == false) {
|
||||
addOverride('', pos);
|
||||
} else {
|
||||
addOverride(undefined, pos);
|
||||
}
|
||||
setEnabled(!enabled);
|
||||
}}
|
||||
className={`${
|
||||
enabled ? 'bg-primary' : 'bg-bunker-400'
|
||||
} relative inline-flex h-5 w-9 items-center rounded-full`}
|
||||
>
|
||||
<span className='sr-only'>Enable notifications</span>
|
||||
<span
|
||||
className={`${
|
||||
enabled ? 'translate-x-[1.26rem]' : 'translate-x-0.5'
|
||||
} inline-block h-3.5 w-3.5 transform rounded-full bg-bunker-800 transition`}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
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");
|
||||
|
||||
type ButtonProps = {
|
||||
text?: string;
|
||||
onButtonPressed: () => void;
|
||||
loading?: boolean;
|
||||
color?: string;
|
||||
size: string;
|
||||
icon?: IconProp;
|
||||
active?: boolean;
|
||||
iconDisabled?: IconProp;
|
||||
textDisabled?: string;
|
||||
type?: ButtonHTMLAttributes<any>['type'];
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the main butto component in the app.
|
||||
* @param {object} props
|
||||
* @param {string} props.text - text inside the button
|
||||
* @param {function} props.onButtonPressed - the action that happens when the button is clicked
|
||||
* @param {boolean} props.loading - if a button is currently in the laoding state
|
||||
* @param {string} props.color - button color
|
||||
* @param {string} props.size - button size
|
||||
* @param {FontAwesomeIconProps} props.icon - the icon inside the button
|
||||
* @param {boolean} props.active - if the button is active or disabled
|
||||
* @param {FontAwesomeIconProps} props.text - the icon inside the button when it is disabled
|
||||
* @param {string} props.textDisable - text inside the button when it is disabled
|
||||
* @returns
|
||||
*/
|
||||
export default function Button(props: 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 styleButton = classNames(
|
||||
"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",
|
||||
|
||||
// Changing the opacity when active vs when not
|
||||
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"
|
||||
);
|
||||
|
||||
const styleMainDiv = classNames(
|
||||
"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" : "",
|
||||
|
||||
props.size == "icon" && "flex items-center justify-center"
|
||||
);
|
||||
|
||||
const textStyle = classNames(
|
||||
"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"
|
||||
);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
disabled={!activityStatus}
|
||||
type={props.type}
|
||||
onClick={props.onButtonPressed}
|
||||
className={styleButton}
|
||||
>
|
||||
<div className={styleMainDiv}>
|
||||
<div
|
||||
className={`${
|
||||
props.loading == true ? "opacity-100" : "opacity-0"
|
||||
} absolute flex items-center px-3 bg-primary duration-200 w-full`}
|
||||
>
|
||||
<Image
|
||||
src="/images/loading/loadingblack.gif"
|
||||
height={25}
|
||||
width={42}
|
||||
alt="loading animation"
|
||||
className={`rounded-xl`}
|
||||
></Image>
|
||||
</div>
|
||||
{props.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"}`}
|
||||
/>
|
||||
)}
|
||||
{props.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"}`}
|
||||
/>
|
||||
)}
|
||||
{(props.text || props.textDisabled) && (
|
||||
<p className={textStyle}>
|
||||
{activityStatus ? props.text : props.textDisabled}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
return button;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
selectedIntegrationOption: never[] | null;
|
||||
handleBotActivate: () => Promise<void>;
|
||||
handleIntegrationOption: (arg: { integrationOption: never[] }) => void;
|
||||
};
|
||||
|
||||
const ActivateBotDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. activate bot
|
||||
await handleBotActivate();
|
||||
|
||||
// type check
|
||||
if (!selectedIntegrationOption) return;
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
{t('integrations:why-infisical-needs-access')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-6 max-w-max'>
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color='mineshaft'
|
||||
text={t('integrations:grant-access-button') as string}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivateBotDialog;
|
||||
@@ -0,0 +1,225 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
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 Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
import ListBox from '../Listbox';
|
||||
|
||||
const expiryMapping = {
|
||||
'1 day': 86400,
|
||||
'7 days': 604800,
|
||||
'1 month': 2592000,
|
||||
'6 months': 15552000,
|
||||
'12 months': 31104000,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
// TODO: These never and any will be filled by single folder that contains types and hooks about the API
|
||||
apiKeys: any[];
|
||||
setApiKeys: (arg: any[]) => void;
|
||||
};
|
||||
|
||||
// TODO: convert to TS
|
||||
const AddApiKeyDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
apiKeys,
|
||||
setApiKeys,
|
||||
}: Props) => {
|
||||
const [apiKey, setApiKey] = useState('');
|
||||
const [apiKeyName, setApiKeyName] = useState('');
|
||||
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState('1 day');
|
||||
const [apiKeyCopied, setApiKeyCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const generateAPIKey = async () => {
|
||||
const newApiKey = await addAPIKey({
|
||||
name: apiKeyName,
|
||||
expiresIn: expiryMapping[apiKeyExpiresIn as keyof typeof expiryMapping],
|
||||
});
|
||||
|
||||
setApiKeys([...apiKeys, newApiKey.apiKeyData]);
|
||||
setApiKey(newApiKey.apiKey);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById('apiKey') as HTMLInputElement;
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setApiKeyCopied(true);
|
||||
setTimeout(() => setApiKeyCopied(false), 2000);
|
||||
// Alert the copied text
|
||||
// alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
const closeAddApiKeyModal = () => {
|
||||
closeModal();
|
||||
setApiKeyName('');
|
||||
setApiKey('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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'
|
||||
>
|
||||
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
|
||||
</Transition.Child>
|
||||
|
||||
<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'
|
||||
>
|
||||
{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'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
>
|
||||
{t('section-api-key:add-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-api-key:add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='max-h-28 mb-2'>
|
||||
<InputField
|
||||
label={t('section-api-key:add-dialog.name')}
|
||||
onChangeHandler={setApiKeyName}
|
||||
type='varName'
|
||||
value={apiKeyName}
|
||||
placeholder=''
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<ListBox
|
||||
selected={apiKeyExpiresIn}
|
||||
onChange={setApiKeyExpiresIn}
|
||||
data={[
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
isFull={true}
|
||||
text={`${t('common:expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-max'>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={() => generateAPIKey()}
|
||||
color='mineshaft'
|
||||
text={t('section-api-key:add-dialog.add') as string}
|
||||
textDisabled={
|
||||
t('section-api-key:add-dialog.add') as string
|
||||
}
|
||||
size='md'
|
||||
active={apiKeyName == '' ? false : true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
) : (
|
||||
<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'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
>
|
||||
{t('section-api-key:add-dialog.copy-service-token')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t(
|
||||
'section-api-key:add-dialog.copy-service-token-description'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
|
||||
<input
|
||||
type='text'
|
||||
value={apiKey}
|
||||
disabled={true}
|
||||
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
|
||||
onClick={copyToClipboard}
|
||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||
>
|
||||
{apiKeyCopied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
|
||||
{t('common:click-to-copy')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={() => closeAddApiKeyModal()}
|
||||
color='mineshaft'
|
||||
text='Close'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
)}
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddApiKeyDialog;
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import addIncidentContact from '~/pages/api/organization/addIncidentContact';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
workspaceId: string;
|
||||
incidentContacts: string[];
|
||||
setIncidentContacts: (arg: string[]) => void;
|
||||
};
|
||||
|
||||
const AddIncidentContactDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
incidentContacts,
|
||||
setIncidentContacts,
|
||||
}: Props) => {
|
||||
const [incidentContactEmail, setIncidentContactEmail] = useState('');
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submit = () => {
|
||||
setIncidentContacts(
|
||||
incidentContacts?.length > 0
|
||||
? incidentContacts.concat([incidentContactEmail])
|
||||
: [incidentContactEmail]
|
||||
);
|
||||
addIncidentContact(
|
||||
localStorage.getItem('orgData.id') as string,
|
||||
incidentContactEmail
|
||||
);
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
<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'
|
||||
>
|
||||
<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('section-incident:add-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-2'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-incident:add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<InputField
|
||||
label={t('common:email')}
|
||||
onChangeHandler={setIncidentContactEmail}
|
||||
type='varName'
|
||||
value={incidentContactEmail}
|
||||
placeholder=''
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6 max-w-max'>
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color='mineshaft'
|
||||
text={
|
||||
t('section-incident:add-dialog.add-incident') as string
|
||||
}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddIncidentContactDialog;
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
import ListBox from '../Listbox';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: () => void;
|
||||
data: any;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
};
|
||||
|
||||
const AddProjectMemberDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
submitModal,
|
||||
data,
|
||||
email,
|
||||
setEmail,
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
<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'
|
||||
>
|
||||
<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'
|
||||
>
|
||||
{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'
|
||||
>
|
||||
{t('section-members:add-dialog.already-all-invited')}
|
||||
</Dialog.Title>
|
||||
)}
|
||||
<div className='mt-2 mb-4'>
|
||||
{data?.length > 0 ? (
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-members:add-dialog.user-will-email')}
|
||||
</p>
|
||||
<div className=''>
|
||||
<Trans
|
||||
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
|
||||
)
|
||||
}
|
||||
/>,
|
||||
// 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'
|
||||
onClick={() =>
|
||||
router.push(
|
||||
'/settings/org/' +
|
||||
router.query.id +
|
||||
'?invite'
|
||||
)
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-members:add-dialog.add-user-org-first')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
{data?.length > 0 && (
|
||||
<ListBox
|
||||
selected={email ? email : data[0]}
|
||||
onChange={setEmail}
|
||||
data={data}
|
||||
isFull={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className='max-w-max'>
|
||||
{data?.length > 0 ? (
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={submitModal}
|
||||
color='mineshaft'
|
||||
text={t('section-members:add-member') as string}
|
||||
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'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddProjectMemberDialog;
|
||||
@@ -0,0 +1,287 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { Fragment, useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
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 {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
} from '../../utilities/cryptography/crypto';
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
import ListBox from '../Listbox';
|
||||
|
||||
const expiryMapping = {
|
||||
'1 day': 86400,
|
||||
'7 days': 604800,
|
||||
'1 month': 2592000,
|
||||
'6 months': 15552000,
|
||||
'12 months': 31104000,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
serviceTokens: any[];
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
setServiceTokens: (arg: any[]) => void;
|
||||
};
|
||||
|
||||
const AddServiceTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
serviceTokens,
|
||||
environments,
|
||||
setServiceTokens,
|
||||
}: Props) => {
|
||||
const [serviceToken, setServiceToken] = useState('');
|
||||
const [serviceTokenName, setServiceTokenName] = useState('');
|
||||
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(
|
||||
environments?.[0]
|
||||
);
|
||||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState('1 day');
|
||||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const generateServiceToken = async () => {
|
||||
const latestFileKey = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestFileKey.latestKey.encryptedKey,
|
||||
nonce: latestFileKey.latestKey.nonce,
|
||||
publicKey: latestFileKey.latestKey.sender.publicKey,
|
||||
privateKey: localStorage.getItem('PRIVATE_KEY') as string,
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString('hex');
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: key,
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const newServiceToken = await addServiceToken({
|
||||
name: serviceTokenName,
|
||||
workspaceId,
|
||||
environment: selectedServiceTokenEnv?.slug
|
||||
? selectedServiceTokenEnv.slug
|
||||
: environments[0]?.name,
|
||||
expiresIn:
|
||||
expiryMapping[serviceTokenExpiresIn as keyof typeof expiryMapping],
|
||||
encryptedKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
});
|
||||
|
||||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
|
||||
setServiceToken(newServiceToken.serviceToken + '.' + randomBytes);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById(
|
||||
'serviceToken'
|
||||
) as HTMLInputElement;
|
||||
|
||||
// Select the text field
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
// Copy the text inside the text field
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
setServiceTokenCopied(true);
|
||||
setTimeout(() => setServiceTokenCopied(false), 2000);
|
||||
// Alert the copied text
|
||||
// alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
const closeAddServiceTokenModal = () => {
|
||||
closeModal();
|
||||
setServiceTokenName('');
|
||||
setServiceToken('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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'
|
||||
>
|
||||
<div className='fixed inset-0 bg-bunker-700 bg-opacity-80' />
|
||||
</Transition.Child>
|
||||
|
||||
<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'
|
||||
>
|
||||
{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'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
>
|
||||
{t('section-token:add-dialog.title', {
|
||||
target: workspaceName,
|
||||
})}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t('section-token:add-dialog.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='max-h-28 mb-2'>
|
||||
<InputField
|
||||
label={t('section-token:add-dialog.name')}
|
||||
onChangeHandler={setServiceTokenName}
|
||||
type='varName'
|
||||
value={serviceTokenName}
|
||||
placeholder=''
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28 mb-2'>
|
||||
<ListBox
|
||||
selected={
|
||||
selectedServiceTokenEnv?.name
|
||||
? selectedServiceTokenEnv?.name
|
||||
: environments[0]?.name
|
||||
}
|
||||
data={environments.map(({ name }) => name)}
|
||||
onChange={(envName) =>
|
||||
setSelectedServiceTokenEnv(
|
||||
environments.find(
|
||||
({ name }) => envName === name
|
||||
) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull={true}
|
||||
text={`${t('common:environment')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<ListBox
|
||||
selected={serviceTokenExpiresIn}
|
||||
onChange={setServiceTokenExpiresIn}
|
||||
data={[
|
||||
'1 day',
|
||||
'7 days',
|
||||
'1 month',
|
||||
'6 months',
|
||||
'12 months',
|
||||
]}
|
||||
isFull={true}
|
||||
text={`${t('common:expired-in')}: `}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-w-max'>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={() => generateServiceToken()}
|
||||
color='mineshaft'
|
||||
text={t('section-token:add-dialog.add') as string}
|
||||
textDisabled={
|
||||
t('section-token:add-dialog.add') as string
|
||||
}
|
||||
size='md'
|
||||
active={serviceTokenName == '' ? false : true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
) : (
|
||||
<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'
|
||||
className='text-lg font-medium leading-6 text-gray-400 z-50'
|
||||
>
|
||||
{t('section-token:add-dialog.copy-service-token')}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<div className='flex flex-col'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
{t(
|
||||
'section-token:add-dialog.copy-service-token-description'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full'>
|
||||
<div className='flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20'>
|
||||
<input
|
||||
type='text'
|
||||
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}
|
||||
className='h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200'
|
||||
>
|
||||
{serviceTokenCopied ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='pr-0.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
)}
|
||||
</button>
|
||||
<span className='absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm'>
|
||||
{t('common:click-to-copy')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-6 flex flex-col justify-start w-max'>
|
||||
<Button
|
||||
onButtonPressed={() => closeAddServiceTokenModal()}
|
||||
color='mineshaft'
|
||||
text='Close'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
)}
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddServiceTokenDialog;
|
||||
@@ -0,0 +1,145 @@
|
||||
import { FormEventHandler, Fragment, useEffect, useState } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
|
||||
type FormFields = { name: string; slug: string };
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
isEditMode?: boolean;
|
||||
// on edit mode load up initial values
|
||||
initialValues?: FormFields;
|
||||
onClose: () => void;
|
||||
onCreateSubmit: (data: FormFields) => void;
|
||||
onEditSubmit: (data: FormFields) => void;
|
||||
};
|
||||
|
||||
// TODO: Migrate to better form management and validation. Preferable react-hook-form + yup
|
||||
/**
|
||||
* The dialog modal for when the user wants to create a new workspace
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const AddUpdateEnvironmentDialog = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onCreateSubmit,
|
||||
onEditSubmit,
|
||||
initialValues,
|
||||
isEditMode,
|
||||
}: Props) => {
|
||||
const [formInput, setFormInput] = useState<FormFields>({
|
||||
name: '',
|
||||
slug: '',
|
||||
});
|
||||
|
||||
// This use effect can be removed when the unmount is happening from outside the component
|
||||
// When unmount happens outside state gets unmounted also
|
||||
useEffect(() => {
|
||||
setFormInput(initialValues || { name: '', slug: '' });
|
||||
}, [isOpen]);
|
||||
|
||||
// REFACTOR: Move to react-hook-form with yup for better form management
|
||||
const onInputChange = (fieldName: string, fieldValue: string) => {
|
||||
setFormInput((state) => ({ ...state, [fieldName]: fieldValue }));
|
||||
};
|
||||
|
||||
const onFormSubmit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
name: formInput.name.toLowerCase(),
|
||||
slug: formInput.slug.toLowerCase(),
|
||||
};
|
||||
if (isEditMode) {
|
||||
onEditSubmit(data);
|
||||
return;
|
||||
}
|
||||
onCreateSubmit(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative z-20' onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-out duration-150'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-70' />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 overflow-y-auto z-50'>
|
||||
<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'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl 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'
|
||||
>
|
||||
{isEditMode
|
||||
? 'Update environment'
|
||||
: 'Create a new environment'}
|
||||
</Dialog.Title>
|
||||
<form onSubmit={onFormSubmit}>
|
||||
<div className='max-h-28 mt-4'>
|
||||
<InputField
|
||||
label='Environment Name'
|
||||
onChangeHandler={(val) => onInputChange('name', val)}
|
||||
type='varName'
|
||||
value={formInput.name}
|
||||
placeholder=''
|
||||
isRequired
|
||||
// error={error.length > 0}
|
||||
// errorText={error}
|
||||
/>
|
||||
</div>
|
||||
<div className='max-h-28 mt-4'>
|
||||
<InputField
|
||||
label='Environment Slug'
|
||||
onChangeHandler={(val) => onInputChange('slug', val)}
|
||||
type='varName'
|
||||
value={formInput.slug}
|
||||
placeholder=''
|
||||
isRequired
|
||||
// error={error.length > 0}
|
||||
// errorText={error}
|
||||
/>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 mt-2'>
|
||||
Slugs are shorthands used in cli to access environment
|
||||
</p>
|
||||
<div className='mt-4 max-w-min'>
|
||||
<Button
|
||||
onButtonPressed={() => null}
|
||||
type='submit'
|
||||
color='mineshaft'
|
||||
text={isEditMode ? 'Update' : 'Create'}
|
||||
active={formInput.name !== '' && formInput.slug !== ''}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Fragment } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import { STRIPE_PRODUCT_STARTER } from '../../utilities/config';
|
||||
import Button from '../buttons/Button';
|
||||
import InputField from '../InputField';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (email: string) => void;
|
||||
workspaceId: string;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
currentPlan: string;
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
const AddUserDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
submitModal,
|
||||
email,
|
||||
setEmail,
|
||||
currentPlan,
|
||||
orgName,
|
||||
}: Props) => {
|
||||
const submit = () => {
|
||||
submitModal(email);
|
||||
};
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className='z-50'>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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'
|
||||
>
|
||||
<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'>
|
||||
<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'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-lg 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 z-50'
|
||||
>
|
||||
Invite others to {orgName}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2 mb-4'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
An invite is specific to an email address and expires
|
||||
after 1 day. For security reasons, you will need to
|
||||
separately add members to projects.
|
||||
</p>
|
||||
</div>
|
||||
<div className='max-h-28'>
|
||||
<InputField
|
||||
label='Email'
|
||||
onChangeHandler={setEmail}
|
||||
type='varName'
|
||||
value={email}
|
||||
placeholder=''
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
{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)
|
||||
}
|
||||
>
|
||||
You can add up to 5 members on a Free tier.
|
||||
</button>
|
||||
<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'
|
||||
onClick={() =>
|
||||
router.push('/settings/billing/' + router.query.id)
|
||||
}
|
||||
>
|
||||
Upgrade now.
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className='mt-4 max-w-max'>
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color='mineshaft'
|
||||
text='Invite'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
{/* <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-xl font-medium leading-6 text-gray-300 z-50"
|
||||
>
|
||||
Unleash Infisical's Full Power
|
||||
</Dialog.Title>
|
||||
<div className="mt-4 mb-4">
|
||||
<p className="text-sm text-gray-400 mb-2">
|
||||
You have exceeded the number of members in a free organization.
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
Upgrade now and get access to adding more members, as well as to other powerful enhancements.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-sm font-medium text-black hover:opacity-80 hover:text-semibold duration-200 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)}
|
||||
>
|
||||
Upgrade Now
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-gray-500 hover:text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={closeModal}
|
||||
>
|
||||
Maybe Later
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel> */}
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUserDialog;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
import { Checkbox } from "../table/Checkbox";
|
||||
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (email: string, addAllUser: boolean) => void;
|
||||
workspaceName: string;
|
||||
setWorkspaceName: (workspaceName: string) => void;
|
||||
error: boolean;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The dialog modal for when the user wants to create a new workspace
|
||||
*/
|
||||
const AddWorkspaceDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
submitModal,
|
||||
workspaceName,
|
||||
setWorkspaceName,
|
||||
error,
|
||||
loading,
|
||||
}:Props) => {
|
||||
const [addAllUsers, setAddAllUsers] = useState(true);
|
||||
const submit = () => {
|
||||
submitModal(workspaceName, addAllUsers);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" 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"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto z-50">
|
||||
<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"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl 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"
|
||||
>
|
||||
Create a new project
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
This project will contain your environmental variables.
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-h-28 mt-4">
|
||||
<InputField
|
||||
label="Project Name"
|
||||
onChangeHandler={setWorkspaceName}
|
||||
type="varName"
|
||||
value={workspaceName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={error}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 ml-1">
|
||||
<Checkbox
|
||||
addAllUsers={addAllUsers}
|
||||
setAddAllUsers={setAddAllUsers}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 max-w-min">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
loading={loading}
|
||||
color="mineshaft"
|
||||
text="Create"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWorkspaceDialog;
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Fragment, useEffect, useState } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
import InputField from '../InputField';
|
||||
|
||||
// REFACTOR: Move all these modals into one reusable one
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onClose: ()=>void;
|
||||
title: string;
|
||||
onSubmit:()=>void;
|
||||
deleteKey?:string;
|
||||
}
|
||||
|
||||
const DeleteActionModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
onSubmit,
|
||||
deleteKey
|
||||
}:Props) => {
|
||||
const [deleteInputField, setDeleteInputField] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setDeleteInputField("");
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as='div' className='relative z-10' onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='ease-out duration-300'
|
||||
enterFrom='opacity-0'
|
||||
enterTo='opacity-100'
|
||||
leave='ease-in duration-150'
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
||||
</Transition.Child>
|
||||
<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'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl 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'
|
||||
>
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className='mt-2'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
This action is irrevertible.
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
<InputField
|
||||
isRequired
|
||||
label={`Type ${deleteKey} to delete the resource`}
|
||||
onChangeHandler={(val) => setDeleteInputField(val)}
|
||||
value={deleteInputField}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={onSubmit}
|
||||
disabled={
|
||||
Boolean(deleteKey) && deleteInputField !== deleteKey
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteActionModal;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Fragment } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={() => {}}>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<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"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" onClick={onClose} />
|
||||
</Transition.Child>
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{t('dashboard:sidebar.delete-key-dialog.title')}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-start">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-red-700 hover:bg-red-600 px-4 py-2 text-sm font-medium text-bunker-200 hover:text-white text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-bunker-500 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-mineshaft-500 hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Fragment } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
// #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state.
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (userIdToBeDeleted: string) => void;
|
||||
userIdToBeDeleted: string;
|
||||
};
|
||||
|
||||
const DeleteUserDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
submitModal,
|
||||
userIdToBeDeleted,
|
||||
}: Props) => {
|
||||
const submit = () => {
|
||||
submitModal(userIdToBeDeleted);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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'
|
||||
>
|
||||
<div className='fixed inset-0 bg-black bg-opacity-25' />
|
||||
</Transition.Child>
|
||||
|
||||
<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'
|
||||
>
|
||||
<Dialog.Panel className='w-full max-w-md transform overflow-hidden rounded-2xl 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'
|
||||
>
|
||||
Are you sure you want to remove this user from the
|
||||
workspace?
|
||||
</Dialog.Title>
|
||||
<div className='mt-2'>
|
||||
<p className='text-sm text-gray-500'>
|
||||
This action is irrevertible.
|
||||
</p>
|
||||
</div>
|
||||
<div className='mt-6'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-alizarin hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={submit}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:border-white hover:text-white hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={submit}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteUserDialog;
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import InputField from "../InputField";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
submitModal: (userIdToBeDeleted: string) => void;
|
||||
selectedIntegrationOption: string;
|
||||
handleBotActivate: () => void;
|
||||
handleIntegrationOption: (arg:{integrationOption:string})=>void;
|
||||
};
|
||||
|
||||
const IntegrationAccessTokenDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
selectedIntegrationOption,
|
||||
handleBotActivate,
|
||||
handleIntegrationOption
|
||||
}:Props) => {
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// 1. activate bot
|
||||
await handleBotActivate();
|
||||
|
||||
// 2. start integration
|
||||
await handleIntegrationOption({
|
||||
integrationOption: selectedIntegrationOption
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
closeModal();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<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"
|
||||
>
|
||||
<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">
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
Grant Infisical access to your secrets
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
Most cloud integrations require Infisical to be able to decrypt your secrets so they can be forwarded over.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
{/* <Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text="Grant access"
|
||||
size="md"
|
||||
/> */}
|
||||
<InputField
|
||||
label="Access token"
|
||||
onChangeHandler={() => {}}
|
||||
type="varName"
|
||||
value={"Hello"}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntegrationAccessTokenDialog;
|
||||
@@ -0,0 +1,72 @@
|
||||
import { faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
interface PopupProps {
|
||||
buttonText: string;
|
||||
buttonLink: string;
|
||||
titleText: string;
|
||||
emoji: string;
|
||||
textLine1: string;
|
||||
textLine2: string;
|
||||
setCheckDocsPopUpVisible: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the notification that pops up at the bottom right when a user performs a certain action
|
||||
* @param {object} org
|
||||
* @param {string} org.buttonText - text of the button inside the notification
|
||||
* @param {string} org.buttonLink - where the button leads to
|
||||
* @param {string} org.titleText - the text at the top of a notification
|
||||
* @param {string} org.emoji - the emoji in the notification title
|
||||
* @param {string} org.textLine1 - first line of the text in the notification
|
||||
* @param {string} org.textLine2 - second line of the text in the notification
|
||||
* @param {string} org.setCheckDocsPopUpVisible - the functions that closes the popup
|
||||
* @returns
|
||||
*/
|
||||
export default function BottonRightPopup({
|
||||
buttonText,
|
||||
buttonLink,
|
||||
titleText,
|
||||
emoji,
|
||||
textLine1,
|
||||
textLine2,
|
||||
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'
|
||||
>
|
||||
<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>
|
||||
<button
|
||||
className='mt-1'
|
||||
onClick={() => setCheckDocsPopUpVisible(false)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
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 */}
|
||||
<a
|
||||
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'
|
||||
>
|
||||
{buttonText}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
interface TokenProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface ServiceTokensProps {
|
||||
data: TokenProps[];
|
||||
setApiKeys: (value: TokenProps[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that we utilize for the api key table
|
||||
* @param {object} obj
|
||||
* @param {any[]} obj.data - current state of the api key table
|
||||
* @param {function} obj.setApiKeys - updating the state of the api key table
|
||||
* @returns
|
||||
*/
|
||||
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>
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
||||
No API keys yet
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyTable;
|
||||
@@ -0,0 +1,31 @@
|
||||
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>
|
||||
)}
|
||||
|
||||
<label className='ml-2 text-gray-500 text-sm'>
|
||||
Add all members of my organization to this project.
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { usePopUp } from '../../../hooks/usePopUp';
|
||||
import Button from '../buttons/Button';
|
||||
import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog';
|
||||
import DeleteActionModal from '../dialog/DeleteActionModal';
|
||||
|
||||
type Env = { name: string; slug: string };
|
||||
|
||||
type Props = {
|
||||
data: Env[];
|
||||
onCreateEnv: (arg0: Env) => Promise<void>;
|
||||
onUpdateEnv: (oldSlug: string, arg0: Env) => Promise<void>;
|
||||
onDeleteEnv: (slug: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const EnvironmentTable = ({
|
||||
data = [],
|
||||
onCreateEnv,
|
||||
onDeleteEnv,
|
||||
onUpdateEnv,
|
||||
}: Props) => {
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
'createUpdateEnv',
|
||||
'deleteEnv',
|
||||
] as const);
|
||||
|
||||
const onEnvCreateCB = async (env: Env) => {
|
||||
try {
|
||||
await onCreateEnv(env);
|
||||
handlePopUpClose('createUpdateEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onEnvUpdateCB = async (env: Env) => {
|
||||
try {
|
||||
await onUpdateEnv(
|
||||
(popUp.createUpdateEnv?.data as Pick<Env, 'slug'>)?.slug,
|
||||
env
|
||||
);
|
||||
handlePopUpClose('createUpdateEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const onEnvDeleteCB = async () => {
|
||||
try {
|
||||
await onDeleteEnv(
|
||||
(popUp.deleteEnv?.data as Pick<Env, 'slug'>)?.slug
|
||||
);
|
||||
handlePopUpClose('deleteEnv');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
</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>
|
||||
</div>
|
||||
<div className='w-48'>
|
||||
<Button
|
||||
text='Add New Env'
|
||||
onButtonPressed={() => handlePopUpOpen('createUpdateEnv')}
|
||||
color='mineshaft'
|
||||
icon={faPlus}
|
||||
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'>
|
||||
<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>
|
||||
</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>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
className='text-center pt-7 pb-4 text-bunker-400'
|
||||
>
|
||||
No environmants found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp['deleteEnv'].isOpen}
|
||||
title={`Are you sure want to delete ${
|
||||
(popUp?.deleteEnv?.data as { name: string })?.name || ' '
|
||||
}?`}
|
||||
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ''}
|
||||
onClose={() => handlePopUpClose('deleteEnv')}
|
||||
onSubmit={onEnvDeleteCB}
|
||||
/>
|
||||
<AddUpdateEnvironmentDialog
|
||||
isOpen={popUp.createUpdateEnv.isOpen}
|
||||
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
|
||||
initialValues={popUp?.createUpdateEnv?.data as any}
|
||||
onClose={() => handlePopUpClose('createUpdateEnv')}
|
||||
onCreateSubmit={onEnvCreateCB}
|
||||
onEditSubmit={onEnvUpdateCB}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvironmentTable;
|
||||
@@ -0,0 +1,100 @@
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
interface TokenProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
environment: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface ServiceTokensProps {
|
||||
data: TokenProps[];
|
||||
workspaceName: string;
|
||||
setServiceTokens: (value: TokenProps[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that we utilize for the service token table
|
||||
* #TODO: add the possibility of choosing and doing operations on multiple users.
|
||||
* @param {object} obj
|
||||
* @param {any[]} obj.data - current state of the service token table
|
||||
* @param {string} obj.workspaceName - name of the current project
|
||||
* @param {function} obj.setServiceTokens - updating the state of the service token table
|
||||
* @returns
|
||||
*/
|
||||
const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: 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>
|
||||
<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">TOKEN NAME</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
|
||||
<th></th>
|
||||
</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>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
|
||||
No service tokens yet
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceTokenTable;
|
||||
@@ -0,0 +1,241 @@
|
||||
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;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
|
||||
import StripeRedirect from '~/pages/api/organization/StripeRedirect';
|
||||
|
||||
import { tempLocalStorage } from '../utilities/checks/tempLocalStorage';
|
||||
|
||||
interface Props {
|
||||
plan: {
|
||||
name: string;
|
||||
price: string;
|
||||
priceExplanation?: string;
|
||||
text: string;
|
||||
subtext?: string;
|
||||
buttonTextMain: string;
|
||||
buttonTextSecondary: string;
|
||||
current: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<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>
|
||||
<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 ? (
|
||||
<>
|
||||
{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'
|
||||
? '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
|
||||
onClick={() =>
|
||||
StripeRedirect({
|
||||
orgId: tempLocalStorage('orgData.id'),
|
||||
})
|
||||
}
|
||||
>
|
||||
{plan.buttonTextMain}
|
||||
</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'>
|
||||
{plan.buttonTextSecondary}
|
||||
</div>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<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'
|
||||
}`}
|
||||
>
|
||||
<p className='text-xs text-black font-semibold'>CURRENT PLAN</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
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';
|
||||
|
||||
interface NotificationProps {
|
||||
notification: Required<NotificationType>;
|
||||
clearNotification: (text: string) => void;
|
||||
}
|
||||
|
||||
const Notification = ({
|
||||
notification,
|
||||
clearNotification
|
||||
}: NotificationProps) => {
|
||||
const timeout = useRef<number>();
|
||||
|
||||
const handleClearNotification = () => clearNotification(notification.text);
|
||||
|
||||
const setNotifTimeout = () => {
|
||||
timeout.current = window.setTimeout(
|
||||
handleClearNotification,
|
||||
notification.timeoutMs
|
||||
);
|
||||
};
|
||||
|
||||
const cancelNotifTimeout = () => {
|
||||
clearTimeout(timeout.current);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNotifTimeout();
|
||||
|
||||
return cancelNotifTimeout;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative w-full flex items-center justify-between px-4 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-bunker-500"
|
||||
role="alert"
|
||||
>
|
||||
{notification.type === 'error' && (
|
||||
<div className="absolute w-full h-1 bg-red top-0 left-0 rounded-t-md"></div>
|
||||
)}
|
||||
{notification.type === 'success' && (
|
||||
<div className="absolute w-full h-1 bg-green top-0 left-0 rounded-t-md"></div>
|
||||
)}
|
||||
{notification.type === 'info' && (
|
||||
<div className="absolute w-full h-1 bg-yellow top-0 left-0 rounded-t-md"></div>
|
||||
)}
|
||||
<p className="text-bunker-200 text-sm font-semibold mt-0.5">
|
||||
{notification.text}
|
||||
</p>
|
||||
<button
|
||||
className="rounded-lg"
|
||||
onClick={() => clearNotification(notification.text)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
className="text-white pl-2 w-4 h-3 hover:text-red"
|
||||
icon={faX}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
|
||||
import Notifications from './Notifications';
|
||||
|
||||
type NotificationType = 'success' | 'error' | 'info';
|
||||
|
||||
export type Notification = {
|
||||
text: string;
|
||||
type?: NotificationType;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
type NotificationContextState = {
|
||||
createNotification: (newNotification: Notification) => void;
|
||||
};
|
||||
|
||||
const NotificationContext = createContext<NotificationContextState>({
|
||||
createNotification: () => console.log('createNotification not set!')
|
||||
});
|
||||
|
||||
export const useNotificationContext = () => useContext(NotificationContext);
|
||||
|
||||
interface NotificationProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const NotificationProvider = ({ children }: NotificationProviderProps) => {
|
||||
const [notifications, setNotifications] = useState<Required<Notification>[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
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]);
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider
|
||||
value={{
|
||||
createNotification
|
||||
}}
|
||||
>
|
||||
<Notifications
|
||||
notifications={notifications}
|
||||
clearNotification={clearNotification}
|
||||
/>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationProvider;
|
||||
@@ -0,0 +1,30 @@
|
||||
import Notification from './Notification';
|
||||
import { Notification as NotificationType } from './NotificationProvider';
|
||||
|
||||
interface NoticationsProps {
|
||||
notifications: Required<NotificationType>[];
|
||||
clearNotification: (text: string) => void;
|
||||
}
|
||||
|
||||
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">
|
||||
{notifications.map((notif) => (
|
||||
<Notification
|
||||
key={notif.text}
|
||||
notification={notif}
|
||||
clearNotification={clearNotification}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notifications;
|
||||
@@ -0,0 +1,20 @@
|
||||
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 { 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>
|
||||
}
|
||||
|
||||
export default CommentField;
|
||||
@@ -0,0 +1,166 @@
|
||||
import { memo, SyntheticEvent, useRef } from 'react';
|
||||
import { faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import guidGenerator from '../utilities/randomId';
|
||||
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
interface DashboardInputFieldProps {
|
||||
position: number;
|
||||
onChangeHandler: (value: string, position: number) => void;
|
||||
value: string | undefined;
|
||||
type: 'varName' | 'value';
|
||||
blurred?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
override?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component renders the input fields on the dashboard
|
||||
* @param {object} obj - the order number of a keyPair
|
||||
* @param {number} obj.position - the order number of a keyPair
|
||||
* @param {function} obj.onChangeHandler - what happens when the input is modified
|
||||
* @param {string} obj.type - whether the input field is for a Key Name or for a Key Value
|
||||
* @param {string} obj.value - value of the InputField
|
||||
* @param {boolean} obj.blurred - whether the input field should be blurred (behind the gray dots) or not; this can be turned on/off in the dashboard
|
||||
* @param {boolean} obj.isDuplicate - if the key name is duplicated
|
||||
* @param {boolean} obj.override - whether a secret/row should be displalyed as overriden
|
||||
* @returns
|
||||
*/
|
||||
|
||||
const DashboardInputField = ({
|
||||
position,
|
||||
onChangeHandler,
|
||||
type,
|
||||
value,
|
||||
blurred,
|
||||
isDuplicate,
|
||||
override
|
||||
}: DashboardInputFieldProps) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const syncScroll = (e: SyntheticEvent<HTMLDivElement>) => {
|
||||
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 error = startsWithNumber || isDuplicate;
|
||||
|
||||
return (
|
||||
<div className="flex-col w-full">
|
||||
<div
|
||||
className={`group relative flex flex-col justify-center w-full border ${
|
||||
error ? 'border-red' : 'border-mineshaft-500'
|
||||
} rounded-md`}
|
||||
>
|
||||
<input
|
||||
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 ${
|
||||
error ? 'focus:ring-red/50' : 'focus:ring-primary/50'
|
||||
} duration-200`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
</div>
|
||||
{startsWithNumber && (
|
||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||
Should not start with a number
|
||||
</p>
|
||||
)}
|
||||
{isDuplicate && !startsWithNumber && (
|
||||
<p className="text-red text-xs mt-0.5 mx-1 mb-2 max-w-xs">
|
||||
Secret names should be unique
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else 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>}
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => onChangeHandler(e.target.value, position)}
|
||||
onScroll={syncScroll}
|
||||
className={`${
|
||||
blurred
|
||||
? 'text-transparent group-hover:text-transparent focus:text-transparent active:text-transparent'
|
||||
: ''
|
||||
} z-10 peer font-mono ph-no-capture bg-transparent rounded-md caret-white text-transparent text-md px-2 py-1.5 w-full min-w-16 outline-none focus:ring-2 focus:ring-primary/50 duration-200 no-scrollbar no-scrollbar::-webkit-scrollbar`}
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
ref={ref}
|
||||
className={`${
|
||||
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'
|
||||
}
|
||||
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}>
|
||||
{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) == '}' ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="ph-no-capture text-yellow-400">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span key={id} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
{blurred && (
|
||||
<div className="absolute flex flex-row items-center z-20 peer pr-2 bg-bunker-800 group-hover:hidden peer-hover:hidden peer-focus:hidden peer-active:invisible h-9 w-full rounded-md text-gray-400/50 text-clip">
|
||||
<div className="px-2 flex flex-row items-center overflow-x-scroll no-scrollbar no-scrollbar::-webkit-scrollbar">
|
||||
{value?.split('').map(() => (
|
||||
<FontAwesomeIcon
|
||||
key={guidGenerator()}
|
||||
className="text-xxs mx-0.5"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>Something Wrong</>;
|
||||
};
|
||||
|
||||
function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInputFieldProps) {
|
||||
return prev.value === next.value && prev.type === next.type && prev.position === next.position && prev.blurred === next.blurred && prev.override === next.override && prev.isDuplicate === next.isDuplicate;
|
||||
}
|
||||
|
||||
export default memo(DashboardInputField, inputPropsAreEqual);
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import { DeleteEnvVar } from '../basic/dialog/DeleteEnvVar';
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void
|
||||
}
|
||||
|
||||
export function DeleteActionButton({ onSubmit }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="bg-[#9B3535] opacity-70 hover:opacity-100 w-[4.5rem] h-[2.5rem] rounded-md duration-200 ml-2">
|
||||
<Button
|
||||
text={String(t("Delete"))}
|
||||
// onButtonPressed={onSubmit}
|
||||
color="red"
|
||||
size="md"
|
||||
onButtonPressed={() => setOpen(true)}
|
||||
/>
|
||||
<DeleteEnvVar
|
||||
isOpen={open}
|
||||
onClose={() => {
|
||||
setOpen(false)
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import downloadDotEnv from '../utilities/secrets/downloadDotEnv';
|
||||
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 {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>
|
||||
}
|
||||
|
||||
export default DownloadSecretMenu;
|
||||
@@ -0,0 +1,237 @@
|
||||
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 { parseDotEnv } from '../utilities/parseDotEnv';
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
interface DropZoneProps {
|
||||
// TODO: change Data type from any
|
||||
setData: (data: any) => void;
|
||||
setErrorDragAndDrop: (hasError: boolean) => void;
|
||||
createNewFile: () => void;
|
||||
errorDragAndDrop: boolean;
|
||||
setButtonReady: (isReady: boolean) => void;
|
||||
keysExist: boolean;
|
||||
numCurrentRows: number;
|
||||
}
|
||||
|
||||
const DropZone = ({
|
||||
setData,
|
||||
setErrorDragAndDrop,
|
||||
createNewFile,
|
||||
errorDragAndDrop,
|
||||
setButtonReady,
|
||||
keysExist,
|
||||
numCurrentRows,
|
||||
}: DropZoneProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// set dropEffect to copy i.e copy of the source item
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
};
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const getSecrets = (file: ArrayBuffer, fileType: string) => {
|
||||
let secrets;
|
||||
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',
|
||||
};
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'yml': {
|
||||
const parsedFile = parseDocument(file.toString());
|
||||
const keyPairs = parsedFile.contents!.toJSON();
|
||||
|
||||
secrets = Object.keys(keyPairs).map((key, index) => {
|
||||
const fileContent = parsedFile.contents as YAMLMap<Scalar, Scalar>;
|
||||
const comment =
|
||||
fileContent!.items
|
||||
.find((item) => item.key.value === key)
|
||||
?.key?.commentBefore?.split('\n')
|
||||
.map((comment) => comment.trim())
|
||||
.join('\n') ?? '';
|
||||
return {
|
||||
id: guidGenerator(),
|
||||
pos: numCurrentRows + index,
|
||||
key: key,
|
||||
value: keyPairs[key as keyof typeof keyPairs]?.toString() ?? '',
|
||||
comment,
|
||||
type: 'shared',
|
||||
};
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
secrets = '';
|
||||
break;
|
||||
}
|
||||
return secrets;
|
||||
};
|
||||
|
||||
// This function function immediately parses the file after it is dropped
|
||||
const handleDrop = async (e: DragEvent) => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 5000);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = "copy";
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
const reader = new FileReader();
|
||||
const fileType = file.name.split('.')[1];
|
||||
|
||||
reader.onload = (event) => {
|
||||
if (event.target === null || event.target.result === null) return;
|
||||
// parse function's argument looks like to be ArrayBuffer
|
||||
const newData = getSecrets(event.target.result as ArrayBuffer, fileType);
|
||||
setData(newData);
|
||||
setButtonReady(true);
|
||||
};
|
||||
|
||||
// If something is wrong show an error
|
||||
try {
|
||||
reader.readAsText(file);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
setErrorDragAndDrop(true);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// This function is used when the user manually selects a file from the in-browser dircetory (not drag and drop)
|
||||
const handleFileSelect = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setLoading(true);
|
||||
setTimeout(() => setLoading(false), 5000);
|
||||
if (e.currentTarget.files === null) return;
|
||||
const file = e.currentTarget.files[0];
|
||||
const fileType = file.name.split('.')[1];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (event.target === null || event.target.result === null) return;
|
||||
const { result } = event.target;
|
||||
const newData = getSecrets(result as ArrayBuffer, fileType);
|
||||
setData(newData);
|
||||
setButtonReady(true);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
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>
|
||||
</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"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="opacity-0 absolute w-full h-full"
|
||||
accept=".txt,.env,.yml"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
{errorDragAndDrop ? (
|
||||
<div className="my-3 max-w-xl opacity-80"></div>
|
||||
) : (
|
||||
<div className=""></div>
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
{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"
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpload} className="text-7xl mb-8" />
|
||||
<p className="">{t("common:drop-zone")}</p>
|
||||
<input
|
||||
id="fileSelect"
|
||||
type="file"
|
||||
className="opacity-0 absolute w-full h-full"
|
||||
accept=".txt,.env,.yml"
|
||||
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>
|
||||
<p className="text-gray-400 text-xs mx-4">OR</p>
|
||||
<div className="border-t border-gray-700 w-1/5"></div>
|
||||
</div>
|
||||
<div className="z-10 mb-6">
|
||||
<Button
|
||||
color="mineshaft"
|
||||
text={String(t("dashboard:add-secret"))}
|
||||
onButtonPressed={createNewFile}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
{errorDragAndDrop ? (
|
||||
<div className="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 className="py-3">
|
||||
{/* <p className="text-xs text-gray-500"> If you are expecting to see a file here, contact your administrator for permission. </p> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropZone;
|
||||
@@ -0,0 +1,95 @@
|
||||
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 [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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row absolute bottom-[0.4rem] right-[3.3rem] w-16 bg-bunker-800 border border-chicago-700 rounded-md text-bunker-200 ">
|
||||
<div
|
||||
className="m-0.5 px-1 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength > 1) {
|
||||
setRandomStringLength(randomStringLength - 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<input
|
||||
onChange={(e) =>
|
||||
setRandomStringLength(parseInt(e.target.value))
|
||||
}
|
||||
value={randomStringLength}
|
||||
className="text-center z-20 peer text-sm bg-transparent w-full outline-none"
|
||||
spellCheck="false"
|
||||
/>
|
||||
<div
|
||||
className="m-0.5 px-1 pb-0.5 cursor-pointer rounded-md hover:bg-chicago-700"
|
||||
onClick={() => {
|
||||
if (randomStringLength < 32) {
|
||||
setRandomStringLength(randomStringLength + 1);
|
||||
}
|
||||
}}
|
||||
>
|
||||
+
|
||||
</div>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
}
|
||||
|
||||
export default GenerateSecretMenu;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { faEllipsis } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { SecretDataProps } from 'public/data/frequentInterfaces';
|
||||
|
||||
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;
|
||||
isBlurred: boolean;
|
||||
isDuplicate: boolean;
|
||||
toggleSidebar: (id: string) => void;
|
||||
sidebarSecretId: string;
|
||||
isSnapshot: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component represent a single row for an environemnt variable on the dashboard
|
||||
* @param {object} obj
|
||||
* @param {String[]} obj.keyPair - data related to the environment variable (id, pos, key, value, public/private)
|
||||
* @param {function} obj.modifyKey - modify the key of a certain environment variable
|
||||
* @param {function} obj.modifyValue - modify the value of a certain environment variable
|
||||
* @param {function} obj.modifyValueOverride - modify the value of a certain environment variable if it is overriden
|
||||
* @param {boolean} obj.isBlurred - if the blurring setting is turned on
|
||||
* @param {boolean} obj.isDuplicate - list of all the duplicates secret names on the dashboard
|
||||
* @param {function} obj.toggleSidebar - open/close/switch sidebar
|
||||
* @param {string} obj.sidebarSecretId - the id of a secret for the side bar is displayed
|
||||
* @param {boolean} obj.isSnapshot - whether this keyPair is in a snapshot. If so, it won't have some features like sidebar
|
||||
* @returns
|
||||
*/
|
||||
const KeyPair = ({
|
||||
keyPair,
|
||||
modifyKey,
|
||||
modifyValue,
|
||||
modifyValueOverride,
|
||||
isBlurred,
|
||||
isDuplicate,
|
||||
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>
|
||||
</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>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KeyPair;
|
||||
@@ -0,0 +1,168 @@
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import SecretVersionList from '@app/ee/components/SecretVersionList';
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import Toggle from '../basic/Toggle';
|
||||
import CommentField from './CommentField';
|
||||
import DashboardInputField from './DashboardInputField';
|
||||
import { DeleteActionButton } from './DeleteActionButton';
|
||||
import GenerateSecretMenu from './GenerateSecretMenu';
|
||||
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
valueOverride: string | undefined;
|
||||
pos: number;
|
||||
id: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface OverrideProps {
|
||||
id: string;
|
||||
valueOverride: string;
|
||||
}
|
||||
export interface DeleteRowFunctionProps {
|
||||
ids: string[];
|
||||
secretName: string;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
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;
|
||||
buttonReady: boolean;
|
||||
savePush: () => void;
|
||||
sharedToHide: string[];
|
||||
setSharedToHide: (values: string[]) => void;
|
||||
deleteRow: (props: DeleteRowFunctionProps) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
|
||||
* @param {function} obj.modifyKey - function that modifies the secret key
|
||||
* @param {function} obj.modifyValue - function that modifies the secret value
|
||||
* @param {function} obj.modifyValueOverride - function that modifies the secret value if it is an override
|
||||
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
|
||||
* @param {function} obj.savePush - save changes andp ush secrets
|
||||
* @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,
|
||||
modifyComment,
|
||||
buttonReady,
|
||||
savePush,
|
||||
deleteRow
|
||||
}: SideBarProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
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>
|
||||
) : (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<SecretVersionList secretId={data[0]?.id} />
|
||||
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} position={data[0]?.pos} />
|
||||
</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;
|
||||
@@ -0,0 +1,117 @@
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import deleteIntegrationAuth from '../../pages/api/integrations/DeleteIntegrationAuth';
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOption: CloudIntegrationOption;
|
||||
setSelectedIntegrationOption: (
|
||||
cloudIntegration: CloudIntegrationOption
|
||||
) => void;
|
||||
integrationOptionPress: (
|
||||
cloudIntegrationOption: CloudIntegrationOption
|
||||
) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
}
|
||||
|
||||
const CloudIntegration = ({
|
||||
cloudIntegrationOption,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths,
|
||||
}: Props) => {
|
||||
const router = useRouter();
|
||||
return integrationAuths ? (
|
||||
<div
|
||||
className={`relative ${
|
||||
cloudIntegrationOption.isAvailable
|
||||
? 'hover:bg-white/10 duration-200 cursor-pointer'
|
||||
: 'opacity-50'
|
||||
} flex flex-row bg-white/5 h-32 rounded-md p-4 items-center`}
|
||||
onClick={() => {
|
||||
if (!cloudIntegrationOption.isAvailable) return;
|
||||
setSelectedIntegrationOption(cloudIntegrationOption);
|
||||
integrationOptionPress(cloudIntegrationOption);
|
||||
}}
|
||||
key={cloudIntegrationOption.name}
|
||||
>
|
||||
<Image
|
||||
src={`/images/integrations/${cloudIntegrationOption.name}.png`}
|
||||
height={70}
|
||||
width={70}
|
||||
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>{cloudIntegrationOption.name.split(' ')[0]}</div>
|
||||
<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'>
|
||||
{cloudIntegrationOption.name}
|
||||
</div>
|
||||
)}
|
||||
{cloudIntegrationOption.isAvailable &&
|
||||
integrationAuths
|
||||
.map((authorization) => authorization.integration)
|
||||
.includes(cloudIntegrationOption.name.toLowerCase()) && (
|
||||
<div className='absolute group z-40 top-0 right-0 flex flex-row'>
|
||||
<div
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
deleteIntegrationAuth({
|
||||
integrationAuthId: integrationAuths
|
||||
.filter(
|
||||
(authorization) =>
|
||||
authorization.integration ==
|
||||
cloudIntegrationOption.name.toLowerCase()
|
||||
)
|
||||
.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'
|
||||
>
|
||||
<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' />
|
||||
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'>
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudIntegration;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import CloudIntegration from './CloudIntegration';
|
||||
|
||||
interface CloudIntegrationOption {
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
type: string;
|
||||
clientId: string;
|
||||
docsLink: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOptions: CloudIntegrationOption[];
|
||||
setSelectedIntegrationOption: () => void;
|
||||
integrationOptionPress: () => void;
|
||||
integrationAuths: any;
|
||||
}
|
||||
|
||||
const CloudIntegrationSection = ({
|
||||
cloudIntegrationOptions,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
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')}
|
||||
</h1>
|
||||
<p className='text-base text-gray-400'>
|
||||
{t('integrations:click-to-start')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-4 grid-cols-4 grid-rows-2 mx-6 max-w-5xl'>
|
||||
{cloudIntegrationOptions.map((cloudIntegrationOption) => (
|
||||
<CloudIntegration
|
||||
cloudIntegrationOption={cloudIntegrationOption}
|
||||
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
integrationAuths={integrationAuths}
|
||||
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudIntegrationSection;
|
||||
@@ -0,0 +1,39 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
interface Framework {
|
||||
name: string;
|
||||
slug: string;
|
||||
image: string;
|
||||
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`}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrameworkIntegration;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import FrameworkIntegration from './FrameworkIntegration';
|
||||
|
||||
interface Framework {
|
||||
name: string;
|
||||
image: string;
|
||||
link: string;
|
||||
slug: string;
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
frameworks: [Framework];
|
||||
}
|
||||
|
||||
const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex flex-col justify-between items-start mx-4 mt-12 mb-4 text-xl max-w-5xl px-2'>
|
||||
<h1 className='font-semibold text-3xl'>
|
||||
{t('integrations:framework-integrations')}
|
||||
</h1>
|
||||
<p className='text-base text-gray-400'>
|
||||
{t('integrations:click-to-setup')}
|
||||
</p>
|
||||
</div>
|
||||
<div className='grid gap-4 grid-cols-7 grid-rows-2 mx-6 mt-4 max-w-5xl'>
|
||||
{frameworks.map((framework) => (
|
||||
<FrameworkIntegration
|
||||
framework={framework}
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrameworkIntegrationSection;
|
||||
@@ -0,0 +1,254 @@
|
||||
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 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 IntegrationApp {
|
||||
name: string;
|
||||
siteId: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
integration: Integration;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
/>
|
||||
<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;
|
||||
@@ -0,0 +1,45 @@
|
||||
import guidGenerator from '~/utilities/randomId';
|
||||
|
||||
import Integration from './Integration';
|
||||
|
||||
interface Props {
|
||||
integrations: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
}
|
||||
|
||||
interface IntegrationType {
|
||||
_id: string;
|
||||
app?: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
integrationAuth: string;
|
||||
isActive: boolean;
|
||||
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'>
|
||||
Manage your integrations of Infisical with third-party services.
|
||||
</p>
|
||||
</div>
|
||||
{integrations.map((integration: IntegrationType) => (
|
||||
<Integration
|
||||
key={guidGenerator()}
|
||||
integration={integration}
|
||||
environments={environments}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div></div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectIntegrationSection;
|
||||
@@ -0,0 +1,323 @@
|
||||
/* 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 {
|
||||
faAngleDown,
|
||||
faBook,
|
||||
faCoins,
|
||||
faEnvelope,
|
||||
faGear,
|
||||
faPlus,
|
||||
faRightFromBracket,
|
||||
faUpRightFromSquare,
|
||||
} 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 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",
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
||||
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",
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
||||
t("nav:support.email"),
|
||||
"mailto:[email protected]",
|
||||
],
|
||||
];
|
||||
|
||||
export interface ICurrentOrg {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the navigation bar in the main app.
|
||||
* It has two main components: support options and user menu (inlcudes billing, logout, org/user settings)
|
||||
* @returns NavBar
|
||||
*/
|
||||
export default function Navbar() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<IUser | undefined>();
|
||||
const [orgs, setOrgs] = useState([]);
|
||||
const [currentOrg, setCurrentOrg] = useState<ICurrentOrg | undefined>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const userData = await getUser();
|
||||
setUser(userData);
|
||||
const orgsData = await getOrganizations();
|
||||
setOrgs(orgsData);
|
||||
const currentOrg = await getOrganization({
|
||||
orgId: String(localStorage.getItem("orgData.id")),
|
||||
});
|
||||
setCurrentOrg(currentOrg);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const closeApp = async () => {
|
||||
console.log("Logging out...");
|
||||
await logout();
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-50">
|
||||
<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"
|
||||
/>
|
||||
</div>
|
||||
<a 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
|
||||
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">
|
||||
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
|
||||
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">
|
||||
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
|
||||
</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 right-0 mt-0.5 w-64 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-2 py-1.5">
|
||||
{supportOptionsList.map(([icon, text, url]) => (
|
||||
<a
|
||||
key={guidGenerator()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5"
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</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">
|
||||
{user?.firstName} {user?.lastName}
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleDown}
|
||||
className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100"
|
||||
/>
|
||||
</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 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")}
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="text-lg text-gray-400 p-2 mr-1 rounded-md cursor-pointer hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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")}
|
||||
</div>
|
||||
<div
|
||||
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>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
// onClick={buttonAction}
|
||||
className="cursor-pointer w-full"
|
||||
>
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
// onClick={buttonAction}
|
||||
className="cursor-pointer w-full mb-2"
|
||||
>
|
||||
<div
|
||||
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>
|
||||
</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")}
|
||||
</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")
|
||||
)
|
||||
.map((org: { _id: string; name: string }) => (
|
||||
<div
|
||||
key={guidGenerator()}
|
||||
onClick={() => {
|
||||
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"
|
||||
>
|
||||
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={closeApp}
|
||||
className={`${
|
||||
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">
|
||||
<FontAwesomeIcon
|
||||
className="text-lg ml-1.5 mr-3"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
{t("common:logout")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
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';
|
||||
|
||||
/**
|
||||
* This is the component at the top of almost every page.
|
||||
* It shows how to navigate to a certain page.
|
||||
* It future these links should also be clickable and hoverable
|
||||
* @param obj
|
||||
* @param obj.pageName - Name of the page
|
||||
* @param obj.isProjectRelated - whether this page is related to project or now (determine if it's 2 or 3 navigation steps)
|
||||
* @returns
|
||||
*/
|
||||
export default function NavHeader({
|
||||
pageName,
|
||||
isProjectRelated,
|
||||
}: {
|
||||
pageName: string;
|
||||
isProjectRelated?: boolean;
|
||||
}): JSX.Element {
|
||||
const [orgName, setOrgName] = useState('');
|
||||
const [workspaceName, setWorkspaceName] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const orgId = localStorage.getItem('orgData.id');
|
||||
const org = await getOrganization({
|
||||
orgId: orgId ? orgId : '',
|
||||
});
|
||||
setOrgName(org.name);
|
||||
|
||||
const workspace = await getProjectInfo({
|
||||
projectId: String(router.query.id),
|
||||
});
|
||||
setWorkspaceName(workspace.name);
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
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'>
|
||||
{orgName?.charAt(0)}
|
||||
</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='text-gray-400 text-sm'>{pageName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
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";
|
||||
|
||||
|
||||
// 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"
|
||||
},
|
||||
} 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"
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface CodeInputStepProps {
|
||||
email: string;
|
||||
incrementStep: () => void;
|
||||
setCode: (value: string) => void;
|
||||
codeError: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the second step of sign up where users need to verify their email
|
||||
* @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
|
||||
*/
|
||||
export default function CodeInputStep({ email, incrementStep, setCode, codeError }: CodeInputStepProps): JSX.Element {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResendingVerificationEmail, setIsResendingVerificationEmail] =
|
||||
useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const resendVerificationEmail = async () => {
|
||||
setIsResendingVerificationEmail(true);
|
||||
setIsLoading(true);
|
||||
sendVerificationEmail(email);
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsResendingVerificationEmail(false);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
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>
|
||||
<div className="hidden md:block">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setCode}
|
||||
{...props}
|
||||
className="mt-6 mb-2"
|
||||
/>
|
||||
</div>
|
||||
<div className="block md:hidden">
|
||||
<ReactCodeInput
|
||||
name=""
|
||||
inputMode="tel"
|
||||
type="text"
|
||||
fields={6}
|
||||
onChange={setCode}
|
||||
{...propsPhone}
|
||||
className="mt-2 mb-6"
|
||||
/>
|
||||
</div>
|
||||
{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"
|
||||
/>
|
||||
</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")}
|
||||
</button>
|
||||
</u>
|
||||
</div>
|
||||
<p className="text-sm text-bunker-400 pb-2">
|
||||
{t("signup:step2-spam-alert")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faWarning } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import issueBackupKey from '../utilities/cryptography/issueBackupKey';
|
||||
|
||||
interface DownloadBackupPDFStepProps {
|
||||
incrementStep: () => void;
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the step of the signup flow where the user downloads the backup pdf
|
||||
* @param {object} obj
|
||||
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
|
||||
* @param {string} obj.email - user's email
|
||||
* @param {string} obj.password - user's password
|
||||
* @param {string} obj.name - user's name
|
||||
* @returns
|
||||
*/
|
||||
export default function DonwloadBackupPDFStep({
|
||||
incrementStep,
|
||||
email,
|
||||
password,
|
||||
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'>
|
||||
{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>{t('signup:step4-description1')}</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' />
|
||||
{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'>
|
||||
<Button
|
||||
text='Download PDF'
|
||||
onButtonPressed={async () => {
|
||||
await issueBackupKey({
|
||||
email,
|
||||
password,
|
||||
personalName: name,
|
||||
setBackupKeyError: (value: boolean) => {},
|
||||
setBackupKeyIssued: (value: boolean) => {},
|
||||
});
|
||||
incrementStep();
|
||||
}}
|
||||
size='lg'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
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";
|
||||
|
||||
|
||||
interface DownloadBackupPDFStepProps {
|
||||
incrementStep: () => void;
|
||||
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 {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
|
||||
*/
|
||||
export default function EnterEmailStep({ email, setEmail, incrementStep }: DownloadBackupPDFStepProps): JSX.Element {
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const [emailErrorMessage, setEmailErrorMessage] = useState("");
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Verifies if the entered email "looks" correct
|
||||
*/
|
||||
const emailCheck = () => {
|
||||
let emailCheckBool = false;
|
||||
if (!email) {
|
||||
setEmailError(true);
|
||||
setEmailErrorMessage("Please enter your email.");
|
||||
emailCheckBool = true;
|
||||
} else if (
|
||||
!email.includes("@") ||
|
||||
!email.includes(".") ||
|
||||
!/[a-z]/.test(email)
|
||||
) {
|
||||
setEmailError(true);
|
||||
setEmailErrorMessage("Please enter a valid email.");
|
||||
emailCheckBool = true;
|
||||
} else {
|
||||
setEmailError(false);
|
||||
}
|
||||
|
||||
// If everything is correct, go to the next step
|
||||
if (!emailCheckBool) {
|
||||
sendVerificationEmail(email);
|
||||
incrementStep();
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<p className="text-4xl font-semibold flex justify-center text-primary">
|
||||
{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") ?? ""}
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
error={emailError}
|
||||
errorText={emailErrorMessage}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</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>
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center justify-center w-full md:pb-2 max-w-md mx-auto pt-2 mb-48 md:mb-16 mt-2">
|
||||
<Link href="/login">
|
||||
<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")}
|
||||
</u>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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";
|
||||
|
||||
|
||||
/**
|
||||
* 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 { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
// Redirect user to the getting started page
|
||||
const redirectToHome = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push("/home/" + userWorkspace);
|
||||
|
||||
}
|
||||
|
||||
const inviteUsers = async ({ emails }: { emails: string; }) => {
|
||||
emails
|
||||
.split(',')
|
||||
.map(email => email.trim())
|
||||
.map(async (email) => await 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")}
|
||||
</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")}
|
||||
</p>
|
||||
<div>
|
||||
<div className="overflow-auto bg-bunker-800">
|
||||
<div className="whitespace-pre-wrap break-words bg-transparent">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<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)}
|
||||
placeholder="[email protected], [email protected]..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row max-w-max min-w-28 items-center justify-center md:p-2 max-h-24 mx-auto text-lg px-4 mt-4 mb-2">
|
||||
<div
|
||||
className="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")}
|
||||
</div>
|
||||
<Button
|
||||
text={t("signup:step5-send-invites") ?? ""}
|
||||
onButtonPressed={() => inviteUsers({ emails})}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import React, { useState } from 'react';
|
||||
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 completeAccountInformationSignup from '~/pages/api/auth/CompleteAccountInformationSignup';
|
||||
|
||||
import Button from '../basic/buttons/Button';
|
||||
import InputField from '../basic/InputField';
|
||||
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');
|
||||
const client = new jsrp.client();
|
||||
|
||||
interface UserInfoStepProps {
|
||||
verificationToken: string;
|
||||
incrementStep: () => void;
|
||||
email: string;
|
||||
password: string;
|
||||
setPassword: (value: string) => void;
|
||||
firstName: string;
|
||||
setFirstName: (value: string) => void;
|
||||
lastName: string;
|
||||
setLastName: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the step of the sign up flow where people provife their name/surname and password
|
||||
* @param {object} obj
|
||||
* @param {string} obj.verificationToken - the token which we use to verify the legitness of a user
|
||||
* @param {string} obj.incrementStep - a function to move to the next signup step
|
||||
* @param {string} obj.email - email of a user who is signing up
|
||||
* @param {string} obj.password - user's password
|
||||
* @param {string} obj.setPassword - function managing the state of user's password
|
||||
* @param {string} obj.firstName - user's first name
|
||||
* @param {string} obj.setFirstName - function managing the state of user's first name
|
||||
* @param {string} obj.lastName - user's lastName
|
||||
* @param {string} obj.setLastName - function managing the state of user's last name
|
||||
*/
|
||||
export default function UserInfoStep({
|
||||
verificationToken,
|
||||
incrementStep,
|
||||
email,
|
||||
password,
|
||||
setPassword,
|
||||
firstName,
|
||||
setFirstName,
|
||||
lastName,
|
||||
setLastName,
|
||||
}: UserInfoStepProps): JSX.Element {
|
||||
const [firstNameError, setFirstNameError] = useState(false);
|
||||
const [lastNameError, setLastNameError] = useState(false);
|
||||
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
|
||||
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
|
||||
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
// Verifies if the information that the users entered (name, workspace)
|
||||
// is there, and if the password matches the criteria.
|
||||
const signupErrorCheck = async () => {
|
||||
setIsLoading(true);
|
||||
let errorCheck = false;
|
||||
if (!firstName) {
|
||||
setFirstNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setFirstNameError(false);
|
||||
}
|
||||
if (!lastName) {
|
||||
setLastNameError(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setLastNameError(false);
|
||||
}
|
||||
errorCheck = passwordCheck({
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck,
|
||||
});
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
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 { 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'
|
||||
),
|
||||
}) as { ciphertext: string; iv: string; tag: string };
|
||||
|
||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
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,
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
try {
|
||||
await attemptLogin(
|
||||
email,
|
||||
password,
|
||||
(value: boolean) => {},
|
||||
router,
|
||||
true,
|
||||
false
|
||||
);
|
||||
incrementStep();
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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'>
|
||||
{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'>
|
||||
<InputField
|
||||
label={t('common:first-name')}
|
||||
onChangeHandler={setFirstName}
|
||||
type='name'
|
||||
value={firstName}
|
||||
isRequired
|
||||
errorText={
|
||||
t('common:validate-required', {
|
||||
name: t('common:first-name'),
|
||||
}) as string
|
||||
}
|
||||
error={firstNameError}
|
||||
autoComplete='given-name'
|
||||
/>
|
||||
</div>
|
||||
<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'
|
||||
value={lastName}
|
||||
isRequired
|
||||
errorText={
|
||||
t('common:validate-required', {
|
||||
name: t('common:last-name'),
|
||||
}) as string
|
||||
}
|
||||
error={lastNameError}
|
||||
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'>
|
||||
<InputField
|
||||
label={t('section-password:password')}
|
||||
onChangeHandler={(password: string) => {
|
||||
setPassword(password);
|
||||
passwordCheck({
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck: false,
|
||||
});
|
||||
}}
|
||||
type='password'
|
||||
value={password}
|
||||
isRequired
|
||||
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 ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className='text-md text-red mr-2.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='text-md text-primary mr-2'
|
||||
/>
|
||||
)}
|
||||
<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'>
|
||||
{passwordErrorLowerCase ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className='text-md text-red mr-2.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='text-md text-primary mr-2'
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
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'>
|
||||
{passwordErrorNumber ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faX}
|
||||
className='text-md text-red mr-2.5'
|
||||
/>
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className='text-md text-primary mr-2'
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
<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'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import token from '~/pages/api/auth/Token';
|
||||
|
||||
export default class SecurityClient {
|
||||
static #token = '';
|
||||
|
||||
constructor() {}
|
||||
|
||||
static setToken(token: string) {
|
||||
this.#token = token;
|
||||
}
|
||||
|
||||
static async fetchCall(
|
||||
resource: RequestInfo,
|
||||
options?: RequestInit | undefined
|
||||
) {
|
||||
const req = new Request(resource, options);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.#token) {
|
||||
req.headers.set('Authorization', 'Bearer ' + this.#token);
|
||||
}
|
||||
|
||||
return fetch(req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
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 { encryptAssymmetric } from './cryptography/crypto';
|
||||
import encryptSecrets from './secrets/encryptSecrets';
|
||||
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');
|
||||
const client = new jsrp.client();
|
||||
|
||||
/**
|
||||
* This function logs in the user (whether it's right after signup, or a normal login)
|
||||
* @param {string} email - email of the user logging in
|
||||
* @param {string} password - password of the user logging in
|
||||
* @param {function} setErrorLogin - function that visually dispay an error is something is wrong
|
||||
* @param {*} router
|
||||
* @param {boolean} isSignUp - whether this log in is a part of signup
|
||||
* @param {boolean} isLogin - ?
|
||||
* @returns
|
||||
*/
|
||||
const attemptLogin = async (
|
||||
email: string,
|
||||
password: string,
|
||||
setErrorLogin: (value: boolean) => void,
|
||||
router: any,
|
||||
isSignUp: boolean,
|
||||
isLogin: boolean
|
||||
) => {
|
||||
try {
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
password: password
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
|
||||
try {
|
||||
const { serverPublicKey, salt } = await login1(email, clientPublicKey);
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = client.getProof(); // called M1
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
const { token, publicKey, encryptedPrivateKey, iv, tag } =
|
||||
await login2(email, clientProof);
|
||||
|
||||
SecurityClient.setToken(token);
|
||||
|
||||
const privateKey = Aes256Gcm.decrypt({
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 + (password.slice(0, 32).length - new Blob([password]).size),
|
||||
'0'
|
||||
)
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgsData = userOrgs.map((org: { _id: string; }) => org._id);
|
||||
|
||||
let orgToLogin;
|
||||
if (userOrgsData.includes(localStorage.getItem('orgData.id'))) {
|
||||
orgToLogin = localStorage.getItem('orgData.id');
|
||||
} else {
|
||||
orgToLogin = userOrgsData[0];
|
||||
localStorage.setItem('orgData.id', orgToLogin);
|
||||
}
|
||||
|
||||
let orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: orgToLogin
|
||||
});
|
||||
|
||||
orgUserProjects = orgUserProjects?.map((project: { _id: string; }) => project._id);
|
||||
let projectToLogin;
|
||||
if (
|
||||
orgUserProjects.includes(localStorage.getItem('projectData.id'))
|
||||
) {
|
||||
projectToLogin = localStorage.getItem('projectData.id');
|
||||
} else {
|
||||
try {
|
||||
projectToLogin = orgUserProjects[0];
|
||||
localStorage.setItem('projectData.id', projectToLogin);
|
||||
} catch (error) {
|
||||
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 myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
}) as { ciphertext: string; nonce: string };
|
||||
|
||||
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')) });
|
||||
}
|
||||
|
||||
if (isLogin) {
|
||||
router.push('/dashboard/' + localStorage.getItem('projectData.id'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
setErrorLogin(true);
|
||||
console.log('Login response not available');
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during authentication');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default attemptLogin;
|
||||
@@ -0,0 +1,71 @@
|
||||
import getOrganizationUsers from '~/pages/api/organization/GetOrgUsers';
|
||||
import checkUserAction from '~/pages/api/userActions/checkUserAction';
|
||||
|
||||
interface OnboardingCheckProps {
|
||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||
setHasUserClickedSlack?: (value: boolean) => void;
|
||||
setHasUserClickedIntro?: (value: boolean) => void;
|
||||
setHasUserStarred?: (value: boolean) => void;
|
||||
setHasUserPushedSecrets?: (value: boolean) => void;
|
||||
setUsersInOrg?: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks which onboarding steps a user has already finished.
|
||||
*/
|
||||
const onboardingCheck = async ({
|
||||
setTotalOnboardingActionsDone,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserClickedIntro,
|
||||
setHasUserStarred,
|
||||
setHasUserPushedSecrets,
|
||||
setUsersInOrg
|
||||
}: OnboardingCheckProps) => {
|
||||
let countActions = 0;
|
||||
const userActionSlack = await checkUserAction({
|
||||
action: 'slack_cta_clicked'
|
||||
});
|
||||
if (userActionSlack) {
|
||||
countActions = countActions + 1;
|
||||
}
|
||||
setHasUserClickedSlack &&
|
||||
setHasUserClickedSlack(userActionSlack ? true : false);
|
||||
|
||||
const userActionSecrets = await checkUserAction({
|
||||
action: 'first_time_secrets_pushed'
|
||||
});
|
||||
if (userActionSecrets) {
|
||||
countActions = countActions + 1;
|
||||
}
|
||||
setHasUserPushedSecrets &&
|
||||
setHasUserPushedSecrets(userActionSecrets ? true : false);
|
||||
|
||||
const userActionIntro = await checkUserAction({
|
||||
action: 'intro_cta_clicked'
|
||||
});
|
||||
if (userActionIntro) {
|
||||
countActions = countActions + 1;
|
||||
}
|
||||
setHasUserClickedIntro &&
|
||||
setHasUserClickedIntro(userActionIntro ? true : false);
|
||||
|
||||
const userActionStar = await checkUserAction({
|
||||
action: 'star_cta_clicked'
|
||||
});
|
||||
if (userActionStar) {
|
||||
countActions = countActions + 1;
|
||||
}
|
||||
setHasUserStarred && setHasUserStarred(userActionStar ? true : false);
|
||||
|
||||
const orgId = localStorage.getItem('orgData.id');
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: orgId ? orgId : ''
|
||||
});
|
||||
if (orgUsers.length > 1) {
|
||||
countActions = countActions + 1;
|
||||
}
|
||||
setUsersInOrg && setUsersInOrg(orgUsers.length > 1);
|
||||
setTotalOnboardingActionsDone && setTotalOnboardingActionsDone(countActions);
|
||||
};
|
||||
|
||||
export default onboardingCheck;
|
||||
@@ -0,0 +1,67 @@
|
||||
interface PasswordCheckProps {
|
||||
password: string;
|
||||
errorCheck: boolean;
|
||||
setPasswordErrorLength: (value: boolean) => void;
|
||||
setPasswordErrorNumber: (value: boolean) => void;
|
||||
setPasswordErrorLowerCase: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks a user password with respect to some criteria.
|
||||
*/
|
||||
const passwordCheck = ({
|
||||
password,
|
||||
setPasswordErrorLength,
|
||||
setPasswordErrorNumber,
|
||||
setPasswordErrorLowerCase,
|
||||
errorCheck,
|
||||
}: PasswordCheckProps) => {
|
||||
if (!password || password.length < 14) {
|
||||
setPasswordErrorLength(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setPasswordErrorLength(false);
|
||||
}
|
||||
|
||||
if (!/\d/.test(password)) {
|
||||
setPasswordErrorNumber(true);
|
||||
errorCheck = true;
|
||||
} else {
|
||||
setPasswordErrorNumber(false);
|
||||
}
|
||||
|
||||
if (!/[a-z]/.test(password)) {
|
||||
setPasswordErrorLowerCase(true);
|
||||
errorCheck = true;
|
||||
// } else if (/(.)(?:(?!\1).){1,2}/.test(password)) {
|
||||
// console.log(111)
|
||||
// setPasswordError(true);
|
||||
// setPasswordErrorMessage("Password should not contain repeating characters.");
|
||||
// errorCheck = true;
|
||||
// } else if (RegExp(`[${email}]`).test(password)) {
|
||||
// console.log(222)
|
||||
// setPasswordError(true);
|
||||
// setPasswordErrorMessage("Password should not contain your email.");
|
||||
// errorCheck = true;
|
||||
} else {
|
||||
setPasswordErrorLowerCase(false);
|
||||
}
|
||||
|
||||
// if (!/[A-Z]/.test(password)) {
|
||||
// setPasswordErrorUpperCase(true);
|
||||
// errorCheck = true;
|
||||
// } else {
|
||||
// setPasswordErrorUpperCase(false);
|
||||
// }
|
||||
|
||||
// if (!/(?=.*[!@#$%^&*])/.test(password)) {
|
||||
// setPasswordErrorSpecialChar(true);
|
||||
// // "Please add at least 1 special character (*, !, #, %)."
|
||||
// errorCheck = true;
|
||||
// } else {
|
||||
// setPasswordErrorSpecialChar(false);
|
||||
// }
|
||||
return errorCheck;
|
||||
};
|
||||
|
||||
export default passwordCheck;
|
||||
@@ -0,0 +1,11 @@
|
||||
// this is temporary util function. create error handling logic for localStorage and delete this.
|
||||
export const tempLocalStorage = (key: string) => {
|
||||
const value = localStorage.getItem(key);
|
||||
|
||||
if (value === null || value === "") {
|
||||
console.warn("No value found in localStorage for key");
|
||||
return "";
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
|
||||
const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
|
||||
const POSTHOG_HOST =
|
||||
process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
|
||||
const STRIPE_PRODUCT_PRO = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_PRO!;
|
||||
const STRIPE_PRODUCT_STARTER = process.env.NEXT_PUBLIC_STRIPE_PRODUCT_STARTER!;
|
||||
|
||||
export {
|
||||
ENV,
|
||||
POSTHOG_API_KEY,
|
||||
POSTHOG_HOST,
|
||||
STRIPE_PRODUCT_PRO,
|
||||
STRIPE_PRODUCT_STARTER
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @fileoverview Provides easy encryption/decryption methods using AES 256 GCM.
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const BLOCK_SIZE_BYTES = 16; // 128 bit
|
||||
|
||||
interface EncryptProps {
|
||||
text: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface DecryptProps {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
secret: string;
|
||||
}
|
||||
|
||||
interface EncryptOutputProps {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides easy encryption/decryption methods using AES 256 GCM.
|
||||
*/
|
||||
class Aes256Gcm {
|
||||
/**
|
||||
* No need to run the constructor. The class only has static methods.
|
||||
*/
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Encrypts text with AES 256 GCM.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.text - Cleartext to encode.
|
||||
* @param {string} obj.secret - Shared secret key, must be 32 bytes.
|
||||
* @returns {object}
|
||||
*/
|
||||
// { ciphertext: string; iv: string; tag: string; }
|
||||
static encrypt({ text, secret }: EncryptProps): EncryptOutputProps {
|
||||
const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
|
||||
|
||||
let ciphertext = cipher.update(text, 'utf8', 'base64');
|
||||
ciphertext += cipher.final('base64');
|
||||
return {
|
||||
ciphertext,
|
||||
iv: iv.toString('base64'),
|
||||
tag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts AES 256 CGM encrypted text.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.ciphertext - Base64-encoded ciphertext.
|
||||
* @param {string} obj.iv - The base64-encoded initialization vector.
|
||||
* @param {string} obj.tag - The base64-encoded authentication tag generated by getAuthTag().
|
||||
* @param {string} obj.secret - Shared secret key, must be 32 bytes.
|
||||
* @returns {string}
|
||||
*/
|
||||
static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
ALGORITHM,
|
||||
secret,
|
||||
Buffer.from(iv, 'base64')
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
||||
|
||||
let cleartext = decipher.update(ciphertext, 'base64', 'utf8');
|
||||
cleartext += decipher.final('utf8');
|
||||
|
||||
return cleartext;
|
||||
}
|
||||
}
|
||||
|
||||
export default Aes256Gcm;
|
||||
@@ -0,0 +1,115 @@
|
||||
import jsrp from 'jsrp';
|
||||
|
||||
import changePassword2 from '~/pages/api/auth/ChangePassword2';
|
||||
import SRP1 from '~/pages/api/auth/SRP1';
|
||||
|
||||
import Aes256Gcm from './aes-256-gcm';
|
||||
|
||||
const clientOldPassword = new jsrp.client();
|
||||
const clientNewPassword = new jsrp.client();
|
||||
|
||||
/**
|
||||
* This function loggs in the user (whether it's right after signup, or a normal login)
|
||||
* @param {*} email
|
||||
* @param {*} password
|
||||
* @param {*} setErrorLogin
|
||||
* @param {*} router
|
||||
* @param {*} isSignUp
|
||||
* @returns
|
||||
*/
|
||||
const changePassword = async (
|
||||
email: string,
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
setCurrentPasswordError: (arg: boolean) => void,
|
||||
setPasswordChanged: (arg: boolean) => void,
|
||||
setCurrentPassword: (arg: string) => void,
|
||||
setNewPassword: (arg: string) => void
|
||||
) => {
|
||||
try {
|
||||
setPasswordChanged(false);
|
||||
setCurrentPasswordError(false);
|
||||
|
||||
clientOldPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: currentPassword,
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||
|
||||
let serverPublicKey, salt;
|
||||
try {
|
||||
const res = await SRP1({
|
||||
clientPublicKey: clientPublicKey,
|
||||
});
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
} catch (err) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log('Wrong current password', err, 1);
|
||||
}
|
||||
|
||||
clientOldPassword.setSalt(salt);
|
||||
clientOldPassword.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = clientOldPassword.getProof(); // called M1
|
||||
|
||||
clientNewPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: newPassword,
|
||||
},
|
||||
async () => {
|
||||
clientNewPassword.createVerifier(async (err, result) => {
|
||||
// The Blob part here is needed to account for symbols that count as 2+ bytes (e.g., é, å, ø)
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: localStorage.getItem('PRIVATE_KEY') as string,
|
||||
secret: newPassword
|
||||
.slice(0, 32)
|
||||
.padStart(
|
||||
32 +
|
||||
(newPassword.slice(0, 32).length -
|
||||
new Blob([newPassword]).size),
|
||||
'0'
|
||||
),
|
||||
});
|
||||
|
||||
if (ciphertext) {
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await changePassword2({
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
clientProof,
|
||||
});
|
||||
if (res && res.status == 400) {
|
||||
setCurrentPasswordError(true);
|
||||
} else if (res && res.status == 200) {
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
}
|
||||
} catch (err) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during changing the password');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default changePassword;
|
||||
@@ -0,0 +1,151 @@
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
import aes from './aes-256-gcm';
|
||||
|
||||
type encryptAsymmetricProps = {
|
||||
plaintext: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||
* [publicKey] likely belongs to the recipient.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
* @param {String} obj.publicKey - public key of the recipient
|
||||
* @param {String} obj.privateKey - private key of the sender (current user)
|
||||
* @returns {Object} obj
|
||||
* @returns {String} ciphertext - base64-encoded ciphertext
|
||||
* @returns {String} nonce - base64-encoded nonce
|
||||
*/
|
||||
const encryptAssymmetric = ({
|
||||
plaintext,
|
||||
publicKey,
|
||||
privateKey,
|
||||
}: encryptAsymmetricProps): {
|
||||
ciphertext: string;
|
||||
nonce: string;
|
||||
} => {
|
||||
const nonce = nacl.randomBytes(24);
|
||||
const ciphertext = nacl.box(
|
||||
nacl.util.decodeUTF8(plaintext),
|
||||
nonce,
|
||||
nacl.util.decodeBase64(publicKey),
|
||||
nacl.util.decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
return {
|
||||
ciphertext: nacl.util.encodeBase64(ciphertext),
|
||||
nonce: nacl.util.encodeBase64(nonce),
|
||||
};
|
||||
};
|
||||
|
||||
type decryptAsymmetricProps = {
|
||||
ciphertext: string;
|
||||
nonce: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return assymmetrically decrypted [ciphertext] using [privateKey] where
|
||||
* [privateKey] likely belongs to the recipient.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.nonce - nonce
|
||||
* @param {String} obj.publicKey - base64-encoded public key of the sender
|
||||
* @param {String} obj.privateKey - base64-encoded private key of the receiver (current user)
|
||||
*/
|
||||
const decryptAssymmetric = ({
|
||||
ciphertext,
|
||||
nonce,
|
||||
publicKey,
|
||||
privateKey,
|
||||
}: decryptAsymmetricProps): string => {
|
||||
const plaintext = nacl.box.open(
|
||||
nacl.util.decodeBase64(ciphertext),
|
||||
nacl.util.decodeBase64(nonce),
|
||||
nacl.util.decodeBase64(publicKey),
|
||||
nacl.util.decodeBase64(privateKey)
|
||||
);
|
||||
|
||||
return nacl.util.encodeUTF8(plaintext);
|
||||
};
|
||||
|
||||
type encryptSymmetricProps = {
|
||||
plaintext: string;
|
||||
key: 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;
|
||||
try {
|
||||
const obj = aes.encrypt({ text: plaintext, secret: key });
|
||||
ciphertext = obj.ciphertext;
|
||||
iv = obj.iv;
|
||||
tag = obj.tag;
|
||||
} catch (err) {
|
||||
console.log('Failed to perform encryption');
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
};
|
||||
};
|
||||
|
||||
type decryptSymmetricProps = {
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
key: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return symmetrically decypted [ciphertext] using [iv], [tag],
|
||||
* and [key].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
* @param {String} obj.key - 32-byte hex key
|
||||
*
|
||||
*/
|
||||
const decryptSymmetric = ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key,
|
||||
}: decryptSymmetricProps): string => {
|
||||
let plaintext;
|
||||
try {
|
||||
plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
|
||||
} catch (err) {
|
||||
console.log('Failed to perform decryption');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
};
|
||||
|
||||
export {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric,
|
||||
encryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import issueBackupPrivateKey from '~/pages/api/auth/IssueBackupPrivateKey';
|
||||
import SRP1 from '~/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;
|
||||
password: string;
|
||||
personalName: string;
|
||||
setBackupKeyError: (value: boolean) => void;
|
||||
setBackupKeyIssued: (value: boolean) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function issue a backup key for a user
|
||||
* @param {obkect} obj
|
||||
* @param {string} obj.email - email of a user issuing a backup key
|
||||
* @param {string} obj.password - password of a user issuing a backup key
|
||||
* @param {string} obj.personalName - name of a user issuing a backup key
|
||||
* @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key
|
||||
* @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly
|
||||
* @returns
|
||||
*/
|
||||
const issueBackupKey = async ({
|
||||
email,
|
||||
password,
|
||||
personalName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
}: BackupKeyProps) => {
|
||||
try {
|
||||
setBackupKeyError(false);
|
||||
setBackupKeyIssued(false);
|
||||
clientPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: password
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = clientPassword.getPublicKey();
|
||||
|
||||
let serverPublicKey, salt;
|
||||
try {
|
||||
const res = await SRP1({
|
||||
clientPublicKey: clientPublicKey
|
||||
});
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
} catch (err) {
|
||||
setBackupKeyError(true);
|
||||
console.log('Wrong current password', err, 1);
|
||||
}
|
||||
|
||||
clientPassword.setSalt(salt);
|
||||
clientPassword.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = clientPassword.getProof(); // called M1
|
||||
|
||||
const generatedKey = crypto.randomBytes(16).toString('hex');
|
||||
|
||||
clientKey.init(
|
||||
{
|
||||
username: email,
|
||||
password: generatedKey
|
||||
},
|
||||
async () => {
|
||||
clientKey.createVerifier(
|
||||
async (err: any, result: { salt: string; verifier: string }) => {
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: String(localStorage.getItem('PRIVATE_KEY')),
|
||||
secret: generatedKey
|
||||
});
|
||||
|
||||
const res = await issueBackupPrivateKey({
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
clientProof
|
||||
});
|
||||
|
||||
if (res?.status == 400) {
|
||||
setBackupKeyError(true);
|
||||
} else if (res?.status == 200) {
|
||||
generateBackupPDF({
|
||||
personalName,
|
||||
personalEmail: email,
|
||||
generatedKey
|
||||
});
|
||||
setBackupKeyIssued(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
setBackupKeyError(true);
|
||||
console.log('Failed to issue a backup key');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default issueBackupKey;
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,66 @@
|
||||
const LINE =
|
||||
/(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
|
||||
|
||||
/**
|
||||
* Return text that is the buffer parsed
|
||||
* @param {ArrayBuffer} src - source buffer
|
||||
* @returns {String} text - text of buffer
|
||||
*/
|
||||
export function parseDotEnv(src: ArrayBuffer) {
|
||||
const object: {
|
||||
[key: string]: { value: string; comments: string[] };
|
||||
} = {};
|
||||
|
||||
// Convert buffer to string
|
||||
let lines = src.toString();
|
||||
|
||||
// Convert line breaks to same format
|
||||
lines = lines.replace(/\r\n?/gm, '\n');
|
||||
|
||||
let comments: string[] = [];
|
||||
|
||||
lines
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
// collect comments of each env variable
|
||||
if (line.startsWith('#')) {
|
||||
comments.push(line.replace('#', '').trim());
|
||||
} else if (line) {
|
||||
let match;
|
||||
let item: [string, string, string[]] | [] = [];
|
||||
|
||||
while ((match = LINE.exec(line)) !== null) {
|
||||
const key = match[1];
|
||||
|
||||
// Default undefined or null to empty string
|
||||
let value = match[2] || '';
|
||||
|
||||
// Remove whitespace
|
||||
value = value.trim();
|
||||
|
||||
// Check if double quoted
|
||||
const maybeQuote = value[0];
|
||||
|
||||
// Remove surrounding quotes
|
||||
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, '$2');
|
||||
|
||||
// Expand newlines if double quoted
|
||||
if (maybeQuote === '"') {
|
||||
value = value.replace(/\\n/g, '\n');
|
||||
value = value.replace(/\\r/g, '\r');
|
||||
}
|
||||
item = [key, value, comments];
|
||||
}
|
||||
comments = [];
|
||||
return item;
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.filter((line) => line.length > 1)
|
||||
.forEach((line) => {
|
||||
const [key, value, comments] = line;
|
||||
object[key as string] = { value, comments };
|
||||
});
|
||||
|
||||
return object;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* This function returns a random identifier.
|
||||
* @returns
|
||||
*/
|
||||
const guidGenerator = () => {
|
||||
const S4 = function () {
|
||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
};
|
||||
return (
|
||||
S4() +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
'-' +
|
||||
S4() +
|
||||
S4() +
|
||||
S4()
|
||||
);
|
||||
};
|
||||
|
||||
export default guidGenerator;
|
||||
@@ -0,0 +1,29 @@
|
||||
interface Props {
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
privateKey: string;
|
||||
}
|
||||
|
||||
export const saveTokenToLocalStorage = ({
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
privateKey,
|
||||
}: Props) => {
|
||||
try {
|
||||
localStorage.setItem("publicKey", publicKey);
|
||||
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
|
||||
localStorage.setItem("iv", iv);
|
||||
localStorage.setItem("tag", tag);
|
||||
localStorage.setItem("PRIVATE_KEY", privateKey);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
throw new Error(
|
||||
"Unable to send the tokens in local storage:" + err.message
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .env file
|
||||
* @param {object} obj
|
||||
* @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
|
||||
* @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'
|
||||
);
|
||||
if (overridenSecrets.length) {
|
||||
overridenSecrets.forEach((secret) => {
|
||||
const index = secrets!.findIndex(
|
||||
(_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));
|
||||
}
|
||||
return secrets;
|
||||
}
|
||||
|
||||
export default checkOverrides;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { envMapping } from "public/data/frequentConstants";
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
|
||||
import checkOverrides from './checkOverrides';
|
||||
|
||||
|
||||
/**
|
||||
* This function downloads the secrets as a .env file
|
||||
* @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; }) => {
|
||||
if (!data) return;
|
||||
const secrets = await checkOverrides({ data });
|
||||
|
||||
const file = secrets!
|
||||
.map(
|
||||
(item: SecretDataProps) =>
|
||||
`${
|
||||
item.comment
|
||||
? item.comment
|
||||
.split('\n')
|
||||
.map((comment) => '# '.concat(comment))
|
||||
.join('\n') + '\n'
|
||||
: ''
|
||||
}` + [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();
|
||||
}
|
||||
|
||||
export default downloadDotEnv;
|
||||
@@ -0,0 +1,46 @@
|
||||
// import YAML from 'yaml';
|
||||
// import { YAMLSeq } from 'yaml/types';
|
||||
|
||||
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 {SecretDataProps[]} obj.data - secrets that we want to download
|
||||
* @param {string} obj.env - used for naming the file
|
||||
* @returns
|
||||
*/
|
||||
const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string; }) => {
|
||||
// if (!data) return;
|
||||
// const doc = new YAML.Document();
|
||||
// doc.contents = new YAMLSeq();
|
||||
// const secrets = await checkOverrides({ data });
|
||||
// secrets.forEach((secret) => {
|
||||
// const pair = YAML.createNode({ [secret.key]: secret.value });
|
||||
// pair.commentBefore = secret.comment
|
||||
// .split('\n')
|
||||
// .map((line) => (line ? ' '.concat(line) : ''))
|
||||
// .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;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { SecretDataProps } from "public/data/frequentInterfaces";
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
encryptSymmetric,
|
||||
} = require("../cryptography/crypto");
|
||||
const nacl = require("tweetnacl");
|
||||
nacl.util = require("tweetnacl-util");
|
||||
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
environment: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
/**
|
||||
* Encypt secrets before pushing the to the DB
|
||||
* @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
|
||||
*/
|
||||
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");
|
||||
|
||||
let randomBytes: string;
|
||||
if (Object.keys(sharedKey).length > 0) {
|
||||
// case: a (shared) key exists for the workspace
|
||||
randomBytes = decryptAssymmetric({
|
||||
ciphertext: sharedKey.latestKey.encryptedKey,
|
||||
nonce: sharedKey.latestKey.nonce,
|
||||
publicKey: sharedKey.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY,
|
||||
});
|
||||
} else {
|
||||
// case: a (shared) key does not exist for the workspace
|
||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
}
|
||||
|
||||
secrets = secretsToEncrypt.map((secret) => {
|
||||
// encrypt key
|
||||
const {
|
||||
ciphertext: secretKeyCiphertext,
|
||||
iv: secretKeyIV,
|
||||
tag: secretKeyTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.key,
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt value
|
||||
const {
|
||||
ciphertext: secretValueCiphertext,
|
||||
iv: secretValueIV,
|
||||
tag: secretValueTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.value,
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
// encrypt comment
|
||||
const {
|
||||
ciphertext: secretCommentCiphertext,
|
||||
iv: secretCommentIV,
|
||||
tag: secretCommentTag,
|
||||
} = encryptSymmetric({
|
||||
plaintext: secret.comment ?? '',
|
||||
key: randomBytes,
|
||||
});
|
||||
|
||||
const result: EncryptedSecretProps = {
|
||||
id: secret.id,
|
||||
createdAt: '',
|
||||
environment: env,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
type: (secret.valueOverride == undefined || secret?.value != secret?.valueOverride) ? 'shared' : 'personal',
|
||||
};
|
||||
|
||||
return result;
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error while encrypting secrets");
|
||||
}
|
||||
|
||||
return secrets;
|
||||
|
||||
}
|
||||
|
||||
export default encryptSecrets;
|
||||
@@ -0,0 +1,141 @@
|
||||
import getSecrets from '~/pages/api/files/GetSecrets';
|
||||
import getLatestFileKey from '~/pages/api/workspace/getLatestFileKey';
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
environment: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string;
|
||||
type: 'personal' | 'shared';
|
||||
comment: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface FunctionProps {
|
||||
env: string;
|
||||
setIsKeyAvailable: any;
|
||||
setData: any;
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the secrets for a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.env - environment for which we are getting secrets
|
||||
* @param {boolean} obj.isKeyAvailable - if a person is able to create new key pairs
|
||||
* @param {function} obj.setData - state function that manages the state of secrets in the dashboard
|
||||
* @param {string} obj.workspaceId - id of a workspace for which we are getting secrets
|
||||
*/
|
||||
const getSecretsForProject = async ({
|
||||
env,
|
||||
setIsKeyAvailable,
|
||||
setData,
|
||||
workspaceId
|
||||
}: FunctionProps) => {
|
||||
try {
|
||||
let encryptedSecrets;
|
||||
try {
|
||||
encryptedSecrets = await getSecrets(workspaceId, env);
|
||||
} catch (error) {
|
||||
console.log('ERROR: Not able to access the latest version of secrets');
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
const tempDecryptedSecrets: SecretProps[] = [];
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
// decrypt secret keys, values, and comments
|
||||
encryptedSecrets.map((secret: EncryptedSecretProps) => {
|
||||
const plainTextKey = decryptSymmetric({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const plainTextValue = decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
let plainTextComment;
|
||||
if (secret.secretCommentCiphertext) {
|
||||
plainTextComment = decryptSymmetric({
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
} else {
|
||||
plainTextComment = "";
|
||||
}
|
||||
|
||||
tempDecryptedSecrets.push({
|
||||
id: secret._id,
|
||||
key: plainTextKey,
|
||||
value: plainTextValue,
|
||||
type: secret.type,
|
||||
comment: plainTextComment
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
});
|
||||
|
||||
setData(result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.log('Something went wrong during accessing or decripting secrets.');
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export default getSecretsForProject;
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-disable */
|
||||
import { PostHog } from 'posthog-js';
|
||||
import { initPostHog } from '~/components/analytics/posthog';
|
||||
import { ENV } from '~/components/utilities/config';
|
||||
|
||||
declare let TELEMETRY_CAPTURING_ENABLED: any;
|
||||
|
||||
class Capturer {
|
||||
api: PostHog;
|
||||
|
||||
constructor() {
|
||||
this.api = initPostHog()!;
|
||||
}
|
||||
|
||||
capture(item: string) {
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
try {
|
||||
this.api.capture(item);
|
||||
} catch (error) {
|
||||
console.error('PostHog', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
identify(id: string) {
|
||||
if (ENV == 'production' && TELEMETRY_CAPTURING_ENABLED) {
|
||||
try {
|
||||
this.api.identify(id);
|
||||
} catch (error) {
|
||||
console.error('PostHog', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default class Telemetry {
|
||||
static instance: Capturer;
|
||||
|
||||
constructor() {
|
||||
if (!Telemetry.instance) {
|
||||
Telemetry.instance = new Capturer();
|
||||
}
|
||||
}
|
||||
|
||||
getInstance() {
|
||||
return Telemetry.instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { GetServerSideProps, GetStaticProps } from "next";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
const DefaultNamespaces = ["common", "nav"];
|
||||
|
||||
type GetTranslatedStaticProps = (
|
||||
namespaces: string[],
|
||||
getStaticProps?: GetStaticProps<any>
|
||||
) => GetStaticProps;
|
||||
|
||||
type GetTranslatedServerSideProps = (
|
||||
namespaces: string[],
|
||||
getStaticProps?: GetServerSideProps<any>
|
||||
) => GetServerSideProps;
|
||||
|
||||
export const getTranslatedStaticProps: GetTranslatedStaticProps =
|
||||
(namespaces, getStaticProps) =>
|
||||
async ({ locale, ...context }) => {
|
||||
let staticProps = { props: {} };
|
||||
if (typeof getStaticProps === "function") {
|
||||
staticProps = (await getStaticProps(context)) as any;
|
||||
}
|
||||
return {
|
||||
...staticProps,
|
||||
props: {
|
||||
...(await serverSideTranslations(locale ?? "en", [
|
||||
...DefaultNamespaces,
|
||||
...namespaces,
|
||||
])),
|
||||
...staticProps.props,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const getTranslatedServerSideProps: GetTranslatedServerSideProps =
|
||||
(namespaces, getServerSideProps) =>
|
||||
async ({ locale, ...context }) => {
|
||||
let staticProps = { props: {} };
|
||||
if (typeof getServerSideProps === "function") {
|
||||
staticProps = (await getServerSideProps(context)) as any;
|
||||
}
|
||||
return {
|
||||
...staticProps,
|
||||
props: {
|
||||
...(await serverSideTranslations(locale ?? "en", [
|
||||
...DefaultNamespaces,
|
||||
...namespaces,
|
||||
])),
|
||||
...staticProps.props,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
export const publicPaths = [
|
||||
`/`,
|
||||
// `/integrations`,
|
||||
`/signupinvite`,
|
||||
`/pricing`,
|
||||
`/signup`,
|
||||
`/login`,
|
||||
`/blog`,
|
||||
`/docs`,
|
||||
`/changelog`,
|
||||
`/security`,
|
||||
`/scheduledemo`,
|
||||
`/blog/[slug]`,
|
||||
`/faq`,
|
||||
`/privacy`,
|
||||
`/terms`,
|
||||
`/subprocessors`,
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
en: "English",
|
||||
ko: "한국어",
|
||||
fr: "Français",
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
actionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the data for a certain action performed by a user
|
||||
* @param {object} obj
|
||||
* @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'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).action;
|
||||
} else {
|
||||
console.log('Failed to get the info about an action');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getActionData;
|
||||
@@ -0,0 +1,72 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
userId: string;
|
||||
actionNames: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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) => {
|
||||
let payload;
|
||||
if (userId != "" && actionNames != '') {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId),
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else if (userId != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
userId: JSON.stringify(userId)
|
||||
}
|
||||
} else if (actionNames != "") {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent',
|
||||
actionNames: actionNames
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
sortBy: 'recent'
|
||||
}
|
||||
}
|
||||
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/logs?' +
|
||||
new URLSearchParams(payload),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).logs;
|
||||
} else {
|
||||
console.log('Failed to get project logs');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectLogs;
|
||||
@@ -0,0 +1,39 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface workspaceProps {
|
||||
workspaceId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {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({
|
||||
offset: String(offset),
|
||||
limit: String(limit)
|
||||
}), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).secretSnapshots;
|
||||
} else {
|
||||
console.log('Failed to get project secret snapshots');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSecretShanpshots;
|
||||
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
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
|
||||
* @returns
|
||||
*/
|
||||
const getProjectSercetSnapshotsCount = async ({ workspaceId }: workspaceProps) => {
|
||||
return SecurityClient.fetchCall(
|
||||
'/api/v1/workspace/' + workspaceId + '/secret-snapshots/count', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getProjectSercetSnapshotsCount;
|
||||
@@ -0,0 +1,31 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface SnapshotProps {
|
||||
secretSnapshotId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the secrets for a certain secret snapshot
|
||||
* @param {object} obj
|
||||
* @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'
|
||||
}
|
||||
}
|
||||
).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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretSnapshotData;
|
||||
@@ -0,0 +1,40 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
|
||||
interface secretVersionProps {
|
||||
secretId: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function fetches the versions of a specific secret
|
||||
* @param {object} obj
|
||||
* @param {string} obj.secretId - the id of a secret for which we are fetching the version history
|
||||
* @param {number} obj.offset - the start of our query
|
||||
* @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)
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return await res.json();
|
||||
} else {
|
||||
console.log('Failed to get secret version history');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getSecretVersions;
|
||||
@@ -0,0 +1,30 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
/**
|
||||
* This function performs a rollback of secrets in a certain project
|
||||
* @param {object} obj
|
||||
* @param {string} obj.workspaceId - id of the project for which we are rolling back data
|
||||
* @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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default performSecretRollback;
|
||||
@@ -0,0 +1,185 @@
|
||||
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 { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
import DashboardInputField from '../../components/dashboard/DashboardInputField';
|
||||
|
||||
|
||||
const {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} = require('../../components/utilities/cryptography/crypto');
|
||||
const nacl = require('tweetnacl');
|
||||
nacl.util = require('tweetnacl-util');
|
||||
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: string) => void;
|
||||
currentAction: string;
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
secret: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyHash: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueHash: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
interface DecryptedSecretProps {
|
||||
newSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
oldSecretVersion: {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @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 { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempActionData = await getActionData({ actionId: currentAction });
|
||||
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
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: {
|
||||
newSecretVersion?: SecretProps;
|
||||
oldSecretVersion?: SecretProps;
|
||||
}) => {
|
||||
return {
|
||||
newSecretVersion: {
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.newSecretVersion!.secretValueIV,
|
||||
tag: encryptedSecretVersion.newSecretVersion!.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
},
|
||||
oldSecretVersion: {
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setActionData(decryptedSecretVersions);
|
||||
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'/>
|
||||
</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>
|
||||
};
|
||||
|
||||
export default ActivitySideBar;
|
||||
@@ -0,0 +1,138 @@
|
||||
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import guidGenerator from '../../components/utilities/randomId';
|
||||
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a single row of the activity table
|
||||
* @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
|
||||
*/
|
||||
const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => {
|
||||
const [payloadOpened, setPayloadOpened] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr key={guidGenerator()} className="bg-bunker-800 duration-100 w-full text-sm">
|
||||
<td
|
||||
onClick={() => setPayloadOpened(!payloadOpened)}
|
||||
className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={payloadOpened ? faAngleDown : faAngleRight}
|
||||
className={`mt-2.5 ml-6 text-bunker-100 hover:bg-mineshaft-700 ${
|
||||
payloadOpened && 'bg-mineshaft-500'
|
||||
} p-1 duration-100 h-4 w-4 rounded-md`}
|
||||
/>
|
||||
</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}
|
||||
</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>
|
||||
</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>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the table for activity logs (one of the tabs)
|
||||
* @param {object} obj
|
||||
* @param {logData} obj.data - data for user activity logs
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @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 { 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>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data?.map((row, index) => {
|
||||
return <ActivityLogsRow key={index} 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>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityTable;
|
||||
@@ -0,0 +1,183 @@
|
||||
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";
|
||||
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";
|
||||
|
||||
|
||||
export interface SecretDataProps {
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
id: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: boolean) => void;
|
||||
setSnapshotData: (value: any) => void;
|
||||
chosenSnapshot: string;
|
||||
}
|
||||
|
||||
interface SnaphotProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
environment: string;
|
||||
type: "personal" | "shared";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {function} obj.setSnapshotData - state manager for snapshot data
|
||||
* @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 { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState<SnaphotProps[]>([]);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 15;
|
||||
|
||||
const loadMoreSnapshots = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
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 secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) })
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => {
|
||||
return {
|
||||
id: encryptedSecretVersion._id,
|
||||
pos: pos,
|
||||
type: encryptedSecretVersion.type,
|
||||
environment: encryptedSecretVersion.environment,
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.secretKeyIV,
|
||||
tag: encryptedSecretVersion.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
});
|
||||
|
||||
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>
|
||||
</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="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='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>
|
||||
};
|
||||
|
||||
export default PITRecoverySidebar;
|
||||
@@ -0,0 +1,123 @@
|
||||
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 { 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';
|
||||
|
||||
interface DecryptedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
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 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 PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map((encryptedSecretVersion: EncrypetedSecretVersionListProps) => {
|
||||
return {
|
||||
createdAt: encryptedSecretVersion.createdAt,
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setSecretVersions(decryptedSecretVersions);
|
||||
setIsLoading(false);
|
||||
} catch (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'
|
||||
})}
|
||||
</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='w-full h-full flex items-center justify-center text-bunker-400'>No version history yet.</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
*
|
||||
* @param textOld - old secret
|
||||
* @param textNew - new (updated) secret
|
||||
* @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
|
||||
*/
|
||||
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
|
||||
* @returns - an ordered Map, with the arr[i] value as the Map key and the
|
||||
* array index i as the Map value.
|
||||
*/
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* @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);
|
||||
|
||||
ma.forEach((val, key, map) => {
|
||||
if (mb.has(key)) {
|
||||
map.set(key, {
|
||||
indexA: val,
|
||||
indexB: mb.get(key)
|
||||
});
|
||||
} else {
|
||||
map.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
return ma;
|
||||
}
|
||||
|
||||
/**
|
||||
* longestCommonSubsequence takes an ordered Map from the function uniqueCommon
|
||||
* and determines the Longest Common Subsequence (LCS).
|
||||
* @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 = [];
|
||||
|
||||
// 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++;
|
||||
}
|
||||
|
||||
if (!ja[i]) {
|
||||
ja[i] = [];
|
||||
}
|
||||
|
||||
if (0 < i) {
|
||||
val.prev = ja[i-1][ja[i - 1].length - 1];
|
||||
}
|
||||
ja[i].push(val);
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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++;
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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--;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
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[i].indexA <= aHi || x[i].indexB <= bHi) {
|
||||
addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurseLCS(0, textOld.length - 1, 0, textNew.length - 1);
|
||||
|
||||
if (diffPlusFlag) {
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
aMove: aMove,
|
||||
aMoveIndex: aMoveIndex,
|
||||
bMove: bMove,
|
||||
bMoveIndex: bMoveIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
lines: result,
|
||||
lineCountDeleted: deleted,
|
||||
lineCountInserted: inserted,
|
||||
lineCountMoved: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* use: patienceDiffPlus( textOld[], textNew[] )
|
||||
*
|
||||
* where:
|
||||
* textOld[] contains the original text lines.
|
||||
* textNew[] contains the new text lines.
|
||||
*
|
||||
* returns an object with the following properties:
|
||||
* lines[] with properties of:
|
||||
* line containing the line of text from textOld or textNew.
|
||||
* aIndex referencing the index in aLine[].
|
||||
* bIndex referencing the index in textNew[].
|
||||
* (Note: The line is text from either textOld or textNew, with aIndex and bIndex
|
||||
* referencing the original index. If aIndex === -1 then the line is new from textNew,
|
||||
* and if bIndex === -1 then the line is old from textOld.)
|
||||
* moved is true if the line was moved from elsewhere in textOld[] or textNew[].
|
||||
* lineCountDeleted is the number of lines from textOld[] not appearing in textNew[].
|
||||
* lineCountInserted is the number of lines from textNew[] not appearing in textOld[].
|
||||
* lineCountMoved is the number of lines that moved.
|
||||
*/
|
||||
|
||||
function patienceDiffPlus(textOld: string[], textNew: string[]) {
|
||||
|
||||
const difference = patienceDiff(textOld, textNew, true);
|
||||
|
||||
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;
|
||||
|
||||
let lastLineCountMoved;
|
||||
|
||||
do {
|
||||
const aMove = aMoveNext;
|
||||
const aMoveIndex = aMoveIndexNext;
|
||||
const bMove = bMoveNext;
|
||||
const bMoveIndex = bMoveIndexNext;
|
||||
|
||||
aMoveNext = [];
|
||||
aMoveIndexNext = [];
|
||||
bMoveNext = [];
|
||||
bMoveIndexNext = [];
|
||||
|
||||
const subDiff = patienceDiff(aMove!, bMove!);
|
||||
|
||||
lastLineCountMoved = difference.lineCountMoved;
|
||||
|
||||
subDiff.lines.forEach((v, i) => {
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
export default patienceDiff;
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Time since a certain date
|
||||
* @param {Date} date - the timestamp got which we want to understand how long ago it happened
|
||||
* @returns {String} text - how much time has passed since a certain timestamp
|
||||
*/
|
||||
function timeSince(date: Date) {
|
||||
const seconds = Math.floor(
|
||||
((new Date() as any) - (date as any)) / 1000
|
||||
) as number;
|
||||
|
||||
let interval = seconds / 31536000;
|
||||
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' years ago';
|
||||
}
|
||||
interval = seconds / 2592000;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' months ago';
|
||||
}
|
||||
interval = seconds / 86400;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' days ago';
|
||||
}
|
||||
interval = seconds / 3600;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' hours ago';
|
||||
}
|
||||
interval = seconds / 60;
|
||||
if (interval > 1) {
|
||||
return Math.floor(interval) + ' minutes ago';
|
||||
}
|
||||
return Math.floor(seconds) + ' seconds ago';
|
||||
}
|
||||
|
||||
export default timeSince;
|
||||
@@ -0,0 +1 @@
|
||||
export { usePopUp } from './usePopUp';
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
interface usePopUpProps {
|
||||
name: Readonly<string>;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* to provide better intellisense
|
||||
* 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]]: {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is used to manage multiple popUps/modal/dialog in a page
|
||||
* 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[]>(
|
||||
popUpNames: 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
|
||||
);
|
||||
|
||||
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 handlePopUpToggle = useCallback((popUpName: keyof usePopUpState<T>) => {
|
||||
setPopUp((popUp) => ({
|
||||
...popUp,
|
||||
[popUpName]: { isOpen: !popUp[popUpName].isOpen },
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
popUp,
|
||||
handlePopUpOpen,
|
||||
handlePopUpClose,
|
||||
handlePopUpToggle,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function Custom404() {
|
||||
return (
|
||||
<div className='bg-bunker-800 md:h-screen flex flex-col justify-between'>
|
||||
<Head>
|
||||
<title>Infisical | Page Not Found</title>
|
||||
<link rel='icon' href='/infisical.ico' />
|
||||
</Head>
|
||||
<div className='flex flex-col items-center justify-center text-gray-200 h-screen w-screen'>
|
||||
<p className='text-4xl mt-32'>Oops, something went wrong</p>
|
||||
<p className='mt-2 mb-1 text-lg'>
|
||||
Think this is a mistake? Email{' '}
|
||||
<a
|
||||
className='text-primary underline underline-offset-4'
|
||||
href='mailto:[email protected]'
|
||||
>
|
||||
team@infisical.com
|
||||
</a>{' '}
|
||||
and we`ll fix it!{' '}
|
||||
</p>
|
||||
<Link
|
||||
href='/dashboard'
|
||||
className='bg-mineshaft-500 mt-8 py-2 px-4 rounded-md hover:bg-primary diration-200 hover:text-black cursor-pointer font-semibold'
|
||||
>
|
||||
Go to Dashboard
|
||||
</Link>
|
||||
<Image
|
||||
src='/images/dragon-404.svg'
|
||||
height={554}
|
||||
width={942}
|
||||
alt='infisical dragon - page not found'
|
||||
></Image>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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 '@fortawesome/fontawesome-svg-core/styles.css';
|
||||
import '../styles/globals.css';
|
||||
|
||||
config.autoAddCss = false;
|
||||
|
||||
type NextAppProp = AppProps & {
|
||||
Component: AppProps['Component'] & { requireAuth: boolean };
|
||||
};
|
||||
|
||||
const App = ({
|
||||
Component,
|
||||
pageProps,
|
||||
...appProps
|
||||
}: NextAppProp): JSX.Element => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const storedLang = localStorage.getItem('lang');
|
||||
if (router.locale ?? 'en' !== storedLang ?? 'en') {
|
||||
router.push(router.asPath, router.asPath, {
|
||||
locale: storedLang ?? 'en',
|
||||
});
|
||||
}
|
||||
}, [router.locale, router.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
// Init for auto capturing
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
|
||||
const handleRouteChange = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
telemetry.capture('$pageview');
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on('routeChangeComplete', handleRouteChange);
|
||||
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', handleRouteChange);
|
||||
};
|
||||
}, [router.events]);
|
||||
|
||||
// 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]) ||
|
||||
!Component.requireAuth
|
||||
) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteGuard>
|
||||
<NotificationProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
</RouteGuard>
|
||||
);
|
||||
};
|
||||
|
||||
export default appWithTranslation(App);
|
||||
|
||||
{
|
||||
/* <Script
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-DQ1XLJJGG1"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
{`
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){window.dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-DQ1XLJJGG1');
|
||||
`}
|
||||
</Script> */
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from "next-i18next";
|
||||
import ActivitySideBar from '@app/ee/components/ActivitySideBar';
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import EventFilter from '~/components/basic/EventFilter';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import { getTranslatedServerSideProps } from '~/components/utilities/withTranslateProps';
|
||||
|
||||
import getProjectLogs from '../../ee/api/secrets/GetProjectLogs';
|
||||
import ActivityTable from '../../ee/components/ActivityTable';
|
||||
|
||||
|
||||
interface logData {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: {
|
||||
email: string;
|
||||
};
|
||||
actions: {
|
||||
_id: string;
|
||||
name: string;
|
||||
payload: {
|
||||
secretVersions: string[];
|
||||
}
|
||||
}[]
|
||||
}
|
||||
|
||||
interface PayloadProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface logDataPoint {
|
||||
_id: string;
|
||||
channel: string;
|
||||
createdAt: string;
|
||||
ipAddress: string;
|
||||
user: string;
|
||||
payload: PayloadProps[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the tab that includes all of the user activity logs
|
||||
*/
|
||||
export default function Activity() {
|
||||
const router = useRouter();
|
||||
const [eventChosen, setEventChosen] = useState('');
|
||||
const [logsData, setLogsData] = useState<logDataPoint[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 10;
|
||||
const [currentSidebarAction, toggleSidebar] = useState<string>()
|
||||
const { t } = useTranslation();
|
||||
|
||||
// this use effect updates the data in case of a new filter being added
|
||||
useEffect(() => {
|
||||
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
|
||||
}
|
||||
})
|
||||
}
|
||||
}))
|
||||
setIsLoading(false);
|
||||
}
|
||||
getLogData();
|
||||
}, [eventChosen]);
|
||||
|
||||
// this use effect adds more data in case 'View More' button is clicked
|
||||
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 {
|
||||
_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} />}
|
||||
<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>
|
||||
</div>
|
||||
<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}
|
||||
/>
|
||||
</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"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Activity.requireAuth = true;
|
||||
|
||||
export const getServerSideProps = getTranslatedServerSideProps(["activity", "common"]);
|
||||
@@ -0,0 +1,37 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route adds an API key for the user
|
||||
* @param {object} obj
|
||||
* @param {string} obj.name - name of the API key
|
||||
* @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/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
expiresIn
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json());
|
||||
} else {
|
||||
console.log('Failed to add API key');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default addAPIKey;
|
||||
@@ -0,0 +1,30 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
apiKeyId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route revokes the API key with id [apiKeyId]
|
||||
* @param {object} obj
|
||||
* @param {string} obj.apiKeyId - id of the API key to delete
|
||||
* @returns
|
||||
*/
|
||||
const deleteAPIKey = ({
|
||||
apiKeyId
|
||||
}: Props) => {
|
||||
return 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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default deleteAPIKey;
|
||||
@@ -0,0 +1,26 @@
|
||||
import SecurityClient from '~/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'
|
||||
}
|
||||
}
|
||||
).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return (await res.json()).apiKeyData;
|
||||
} else {
|
||||
console.log('Failed to get API keys');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default getAPIKeys;
|
||||
@@ -0,0 +1,47 @@
|
||||
import SecurityClient from '~/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
clientProof: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the second step of the change password process (pake)
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const changePassword2 = ({
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
clientProof
|
||||
}: Props) => {
|
||||
return 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
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status == 200) {
|
||||
return res;
|
||||
} else {
|
||||
console.log('Failed to change the password');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default changePassword2;
|
||||
@@ -0,0 +1,16 @@
|
||||
import SecurityClient from '~/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', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then((res) => res);
|
||||
};
|
||||
|
||||
export default checkAuth;
|
||||
@@ -0,0 +1,26 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route check the verification code from the email that user just recieved
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email
|
||||
* @param {string} obj.code
|
||||
* @returns
|
||||
*/
|
||||
const checkEmailVerificationCode = ({ email, code }: Props) => {
|
||||
return fetch('/api/v1/signup/email/verify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
code: code
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export default checkEmailVerificationCode;
|
||||
@@ -0,0 +1,66 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
publicKey: string;
|
||||
ciphertext: string;
|
||||
organizationName: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of the user completing signup
|
||||
* @param {string} obj.firstName - first name of the user completing signup
|
||||
* @param {string} obj.lastName - last name of the user completing sign up
|
||||
* @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
|
||||
* @param {string} obj.publicKey - public key of the user completing signup
|
||||
* @param {string} obj.ciphertext
|
||||
* @param {string} obj.iv
|
||||
* @param {string} obj.tag
|
||||
* @param {string} obj.salt
|
||||
* @param {string} obj.verifier
|
||||
* @param {string} obj.token - token that confirms a user's identity
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignup = ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName,
|
||||
publicKey,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}: Props) => {
|
||||
return fetch('/api/v1/signup/complete-account/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
organizationName,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export default completeAccountInformationSignup;
|
||||
@@ -0,0 +1,62 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
publicKey: string;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of the user completing signupinvite flow
|
||||
* @param {string} obj.firstName - first name of the user completing signupinvite flow
|
||||
* @param {string} obj.lastName - last name of the user completing signupinvite flow
|
||||
* @param {string} obj.publicKey - public key of the user completing signupinvite flow
|
||||
* @param {string} obj.ciphertext
|
||||
* @param {string} obj.iv
|
||||
* @param {string} obj.tag
|
||||
* @param {string} obj.salt
|
||||
* @param {string} obj.verifier
|
||||
* @param {string} obj.token - token that confirms a user's identity
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignupInvite = ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}: Props) => {
|
||||
return fetch('/api/v1/signup/complete-account/invite', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
publicKey: publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv: iv,
|
||||
tag: tag,
|
||||
salt: salt,
|
||||
verifier: verifier
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export default completeAccountInformationSignupInvite;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user