Merge branch 'main' into issue-318

This commit is contained in:
mv-turtle
2023-02-20 20:36:44 -08:00
committed by GitHub
179 changed files with 7116 additions and 2948 deletions
+6 -6
View File
@@ -3,8 +3,8 @@ import { Switch } from '@headlessui/react';
interface ToggleProps {
enabled: boolean;
setEnabled: (value: boolean) => void;
addOverride: (value: string | undefined, pos: number) => void;
pos: number;
addOverride: (value: string | undefined, id: string) => void;
id: string;
}
/**
@@ -13,18 +13,18 @@ interface ToggleProps {
* @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
* @param {number} obj.id - id of a certain secret
* @returns
*/
const Toggle = ({ enabled, setEnabled, addOverride, pos }: ToggleProps): JSX.Element => {
const Toggle = ({ enabled, setEnabled, addOverride, id }: ToggleProps): JSX.Element => {
return (
<Switch
checked={enabled}
onChange={() => {
if (enabled === false) {
addOverride('', pos);
addOverride('', id);
} else {
addOverride(undefined, pos);
addOverride(undefined, id);
}
setEnabled(!enabled);
}}
@@ -1,7 +1,5 @@
import { Fragment } from 'react';
import { useRouter } from 'next/router';
import { Dialog, Transition } from '@headlessui/react';
import { plans } from 'public/data/frequentConstants';
import Button from '../buttons/Button';
import InputField from '../InputField';
@@ -12,7 +10,6 @@ type Props = {
submitModal: (email: string) => void;
email: string;
setEmail: (email: string) => void;
currentPlan: string;
orgName: string;
};
@@ -22,13 +19,11 @@ const AddUserDialog = ({
submitModal,
email,
setEmail,
currentPlan,
orgName,
}: Props) => {
const submit = () => {
submitModal(email);
};
const router = useRouter();
return (
<div className='z-50'>
@@ -81,28 +76,6 @@ const AddUserDialog = ({
isRequired
/>
</div>
{currentPlan === plans.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 the Starter plan.
</button>
<button
type='button'
className='ml-1 inline-flex justify-center rounded-md py-1 underline underline-offset-2 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}
@@ -45,19 +45,19 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => {
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">
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker border border-mineshaft-600 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-bunker-200">
{t('dashboard:sidebar.delete-key-dialog.title')}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
<p className="text-sm text-bunker-300">
{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"
className="inline-flex justify-center rounded-md border border-transparent bg-red-500 opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium text-bunker-100 text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={onSubmit}
>
Delete
@@ -30,7 +30,7 @@ type Props = {
type EnvironmentProps = {
name: string;
slug: string;
}
};
/**
* This is the component that shows the users of a certin project
@@ -91,26 +91,38 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
});
};
const handlePermissionUpdate = (index: number, val: string, membershipId: string, slug: string ) => {
let denials: { ability: string; environmentSlug: string; }[];
if (val === "Read Only") {
denials = [{
ability: "write",
environmentSlug: slug
}];
} else if (val === "No Access") {
denials = [{
ability: "write",
environmentSlug: slug
}, {
ability: "read",
environmentSlug: slug
}];
} else if (val === "Add Only") {
denials = [{
ability: "read",
environmentSlug: slug
}];
const handlePermissionUpdate = (
index: number,
val: string,
membershipId: string,
slug: string
) => {
let denials: { ability: string; environmentSlug: string }[];
if (val === 'Read Only') {
denials = [
{
ability: 'write',
environmentSlug: slug
}
];
} else if (val === 'No Access') {
denials = [
{
ability: 'write',
environmentSlug: slug
},
{
ability: 'read',
environmentSlug: slug
}
];
} else if (val === 'Add Only') {
denials = [
{
ability: 'read',
environmentSlug: slug
}
];
} else {
denials = [];
}
@@ -118,8 +130,12 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
if (currentPlan !== plans.professional && host === 'https://app.infisical.com') {
setIsUpgradeModalOpen(true);
} else {
const allDenials = userData[index].deniedPermissions.filter((perm: { ability: string; environmentSlug: string; }) => perm.environmentSlug !== slug).concat(denials);
updateUserProjectPermission({ membershipId, denials: allDenials});
const allDenials = userData[index].deniedPermissions
.filter(
(perm: { ability: string; environmentSlug: string }) => perm.environmentSlug !== slug
)
.concat(denials);
updateUserProjectPermission({ membershipId, denials: allDenials });
changeData([
...userData.slice(0, index),
...[
@@ -156,7 +172,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
orgId
});
if (subscriptions) {
setCurrentPlan(subscriptions.data[0].plan.product)
setCurrentPlan(subscriptions.data[0].plan.product);
}
})();
}, [userData, myUser]);
@@ -186,25 +202,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
const closeUpgradeModal = () => {
setIsUpgradeModalOpen(false);
}
};
return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max">
<div className="absolute rounded-t-md w-full h-[3.1rem] bg-white/5" />
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-700 bg-bunker">
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
<UpgradePlanModal
isOpen={isUpgradeModalOpen}
onClose={closeUpgradeModal}
text="You can change user permissions if you switch to Infisical's Professional plan."
/>
<table className="w-full my-0.5">
<thead className="text-gray-400 text-xs font-light">
<table className="my-0.5 w-full">
<thead className="text-xs font-light text-gray-400">
<tr>
<th className="text-left pl-4 py-3.5">NAME</th>
<th className="text-left pl-4 py-3.5">EMAIL</th>
<th className="text-left pl-6 pr-10 py-3.5">ROLE</th>
{workspaceEnvs.map(env => (
<th key={guidGenerator()} className="text-left pl-2 py-1 max-w-min break-normal">
<span>{env.slug.toUpperCase()}<br/></span>
<th className="py-3.5 pl-4 text-left">NAME</th>
<th className="py-3.5 pl-4 text-left">EMAIL</th>
<th className="py-3.5 pl-6 pr-10 text-left">ROLE</th>
{workspaceEnvs.map((env) => (
<th key={guidGenerator()} className="max-w-min break-normal py-1 pl-2 text-left">
<span>
{env.slug.toUpperCase()}
<br />
</span>
{/* <span>PERMISSION</span> */}
</th>
))}
@@ -227,28 +246,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)
.map((row, index) => (
<tr key={guidGenerator()} className="bg-bunker-600 text-sm hover:bg-bunker-500">
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
{row.firstName} {row.lastName}
</td>
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
<td className="border-t border-mineshaft-700 py-2 pl-4 text-gray-300">
{row.email}
</td>
<td className="pl-6 pr-10 py-2 border-mineshaft-700 border-t text-gray-300">
<div className="justify-start h-full flex flex-row items-center">
<Select
<td className="border-t border-mineshaft-700 py-2 pl-6 pr-10 text-gray-300">
<div className="flex h-full flex-row items-center justify-start">
<Select
className="w-36 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
// open={isOpen}
onValueChange={(e) => handleRoleUpdate(index, e)}
value={row.role}
disabled={myRole !== 'admin' || myUser === row.email}
isDisabled={myRole !== 'admin' || myUser === row.email}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
</Select>
{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">
<div className="rounded-md border border-mineshaft-700 bg-white/5 text-white duration-200 hover:bg-primary hover:text-black">
<Button
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
color="mineshaft"
@@ -259,43 +278,106 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
)}
</div>
</td>
{workspaceEnvs.map((env) => <td key={guidGenerator()} className="pl-2 py-2 border-mineshaft-700 border-t text-gray-300">
<Select
className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
position="item-aligned"
// open={isOpen}
onValueChange={(val) => handlePermissionUpdate(index, val, row.membershipId, env.slug)}
value={
// eslint-disable-next-line no-nested-ternary
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
? "No Access"
// eslint-disable-next-line no-nested-ternary
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Read Only"
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? "Add Only" : "Read & Write")
}
icon={
// eslint-disable-next-line no-nested-ternary
(row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read"))
? faEyeSlash
// eslint-disable-next-line no-nested-ternary
: (row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faEye
: !row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("write") && row.deniedPermissions.filter((perm: any) => perm.environmentSlug === env.slug).map((perm: {ability: string}) => perm.ability).includes("read") ? faPlus : faPenToSquare)
}
disabled={myRole !== 'admin'}
// onOpenChange={(open) => setIsOpen(open)}
{workspaceEnvs.map((env) => (
<td
key={guidGenerator()}
className="border-t border-mineshaft-700 py-2 pl-2 text-gray-300"
>
<SelectItem value="No Access" customIcon={faEyeSlash}>No Access</SelectItem>
<SelectItem value="Read Only" customIcon={faEye}>Read Only</SelectItem>
<SelectItem value="Add Only" customIcon={faPlus}>Add Only</SelectItem>
<SelectItem value="Read & Write" customIcon={faPenToSquare}>Read & Write</SelectItem>
</Select>
</td>)}
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
<Select
className="w-16 bg-mineshaft-700"
dropdownContainerClassName="bg-mineshaft-700"
position="item-aligned"
// open={isOpen}
onValueChange={(val) =>
handlePermissionUpdate(index, val, row.membershipId, env.slug)
}
value={
// eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('write') &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('read')
? 'No Access'
: // eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('write') &&
!row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('read')
? 'Read Only'
: !row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('write') &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('read')
? 'Add Only'
: 'Read & Write'
}
icon={
// eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('write') &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('read')
? faEyeSlash
: // eslint-disable-next-line no-nested-ternary
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('write') &&
!row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('read')
? faEye
: !row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('write') &&
row.deniedPermissions
.filter((perm: any) => perm.environmentSlug === env.slug)
.map((perm: { ability: string }) => perm.ability)
.includes('read')
? faPlus
: faPenToSquare
}
isDisabled={myRole !== 'admin'}
// onOpenChange={(open) => setIsOpen(open)}
>
<SelectItem value="No Access" customIcon={faEyeSlash}>
No Access
</SelectItem>
<SelectItem value="Read Only" customIcon={faEye}>
Read Only
</SelectItem>
<SelectItem value="Add Only" customIcon={faPlus}>
Add Only
</SelectItem>
<SelectItem value="Read & Write" customIcon={faPenToSquare}>
Read & Write
</SelectItem>
</Select>
</td>
))}
<td className="border-0.5 flex flex-row justify-end border-t border-mineshaft-700 py-2 pl-8 pr-8">
{myUser !== row.email &&
// row.role !== "admin" &&
myRole !== 'member' ? (
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
<Button
onButtonPressed={() => handleDelete(row.membershipId, index)}
color="red"
@@ -304,7 +386,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter }: Props) => {
/>
</div>
) : (
<div className="w-9 h-9" />
<div className="h-9 w-9" />
)}
</td>
</tr>
@@ -36,7 +36,7 @@ const Notification = ({ notification, clearNotification }: NotificationProps) =>
return (
<div
className="relative w-full flex items-center justify-between px-6 py-6 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
className="relative w-full flex items-center justify-between px-6 py-4 rounded-md border border-bunker-500 pointer-events-auto bg-mineshaft-700 mb-3 right-3"
role="alert"
>
{notification.type === 'error' && (
@@ -13,7 +13,7 @@ import { Tag } from 'public/data/frequentInterfaces';
* @param {function} obj.modifyTags - modify tags for a certain secret
* @param {Tag[]} obj.position - currently selected tags for a certain secret
*/
const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], position: number) => void; position: number; }) => {
const AddTagsMenu = ({ allTags, currentTags, modifyTags, id }: { allTags: Tag[]; currentTags: Tag[]; modifyTags: (value: Tag[], id: string) => void; id: string; }) => {
const router = useRouter();
return (
<Menu as="div" className="ml-2 relative inline-block text-left">
@@ -41,7 +41,7 @@ const AddTagsMenu = ({ allTags, currentTags, modifyTags, position }: { allTags:
<button
type="button"
className={`${currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? "opacity-30 cursor-default" : "hover:bg-mineshaft-700"} w-full text-left bg-mineshaft-800 px-2 py-0.5 text-bunker-200 rounded-sm flex items-center`}
onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), position)}}}
onClick={() => {if (!currentTags?.map(currentTag => currentTag.name).includes(tag.name)) {modifyTags(currentTags.concat([tag]), id)}}}
>
{currentTags?.map(currentTag => currentTag.name).includes(tag.name) ? <FontAwesomeIcon icon={faCheckSquare} className="text-xs mr-2 text-primary"/> : <FontAwesomeIcon icon={faSquare} className="text-xs mr-2"/>} {tag.name}
</button>
@@ -6,11 +6,11 @@ import { useTranslation } from 'next-i18next';
const CommentField = ({
comment,
modifyComment,
position
id
}: {
comment: string;
modifyComment: (value: string, posistion: number) => void;
position: number;
modifyComment: (value: string, id: string) => void;
id: string;
}) => {
const { t } = useTranslation();
@@ -20,7 +20,7 @@ const CommentField = ({
<textarea
className="placeholder:text-bunker-400 dark:[color-scheme:dark] 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)}
onChange={(e) => modifyComment(e.target.value, id)}
placeholder="Leave any comments here..."
/>
</div>
@@ -9,15 +9,15 @@ import { PopoverObject } from '../v2/Popover/Popover';
const REGEX = /([$]{.*?})/g;
interface DashboardInputFieldProps {
position: number;
onChangeHandler: (value: string, position: number) => void;
id: string;
onChangeHandler: (value: string, id: string) => void;
value: string | undefined;
type: 'varName' | 'value' | 'comment';
blurred?: boolean;
isDuplicate?: boolean;
isCapitalized?: boolean;
overrideEnabled?: boolean;
modifyValueOverride?: (value: string | undefined, position: number) => void;
modifyValueOverride?: (value: string | undefined, id: string) => void;
isSideBarOpen?: boolean;
}
@@ -37,7 +37,7 @@ interface DashboardInputFieldProps {
*/
const DashboardInputField = ({
position,
id,
onChangeHandler,
type,
value,
@@ -72,7 +72,7 @@ const DashboardInputField = ({
}`}
>
<input
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, position)}
onChange={(e) => onChangeHandler(isCapitalized ? e.target.value.toUpperCase() : e.target.value, id)}
type={type}
value={value}
className={`z-10 peer font-mono ph-no-capture bg-transparent h-full caret-bunker-200 text-sm px-2 w-full min-w-16 outline-none ${
@@ -99,15 +99,15 @@ const DashboardInputField = ({
/>
</div>
)}
{!error && <div className={`absolute right-0 top-0 text-red z-50 ${
overrideEnabled ? 'visible group-hover:bg-mineshaft-700' : 'invisible group-hover:visible bg-mineshaft-700'
{!error && <div className={`absolute right-0 top-0 text-red z-50 bg-mineshaft-800 group-hover:bg-mineshaft-700 ${
overrideEnabled ? 'visible' : 'invisible group-hover:visible'
} cursor-pointer duration-0 h-10 flex items-center px-2`}>
<button type="button" onClick={() => {
if (modifyValueOverride) {
if (overrideEnabled === false) {
modifyValueOverride('', position);
modifyValueOverride('', id);
} else {
modifyValueOverride(undefined, position);
modifyValueOverride(undefined, id);
}
}
}}>
@@ -126,7 +126,7 @@ const DashboardInputField = ({
const error = startsWithNumber || isDuplicate;
return (
<PopoverObject text={value || ''} onChangeHandler={onChangeHandler} position={position}>
<PopoverObject text={value || ''} onChangeHandler={onChangeHandler} id={id}>
<div title={value} className={`relative flex-col w-full h-10 overflow-hidden ${
isSideBarOpen && 'bg-mineshaft-700 duration-200'
}`}>
@@ -157,7 +157,7 @@ const DashboardInputField = ({
)}
<input
value={value}
onChange={(e) => onChangeHandler(e.target.value, position)}
onChange={(e) => onChangeHandler(e.target.value, id)}
onScroll={syncScroll}
className={`${
blurred
@@ -175,10 +175,10 @@ const DashboardInputField = ({
} ${overrideEnabled ? 'text-primary-300' : 'text-gray-400'}
absolute flex flex-row whitespace-pre font-mono z-0 ${blurred ? 'invisible' : 'visible'} peer-focus:visible mt-0.5 ph-no-capture overflow-x-scroll bg-transparent h-10 text-sm px-2 py-2 w-full min-w-16 outline-none duration-100 no-scrollbar no-scrollbar::-webkit-scrollbar`}
>
{value?.split(REGEX).map((word, id) => {
{value?.split(REGEX).map((word) => {
if (word.match(REGEX) !== null) {
return (
<span className="ph-no-capture text-yellow" key={`${word}.${id + 1}`}>
<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)}
@@ -231,7 +231,7 @@ function inputPropsAreEqual(prev: DashboardInputFieldProps, next: DashboardInput
return (
prev.value === next.value &&
prev.type === next.type &&
prev.position === next.position &&
prev.id === next.id &&
prev.blurred === next.blurred &&
prev.isCapitalized === next.isCapitalized &&
prev.overrideEnabled === next.overrideEnabled &&
+12 -1
View File
@@ -8,6 +8,7 @@ import { parseDocument, Scalar, YAMLMap } from 'yaml';
import Button from '../basic/buttons/Button';
import Error from '../basic/Error';
import { useNotificationContext } from '../context/Notifications/NotificationProvider';
import { parseDotEnv } from '../utilities/parseDotEnv';
import guidGenerator from '../utilities/randomId';
@@ -32,6 +33,7 @@ const DropZone = ({
numCurrentRows
}: DropZoneProps) => {
const { t } = useTranslation();
const { createNotification } = useNotificationContext();
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
@@ -110,6 +112,15 @@ const DropZone = ({
const file = e.dataTransfer.files[0];
const reader = new FileReader();
if (file === undefined) {
createNotification({
text: `You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.`,
type: 'error',
timeoutMs: 10000
});
setLoading(false);
return;
}
const fileType = file.name.split('.')[1];
reader.onload = (event) => {
@@ -164,7 +175,7 @@ const DropZone = ({
id="fileSelect"
type="file"
className="opacity-0 absolute w-full h-full"
accept=".txt,.env,.yml"
accept=""
onChange={handleFileSelect}
/>
{errorDragAndDrop ? <div className="my-3 max-w-xl opacity-80" /> : <div className="" />}
@@ -10,10 +10,10 @@ import { Menu, Transition } from '@headlessui/react';
*/
const GenerateSecretMenu = ({
modifyValue,
position
id
}: {
modifyValue: (value: string, position: number) => void;
position: number;
modifyValue: (value: string, id: string) => void;
id: string;
}) => {
const [randomStringLength, setRandomStringLength] = useState(32);
const { t } = useTranslation();
@@ -51,7 +51,7 @@ const GenerateSecretMenu = ({
[...Array(randomStringLength)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join(''),
position
id
);
}
}}
+11 -11
View File
@@ -8,11 +8,11 @@ import { DeleteActionButton } from './DeleteActionButton';
interface KeyPairProps {
keyPair: SecretDataProps;
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;
modifyTags: (value: Tag[], position: number) => void;
modifyKey: (value: string, id: string) => void;
modifyValue: (value: string, id: string) => void;
modifyValueOverride: (value: string | undefined, id: string) => void;
modifyComment: (value: string, id: string) => void;
modifyTags: (value: Tag[], id: string) => void;
isBlurred: boolean;
isDuplicate: boolean;
toggleSidebar: (id: string) => void;
@@ -55,7 +55,7 @@ const colorsText = [
/**
* 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 {SecretDataProps[]} 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
@@ -108,7 +108,7 @@ const KeyPair = ({
isCapitalized = {isCapitalized}
onChangeHandler={modifyKey}
type="varName"
position={keyPair.pos}
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
value={keyPair.key}
isDuplicate={isDuplicate}
overrideEnabled={keyPair.valueOverride !== undefined}
@@ -124,7 +124,7 @@ const KeyPair = ({
<DashboardInputField
onChangeHandler={keyPair.valueOverride !== undefined ? modifyValueOverride : modifyValue}
type="value"
position={keyPair.pos}
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
value={keyPair.valueOverride !== undefined ? keyPair.valueOverride : keyPair.value}
blurred={isBlurred}
overrideEnabled={keyPair.valueOverride !== undefined}
@@ -137,7 +137,7 @@ const KeyPair = ({
<DashboardInputField
onChangeHandler={modifyComment}
type="comment"
position={keyPair.pos}
id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')}
value={keyPair.comment}
isDuplicate={isDuplicate}
isSideBarOpen={keyPair.id === sidebarSecretId}
@@ -149,11 +149,11 @@ const KeyPair = ({
{keyPair.tags?.map((tag, index) => (
index < 2 && <div key={keyPair.pos} className={`ml-2 px-1.5 ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.color} rounded-sm text-sm ${tagData.filter(tagDp => tagDp._id === tag._id)[0]?.colorText} flex items-center`}>
<span className='mb-0.5 cursor-default'>{tag.name}</span>
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.pos)}/>
<FontAwesomeIcon icon={faXmark} className="ml-1 cursor-pointer p-1" onClick={() => modifyTags(keyPair.tags.filter(ttag => ttag._id !== tag._id), keyPair.id)}/>
</div>
))}
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} position={keyPair.pos} />
<AddTagsMenu allTags={tags} currentTags={keyPair.tags} modifyTags={modifyTags} id={keyPair.id ? keyPair.id : (keyPair.idOverride || '')} />
</div>
</div>
<div
+11 -11
View File
@@ -33,10 +33,10 @@ export interface DeleteRowFunctionProps {
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;
modifyKey: (value: string, id: string) => void;
modifyValue: (value: string, id: string) => void;
modifyValueOverride: (value: string | undefined, id: string) => void;
modifyComment: (value: string, id: string) => void;
buttonReady: boolean;
savePush: () => void;
sharedToHide: string[];
@@ -110,7 +110,7 @@ const SideBar = ({
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
position={data[0]?.pos}
id={data[0]?.id}
value={data[0]?.key}
isDuplicate={false}
blurred={false}
@@ -128,14 +128,14 @@ const SideBar = ({
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
position={data[0].pos}
id={data[0].id}
value={data[0]?.value}
isDuplicate={false}
blurred
/>
</div>
<div className="absolute bg-bunker-800 right-[1.07rem] top-[1.6rem] z-50">
<GenerateSecretMenu modifyValue={modifyValue} position={data[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValue} id={data[0]?.id} />
</div>
</div>
) : (
@@ -154,7 +154,7 @@ const SideBar = ({
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={modifyValueOverride}
pos={data[0]?.pos}
id={data[0]?.id}
/>
</div>
)}
@@ -167,14 +167,14 @@ const SideBar = ({
<DashboardInputField
onChangeHandler={modifyValueOverride}
type="value"
position={data[0]?.pos}
id={data[0]?.id}
value={overrideEnabled ? data[0]?.valueOverride : data[0]?.value}
isDuplicate={false}
blurred
/>
</div>
<div className="absolute right-[0.57rem] top-[0.3rem] z-50">
<GenerateSecretMenu modifyValue={modifyValueOverride} position={data[0]?.pos} />
<GenerateSecretMenu modifyValue={modifyValueOverride} id={data[0]?.id} />
</div>
</div>
</div>
@@ -182,7 +182,7 @@ const SideBar = ({
<CommentField
comment={data[0]?.comment}
modifyComment={modifyComment}
position={data[0]?.pos}
id={data[0]?.id}
/>
</div>
)}
+147
View File
@@ -0,0 +1,147 @@
import React, { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
/**
* 1st step of login - user enters their username and password
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {Function} obj.setEmail - function to set the email of user
* @param {String} obj.password - password of user
* @param {String} obj.setPassword - function to set the password of user
* @param {Function} obj.setStep - function to set the login flow step
* @returns
*/
export default function LoginStep ({
email,
setEmail,
password,
setPassword,
setStep
}: {
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (password: string) => void;
setStep: (step: number) => void;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { t } = useTranslation();
const handleLogin = async () => {
try {
if (!email || !password) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin(email, password);
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
}
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
{t('login:login')}
</p>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<InputField
label={t('common:email')}
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
autoComplete="username"
/>
</div>
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && loginError && <Error text={t('login:error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login:login') ?? ''}
onButtonPressed={async () => handleLogin()}
loading={isLoading}
size="lg"
/>
</div>
</div>
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t('common:maintenance-alert')}
</div>
)}
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
<p className="text-sm flex justify-center text-gray-400 w-max">
{t('login:need-account')}
</p>
<Link href="/signup">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:create-account')}
</button>
</Link>
</div>
</form>
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
+162
View File
@@ -0,0 +1,162 @@
/* eslint-disable react/jsx-props-no-spreading */
import React, { useState } from 'react';
import ReactCodeInput from 'react-code-input';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import attemptLoginMfa from '@app/components/utilities/attemptLoginMfa';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { useSendMfaToken } from '@app/hooks/api/auth';
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;
interface VerifyMfaTokenError {
response: {
data: {
context: {
code: string;
triesLeft: number;
}
},
status: number;
}
}
/**
* 2nd step of login - users enter their MFA code
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.password - password of user
* @param {Function} obj.setStep - function to set the login flow step
* @returns
*/
export default function MFAStep({
email,
password
}: {
email: string;
password: string;
}): JSX.Element {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isLoadingResend, setIsLoadingResend] = useState(false);
const [mfaCode, setMfaCode] = useState('');
const [triesLeft, setTriesLeft] = useState<number | undefined>(undefined);
const { t } = useTranslation();
const sendMfaToken = useSendMfaToken();
const handleLoginMfa = async () => {
try {
if (mfaCode.length !== 6) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLoginMfa({
email,
password,
mfaToken: mfaCode
});
if (isLoginSuccessful) {
setIsLoading(false);
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
}
} catch (err) {
const error = err as VerifyMfaTokenError;
if (error?.response?.status === 500) {
window.location.reload();
} else if (error?.response?.data?.context?.triesLeft) {
setTriesLeft(error?.response?.data?.context?.triesLeft);
if (error.response.data.context.triesLeft === 0) {
window.location.reload();
}
}
setIsLoading(false);
}
}
const handleResendMfaCode = async () => {
try {
setIsLoadingResend(true);
await sendMfaToken.mutateAsync({ email });
setIsLoadingResend(false);
} catch (err) {
console.error(err);
setIsLoadingResend(false);
}
}
return (
<form 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('mfa: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={setMfaCode}
{...props}
className="mt-6 mb-2"
/>
</div>
{typeof triesLeft === 'number' && <Error text={`${t('mfa:step2-code-error')} ${triesLeft}`} />}
<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('mfa:verify') ?? ''}
onButtonPressed={() => handleLoginMfa()}
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('mfa:step2-resend-alert')}</span>
<u
className={`font-normal ${
isLoadingResend
? 'text-bunker-400'
: 'text-primary-700 hover:text-primary duration-200'
}`}
>
<button disabled={isLoading} onClick={() => handleResendMfaCode()} type="button">
{isLoadingResend
? t('mfa:step2-resend-progress')
: t('mfa:step2-resend-submit')}
</button>
</u>
</div>
<p className="text-sm text-bunker-400 pb-2">{t('mfa:step2-spam-alert')}</p>
</div>
</form>
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'mfa']);
@@ -87,13 +87,12 @@ export default function Navbar() {
}, []);
const closeApp = async () => {
console.log('Logging out...');
await logout();
router.push('/login');
};
return (
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[61]">
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[71]">
<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">
@@ -166,7 +165,7 @@ export default function Navbar() {
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-[65] ring-opacity-5 focus:outline-none">
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[999] ring-opacity-5 focus:outline-none">
<div className="px-1 py-1 z-[100]">
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
{t('nav:user.signed-in-as')}
@@ -31,7 +31,7 @@ export default function DonwloadBackupPDFStep({
return (
<div className="bg-bunker flex flex-col items-center w-full max-w-xs md:max-w-lg h-7/12 py-8 px-4 md:px-6 mx-1 mb-36 md:mb-16 rounded-xl drop-shadow-xl">
<p className="text-4xl text-center font-semibold flex justify-center text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
<p className="text-4xl text-center font-semibold flex justify-center text-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">
@@ -42,7 +42,7 @@ export default function DonwloadBackupPDFStep({
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
{t('signup:step4-description3')}
</div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-4 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<Button
text="Download PDF"
onButtonPressed={async () => {
+93 -44
View File
@@ -1,5 +1,6 @@
import crypto from 'crypto';
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';
@@ -8,18 +9,21 @@ import nacl from 'tweetnacl';
import { encodeBase64 } from 'tweetnacl-util';
import completeAccountInformationSignup from '@app/pages/api/auth/CompleteAccountInformationSignup';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService';
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';
import { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
import SecurityClient from '../utilities/SecurityClient';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface UserInfoStepProps {
verificationToken: string;
incrementStep: () => void;
email: string;
password: string;
@@ -44,7 +48,6 @@ interface UserInfoStepProps {
* @param {string} obj.setLastName - function managing the state of user's last name
*/
export default function UserInfoStep({
verificationToken,
incrementStep,
email,
password,
@@ -62,7 +65,6 @@ export default function UserInfoStep({
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.
@@ -94,17 +96,9 @@ export default function UserInfoStep({
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: PRIVATE_KEY,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
}) as { ciphertext: string; iv: string; tag: string };
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = encodeBase64(publicKeyUint8Array);
localStorage.setItem('PRIVATE_KEY', privateKey);
client.init(
{
@@ -113,35 +107,90 @@ export default function UserInfoStep({
},
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
});
try {
// if everything works, go the main dashboard page.
if (response.status === 200) {
// response = await response.json();
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key from password');
localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
const response = await completeAccountInformationSignup({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
organizationName: `${firstName}'s organization`
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken('');
SecurityClient.setToken(response.token);
try {
await attemptLogin(email, password, () => {}, router, true, false);
incrementStep();
} catch (error) {
setIsLoading(false);
}
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]?._id;
const project = await ProjectService.initProject({
organizationId: orgId,
projectName: 'Example Project'
});
localStorage.setItem('orgData.id', orgId);
localStorage.setItem('projectData.id', project._id);
incrementStep();
} catch (error) {
setIsLoading(false);
console.error(error);
}
});
}
@@ -153,7 +202,7 @@ export default function UserInfoStep({
return (
<div className="bg-bunker w-max mx-auto h-7/12 py-10 px-8 rounded-xl drop-shadow-xl mb-36 md:mb-16">
<p className="text-4xl font-bold flex justify-center mb-6 text-gray-400 mx-8 md:mx-16 text-transparent bg-clip-text bg-gradient-to-br from-sky-400 to-primary">
<p className="text-4xl font-bold flex justify-center mb-6 mx-8 md:mx-16 text-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">
@@ -258,4 +307,4 @@ export default function UserInfoStep({
</div>
</div>
);
}
}
@@ -1,7 +1,19 @@
import { getAuthToken, setAuthToken } from '@app/reactQuery';
import {
getAuthToken,
setAuthToken,
setMfaTempToken,
setSignupTempToken} from '@app/reactQuery';
// depreciated: go for apiRequest module in config/api
export default class SecurityClient {
static setSignupToken(tokenStr: string) {
setSignupTempToken(tokenStr);
}
static setMfaToken(tokenStr: string) {
setMfaTempToken(tokenStr);
}
static setToken(tokenStr: string) {
setAuthToken(tokenStr);
}
+99 -189
View File
@@ -1,20 +1,12 @@
/* eslint-disable prefer-destructuring */
import crypto from 'crypto';
import jsrp from 'jsrp';
import { SecretDataProps } from 'public/data/frequentInterfaces';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import login1 from '@app/pages/api/auth/Login1';
import login2 from '@app/pages/api/auth/Login2';
import addSecrets from '@app/pages/api/files/AddSecrets';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import getUser from '@app/pages/api/user/getUser';
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
import KeyService from '@app/services/KeyService';
import { encryptAssymmetric } from './cryptography/crypto';
import encryptSecrets from './secrets/encryptSecrets';
import Telemetry from './telemetry/Telemetry';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
@@ -22,212 +14,130 @@ import SecurityClient from './SecurityClient';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface IsLoginSuccessful {
mfaEnabled: boolean;
success: boolean;
}
/**
* 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
* Return whether or not login is successful for user with email [email]
* and password [password]
* @param {string} email - email of user to log in
* @param {string} password - password of user to log in
*/
const attemptLogin = async (
email: string,
password: string,
setErrorLogin: (value: boolean) => void,
router: any,
isSignUp: boolean,
isLogin: boolean
) => {
try {
const telemetry = new Telemetry().getInstance();
password: string
): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
return new Promise((resolve, reject) => {
client.init(
{
username: email,
password
},
async () => {
const clientPublicKey = client.getPublicKey();
try {
const clientPublicKey = client.getPublicKey();
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(
const {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2(
email,
clientProof
);
if (mfaEnabled) {
// case: MFA is enabled
SecurityClient.setToken(token);
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(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',
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'Secret referencing example',
id: '',
tags: []
},
{
pos: 1,
key: 'DB_USERNAME',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Override secrets with personal value',
id: '',
tags: []
},
{
pos: 2,
key: 'DB_PASSWORD',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Another secret override',
id: '',
tags: []
},
{
pos: 3,
key: 'DB_USERNAME',
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: '',
tags: []
},
{
pos: 4,
key: 'DB_PASSWORD',
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: '',
tags: []
},
{
pos: 5,
key: 'TWILIO_AUTH_TOKEN',
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: '',
tags: []
},
{
pos: 6,
key: 'WEBSITE_URL',
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: '',
tags: []
}
];
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: String(localStorage.getItem('projectData.id')),
env: 'dev'
resolve({
mfaEnabled,
success: true
});
await addSecrets({
secrets: secrets ?? [],
env: 'dev',
workspaceId: String(localStorage.getItem('projectData.id'))
} else if (
!mfaEnabled &&
encryptionVersion &&
encryptedPrivateKey &&
iv &&
tag &&
token
) {
// case: MFA is not enabled
// set JWT token
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId);
const orgUserProjects = await getOrganizationUserProjects({
orgId
});
if (orgUserProjects.length > 0) {
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
}
if (email) {
telemetry.identify(email);
telemetry.capture('User Logged In');
}
resolve({
mfaEnabled: false,
success: true
});
}
if (isLogin) {
if (localStorage.getItem('projectData.id') !== "undefined") {
router.push(`/dashboard/${localStorage.getItem('projectData.id')}`);
} else {
router.push("/noprojects");
}
}
} catch (error) {
console.log(error);
setErrorLogin(true);
console.log('Login response not available');
} catch (err) {
reject(err);
}
}
);
} catch (error) {
console.log('Something went wrong during authentication');
}
return true;
});
};
export default attemptLogin;
export default attemptLogin;
@@ -0,0 +1,103 @@
/* eslint-disable prefer-destructuring */
import jsrp from 'jsrp';
import login1 from '@app/pages/api/auth/Login1';
import verifyMfaToken from '@app/pages/api/auth/verifyMfaToken';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import KeyService from '@app/services/KeyService';
import { saveTokenToLocalStorage } from './saveTokenToLocalStorage';
import SecurityClient from './SecurityClient';
// eslint-disable-next-line new-cap
const client = new jsrp.client();
/**
* Return whether or not MFA-login is successful for user with email [email]
* and MFA token [mfaToken]
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.mfaToken - MFA code/token
*/
const attemptLoginMfa = async ({
email,
password,
mfaToken
}: {
email: string;
password: string;
mfaToken: string;
}): Promise<Boolean> => {
return new Promise((resolve, reject) => {
client.init({
username: email,
password
}, async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1(email, clientPublicKey);
const {
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await verifyMfaToken({
email,
mfaToken
});
// unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken('');
SecurityClient.setToken(token);
const privateKey = await KeyService.decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
// TODO: in the future - move this logic elsewhere
// because this function is about logging the user in
// and not initializing the login details
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId);
const orgUserProjects = await getOrganizationUserProjects({
orgId
});
localStorage.setItem('projectData.id', orgUserProjects[0]._id);
resolve(true);
} catch (err) {
reject(err);
}
});
});
}
export default attemptLoginMfa;
@@ -9,14 +9,14 @@ const BLOCK_SIZE_BYTES = 16; // 128 bit
interface EncryptProps {
text: string;
secret: string;
secret: string | Buffer;
}
interface DecryptProps {
ciphertext: string;
iv: string;
tag: string;
secret: string;
secret: string | Buffer;
}
interface EncryptOutputProps {
@@ -1,10 +1,14 @@
/* eslint-disable new-cap */
import crypto from 'crypto';
import jsrp from 'jsrp';
import changePassword2 from '@app/pages/api/auth/ChangePassword2';
import SRP1 from '@app/pages/api/auth/SRP1';
import { saveTokenToLocalStorage } from '../saveTokenToLocalStorage';
import Aes256Gcm from './aes-256-gcm';
import { deriveArgonKey } from './crypto';
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
@@ -63,43 +67,70 @@ const changePassword = async (
},
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({
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key from password');
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = 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'
)
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
if (ciphertext) {
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
try {
await changePassword2({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
});
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 (error) {
setCurrentPasswordError(true);
console.log(error);
}
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
});
setPasswordChanged(true);
setCurrentPassword('');
setNewPassword('');
} catch (error) {
setCurrentPasswordError(true);
console.log(error);
}
});
}
@@ -1,3 +1,5 @@
import argon2 from 'argon2-browser';
import aes from './aes-256-gcm';
const nacl = require('tweetnacl');
@@ -9,6 +11,50 @@ type EncryptAsymmetricProps = {
privateKey: string;
};
/**
* Derive a key from password [password] and salt [salt] using Argon2id
* @param {Object} obj
* @param {String} obj.password - password to derive key from
* @param {String} obj.salt - salt to derive key from
* @param {Number} obj.mem - used memory, in KiB
* @param {Number} obj.time - number of iterations
* @param {Number} obj.parallelism - desired parallelism
* @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
* @returns
*/
const deriveArgonKey = async ({
password,
salt,
mem,
time,
parallelism,
hashLen
}: {
password: string;
salt: string;
mem: number;
time: number;
parallelism: number;
hashLen: number;
}) => {
let derivedKey;
try {
derivedKey = await argon2.hash({
pass: password,
salt,
type: argon2.ArgonType.Argon2id,
mem,
time,
parallelism,
hashLen
});
} catch (err) {
console.error(err);
}
return derivedKey;
}
/**
* Return assymmetrically encrypted [plaintext] using [publicKey] where
* [publicKey] likely belongs to the recipient.
@@ -138,4 +184,10 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps):
return plaintext;
};
export { decryptAssymmetric, decryptSymmetric, encryptAssymmetric, encryptSymmetric };
export {
decryptAssymmetric,
decryptSymmetric,
deriveArgonKey,
encryptAssymmetric,
encryptSymmetric
};
@@ -1,12 +1,18 @@
interface Props {
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
privateKey: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
privateKey?: string;
}
export const saveTokenToLocalStorage = ({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
@@ -14,11 +20,46 @@ export const saveTokenToLocalStorage = ({
privateKey,
}: Props) => {
try {
localStorage.setItem("publicKey", publicKey);
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
localStorage.setItem("iv", iv);
localStorage.setItem("tag", tag);
localStorage.setItem("PRIVATE_KEY", privateKey);
if (protectedKey) {
localStorage.removeItem("protectedKey");
localStorage.setItem("protectedKey", protectedKey);
}
if (protectedKeyIV) {
localStorage.removeItem("protectedKeyIV");
localStorage.setItem("protectedKeyIV", protectedKeyIV);
}
if (protectedKeyTag) {
localStorage.removeItem("protectedKeyTag");
localStorage.setItem("protectedKeyTag", protectedKeyTag);
}
if (publicKey) {
localStorage.removeItem("publicKey");
localStorage.setItem("publicKey", publicKey);
}
if (encryptedPrivateKey) {
localStorage.removeItem("encryptedPrivateKey");
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
}
if (iv) {
localStorage.removeItem("iv");
localStorage.setItem("iv", iv);
}
if (tag) {
localStorage.removeItem("tag");
localStorage.setItem("tag", tag);
}
if (privateKey) {
localStorage.removeItem("PRIVATE_KEY");
localStorage.setItem("PRIVATE_KEY", privateKey);
}
} catch (err) {
if (err instanceof Error) {
throw new Error(
+8 -3
View File
@@ -3,14 +3,19 @@ import { twMerge } from 'tailwind-merge';
export type CardTitleProps = {
children: ReactNode;
subTitle?: string;
subTitle?: ReactNode;
className?: string;
};
export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
<div className={twMerge('px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600', className)}>
<div
className={twMerge(
'px-6 py-4 mb-5 font-sans text-lg font-normal border-b border-mineshaft-600',
className
)}
>
{children}
{subTitle && <p className="pt-0.5 text-sm font-normal text-gray-400">{subTitle}</p>}
{subTitle && <p className="pt-2 text-sm font-normal text-gray-400">{subTitle}</p>}
</div>
);
@@ -5,7 +5,7 @@ import { useToggle } from '@app/hooks';
import { Button } from '../Button';
import { FormControl } from '../FormControl';
import { Input } from '../Input';
import { Modal, ModalContent } from '../Modal';
import { Modal, ModalClose, ModalContent } from '../Modal';
type Props = {
isOpen?: boolean;
@@ -64,9 +64,11 @@ export const DeleteActionModal = ({
>
Delete
</Button>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
</ModalClose>{' '}
</div>
}
onClose={onClose}
+5 -5
View File
@@ -12,7 +12,7 @@ type Props = {
};
const inputVariants = cva(
'input w-full py-2 text-gray-400 placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
'input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2',
{
variants: {
size: {
@@ -84,19 +84,19 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
): JSX.Element => {
return (
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
{leftIcon && <span className="absolute left-0 ml-2">{leftIcon}</span>}
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
<input
{...props}
required={isRequired}
ref={ref}
disabled={isDisabled}
className={twMerge(
leftIcon ? 'pl-9' : 'pl-2.5',
rightIcon ? 'pr-9' : 'pr-2.5',
leftIcon ? 'pl-10' : 'pl-2.5',
rightIcon ? 'pr-10' : 'pr-2.5',
inputVariants({ className, isError, size, isRounded, variant })
)}
/>
{rightIcon && <span className="absolute right-0 mr-2">{rightIcon}</span>}
{rightIcon && <span className="absolute right-0 mr-3">{rightIcon}</span>}
</div>
);
}
+1 -1
View File
@@ -9,7 +9,7 @@ import { IconButton } from '../IconButton';
export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode;
subTitle?: string;
subTitle?: ReactNode;
footerContent?: ReactNode;
onClose?: () => void;
};
@@ -5,13 +5,13 @@ import * as Popover from '@radix-ui/react-popover';
type Props = {
children: any;
text: string;
onChangeHandler: (value: string, position: number) => void;
position: number;
onChangeHandler: (value: string, id: string) => void;
id: string;
};
export type PopoverProps = Props;
export const PopoverObject = ({children, text, onChangeHandler, position}: Props) => (
export const PopoverObject = ({children, text, onChangeHandler, id}: Props) => (
<Popover.Root>
<Popover.Trigger asChild className='data-[state=open]:outline data-[state=open]:outline-primary data-[state=closed]:hover:outline data-[state=closed]:hover:outline-mineshaft-400'>
{children}
@@ -26,7 +26,7 @@ export const PopoverObject = ({children, text, onChangeHandler, position}: Props
<div className="flex flex-col pt-2 dark">
<p className="text-bunker-200 text-[15px] leading-[0px] font-medium mb-5">Comment</p>
<textarea
onChange={(e) => onChangeHandler(e.target.value, position)}
onChange={(e) => onChangeHandler(e.target.value, id)}
// type={type}
value={text}
className='z-10 dark:[color-scheme:dark] peer h-[20rem] ph-no-capture bg-bunker-600 border border-mineshaft-500 rounded-md py-2.5 caret-bunker-200 text-sm px-2 w-full outline-none text-bunker-300 focus:text-bunker-100 placeholder:text-bunker-400 placeholder:focus:text-transparent placeholder duration-200'
+20 -9
View File
@@ -14,18 +14,28 @@ type Props = {
dropdownContainerClassName?: string;
isLoading?: boolean;
position?: 'item-aligned' | 'popper';
isDisabled?: boolean;
icon?: IconProp;
};
export type SelectProps = SelectPrimitive.SelectProps & Props;
export type SelectProps = Omit<SelectPrimitive.SelectProps, 'disabled'> & Props;
export const Select = forwardRef<HTMLButtonElement, SelectProps>(
(
{ children, placeholder, className, isLoading, dropdownContainerClassName, position, ...props },
{
children,
placeholder,
className,
isLoading,
isDisabled,
dropdownContainerClassName,
position,
...props
},
ref
): JSX.Element => {
return (
<SelectPrimitive.Root {...props}>
<SelectPrimitive.Root {...props} disabled={isDisabled}>
<SelectPrimitive.Trigger
ref={ref}
className={twMerge(
@@ -34,10 +44,10 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
className
)}
>
<SelectPrimitive.Value placeholder={placeholder}>
<SelectPrimitive.Value placeholder={placeholder}>
{props.icon ? <FontAwesomeIcon icon={props.icon} /> : placeholder}
</SelectPrimitive.Value>
{!props.disabled && (
{!isDisabled && (
<SelectPrimitive.Icon className="ml-3">
<FontAwesomeIcon icon={faChevronDown} size="sm" />
</SelectPrimitive.Icon>
@@ -46,7 +56,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Portal>
<SelectPrimitive.Content
className={twMerge(
'relative top-1 overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md z-[100]',
'relative top-1 z-[100] overflow-hidden rounded-md bg-bunker-800 font-inter text-bunker-100 shadow-md',
dropdownContainerClassName
)}
position={position}
@@ -89,11 +99,12 @@ export const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(
<SelectPrimitive.Item
{...props}
className={twMerge(
`relative flex cursor-pointer
select-none items-center rounded-md py-2 pl-10 pr-4 mb-0.5 text-sm
`relative mb-0.5 flex
cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
outline-none transition-all hover:bg-mineshaft-500`,
isSelected && 'bg-primary',
isDisabled && 'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
isDisabled &&
'cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600',
className
)}
ref={forwardedRef}
+159
View File
@@ -0,0 +1,159 @@
import { useState } from 'react';
import { faArrowUpRightFromSquare, faCheck, faClipboard } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as Tabs from '@radix-ui/react-tabs';
export type TabsProps = Tabs.TabsProps & {};
function copyToClipboard(id: string, setState: (value: boolean) => void) {
// Get the text field
const copyText = document.getElementById(id) 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);
setState(true);
setTimeout(() => setState(false), 2000);
// Alert the copied text
// alert("Copied the text: " + copyText.value);
}
const CodeItem = ({isCopied, setIsCopied, textExplanation, code, id}: {isCopied: boolean, setIsCopied: (value: boolean) => void, textExplanation: string, code: string, id: string}) => {
return <>
<p className="mb-2 mt-4 text-bunker-300 text-sm leading-normal">
{textExplanation}
</p>
<div className='font-mono text-sm px-3 py-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
<input disabled value={code} id={id} className='w-full bg-transparent text-bunker-200'/>
<button
type="button"
onClick={() => copyToClipboard(id, setIsCopied)}
className='h-full pl-3.5 pr-2 text-bunker-300 hover:text-primary-200 duration-200'
>
{isCopied ? (
<FontAwesomeIcon
icon={faCheck}
className='pr-0.5'
/>
) : (
<FontAwesomeIcon icon={faClipboard} />
)}
</button>
</div>
</>
}
export const TabsObject = () => {
const [downloadCodeCopied, setDownloadCodeCopied] = useState(false);
const [downloadCode2Copied, setDownloadCode2Copied] = useState(false);
const [loginCodeCopied, setLoginCodeCopied] = useState(false);
const [initCodeCopied, setInitCodeCopied] = useState(false);
const [runCodeCopied, setRunCodeCopied] = useState(false);
return <Tabs.Root
className="flex flex-col w-full cursor-default border border-mineshaft-600 rounded-md"
defaultValue="tab1"
>
<Tabs.List className="shrink-0 flex border-b border-mineshaft-600" aria-label="Manage your account">
<Tabs.Trigger
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
value="tab1"
>
MacOS
</Tabs.Trigger>
<Tabs.Trigger
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
value="tab2"
>
Windows
</Tabs.Trigger>
<Tabs.Trigger
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
value="tab3"
>
Arch Linux
</Tabs.Trigger>
<a
target='_blank'
rel="noopener noreferrer"
className="bg-bunker-700 hover:text-bunker-100 duration-200 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
href='https://infisical.com/docs/cli/overview'
>
Other Platforms <FontAwesomeIcon icon={faArrowUpRightFromSquare} className='ml-2'/>
</a>
</Tabs.List>
<Tabs.Content
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
value="tab1"
>
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCode" />
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCode" />
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCode" />
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCode" />
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
className='text-primary underline underline-offset-2'
target="_blank"
rel="noopener noreferrer"
href='https://infisical.com/docs/integrations/overview'
>
here
</a>. </p>
</Tabs.Content>
<Tabs.Content
className="grow p-5 bg-bunker-700 rounded-b-md outline-none"
value="tab2"
>
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeW" />
<div className='font-mono text-sm px-3 py-2 mt-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
<input disabled value="scoop install infisical" id="downloadCodeW2" className='w-full bg-transparent text-bunker-200'/>
<button
type="button"
onClick={() => copyToClipboard("downloadCodeW2", setDownloadCode2Copied)}
className='h-full pl-3.5 pr-2 text-bunker-300 hover:text-primary-200 duration-200'
>
{downloadCode2Copied ? (
<FontAwesomeIcon
icon={faCheck}
className='pr-0.5'
/>
) : (
<FontAwesomeIcon icon={faClipboard} />
)}
</button>
</div>
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeW" />
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeW" />
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeW" />
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
className='text-primary underline underline-offset-2'
target="_blank"
rel="noopener noreferrer"
href='https://infisical.com/docs/integrations/overview'
>
here
</a>. </p>
</Tabs.Content>
<Tabs.Content
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
value="tab3"
>
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeL" />
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeL" />
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeL" />
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeL" />
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
className='text-primary underline underline-offset-2'
target="_blank"
rel="noopener noreferrer"
href='https://infisical.com/docs/integrations/overview'
>
here
</a>. </p>
</Tabs.Content>
</Tabs.Root>
};
@@ -0,0 +1,4 @@
export type {
TabsProps
} from './Tabs';
export { TabsObject } from './Tabs';
+24
View File
@@ -0,0 +1,24 @@
import { ReactNode } from 'react';
import { cva, VariantProps } from 'cva';
import { twMerge } from 'tailwind-merge';
type Props = {
children: ReactNode;
className?: string;
} & VariantProps<typeof tagVariants>;
const tagVariants = cva('inline-flex whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 ', {
variants: {
colorSchema: {
gray: 'bg-mineshaft-500',
red: 'bg-red/80 text-bunker-100'
},
size: {
sm: 'px-1 py-0.5'
}
}
});
export const Tag = ({ children, className, colorSchema = 'gray', size = 'sm' }: Props) => (
<div className={twMerge(tagVariants({ colorSchema, className, size }))}>{children}</div>
);
+1
View File
@@ -0,0 +1 @@
export { Tag } from './Tag';
+1
View File
@@ -12,5 +12,6 @@ export * from './Select';
export * from './Spinner';
export * from './Switch';
export * from './Table';
export * from './Tag';
export * from './TextArea';
export * from './UpgradePlanModal';
+14 -2
View File
@@ -1,6 +1,9 @@
import axios from 'axios';
import { getAuthToken } from '@app/reactQuery';
import {
getAuthToken,
getMfaTempToken,
getSignupTempToken} from '@app/reactQuery';
export const apiRequest = axios.create({
baseURL: '/',
@@ -10,8 +13,17 @@ export const apiRequest = axios.create({
});
apiRequest.interceptors.request.use((config) => {
const signupTempToken = getSignupTempToken();
const mfaTempToken = getMfaTempToken();
const token = getAuthToken();
if (token && config.headers) {
if (signupTempToken && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${signupTempToken}`;
} else if (mfaTempToken && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${mfaTempToken}`;
} else if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
+1 -1
View File
@@ -45,4 +45,4 @@ const plansProd: Mapping = {
export const plans = plansProd || plansDev;
export const leaveConfirmDefaultMessage = 'Do you want to save your results before leaving this page?';
export const leaveConfirmDefaultMessage = 'Your changes will be lost if you leave the page. Are you sure you want to continue?';
@@ -102,7 +102,9 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
});
}
const decryptedSecretVersions = secretSnapshotData.secretVersions.map(
const decryptedSecretVersions = secretSnapshotData.secretVersions.filter(
(sv: EncrypetedSecretVersionListProps) => (sv.type !== undefined && sv.environment !== undefined)
).map(
(encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
id: encryptedSecretVersion._id,
pos,
@@ -125,10 +127,13 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
);
const secretKeys = [
...new Set(decryptedSecretVersions.map((secret: SecretDataProps) => secret.key))
...new Set(decryptedSecretVersions.filter((dsv: any) => dsv.type !== undefined || dsv.environemnt !== undefined)
.map((secret: SecretDataProps) => secret.key))
];
const result = secretKeys.map((key, index) => ({
const result = secretKeys.map((key, index) => (decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0]?.id ? {
id: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'shared'
)[0].id,
@@ -146,6 +151,24 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
valueOverride: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
)[0]?.value
} : {
id: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
)[0].id,
pos: index,
key,
environment: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
)[0].environment,
tags: decryptedSecretVersions.filter(
(secret: SecretDataProps) => secret.key === key && secret.type === 'personal'
)[0].tags,
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({
@@ -161,7 +184,7 @@ const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }:
<div
className={`absolute border-l border-mineshaft-500 w-full min-w-sm max-w-sm ${
isLoading ? 'bg-bunker-800' : 'bg-bunker'
} fixed h-full right-0 z-[70] shadow-xl flex flex-col justify-between sticky top-0`}
} fixed h-full right-0 z-[40] shadow-xl flex flex-col justify-between sticky top-0`}
>
{isLoading ? (
<div className="flex items-center justify-center h-full mb-8">
+85
View File
@@ -0,0 +1,85 @@
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
/**
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
*/
const decryptPrivateKeyHelper = async ({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) => {
let privateKey;
try {
if (encryptionVersion === 1) {
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: password
.slice(0, 32)
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
});
} else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) {
const derivedKey = await deriveArgonKey({
password,
salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to generate derived key');
const key = Aes256Gcm.decrypt({
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag,
secret: Buffer.from(derivedKey.hash)
});
// decrypt back the private key
privateKey = Aes256Gcm.decrypt({
ciphertext: encryptedPrivateKey,
iv,
tag,
secret: Buffer.from(key, 'hex')
});
} else {
throw new Error('Insufficient details to decrypt private key');
}
} catch (err) {
console.error(err);
throw new Error('Failed to decrypt private key');
}
return privateKey;
}
export { decryptPrivateKeyHelper };
+140
View File
@@ -0,0 +1,140 @@
import crypto from 'crypto';
import { encryptAssymmetric } from '@app/components/utilities/cryptography/crypto';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import addSecrets from '@app/pages/api/files/AddSecrets';
import getUser from '@app/pages/api/user/getUser';
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
import uploadKeys from '@app/pages/api/workspace/uploadKeys';
const secretsToBeAdded = [
{
pos: 0,
key: 'DATABASE_URL',
// eslint-disable-next-line no-template-curly-in-string
value: 'mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net',
valueOverride: undefined,
comment: 'Secret referencing example',
id: '',
tags: []
},
{
pos: 1,
key: 'DB_USERNAME',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Override secrets with personal value',
id: '',
tags: []
},
{
pos: 2,
key: 'DB_PASSWORD',
value: 'OVERRIDE_THIS',
valueOverride: undefined,
comment:
'Another secret override',
id: '',
tags: []
},
{
pos: 3,
key: 'DB_USERNAME',
value: 'user1234',
valueOverride: 'user1234',
comment: '',
id: '',
tags: []
},
{
pos: 4,
key: 'DB_PASSWORD',
value: 'example_password',
valueOverride: 'example_password',
comment: '',
id: '',
tags: []
},
{
pos: 5,
key: 'TWILIO_AUTH_TOKEN',
value: 'example_twillio_token',
valueOverride: undefined,
comment: '',
id: '',
tags: []
},
{
pos: 6,
key: 'WEBSITE_URL',
value: 'http://localhost:3000',
valueOverride: undefined,
comment: '',
id: '',
tags: []
}
];
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
const initProjectHelper = async ({
organizationId,
projectName
}: {
organizationId: string;
projectName: string;
}) => {
let project;
try {
// create new project
project = await createWorkspace({
workspaceName: projectName,
organizationId
});
// create and upload new (encrypted) project key
const randomBytes = crypto.randomBytes(16).toString('hex');
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
if (!PRIVATE_KEY) throw new Error('Failed to find private key');
const user = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: user.publicKey,
privateKey: PRIVATE_KEY
});
await uploadKeys(project._id, user._id, ciphertext, nonce);
// encrypt and upload secrets to new project
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: project._id,
env: 'dev'
});
await addSecrets({
secrets: secrets ?? [],
env: 'dev',
workspaceId: project._id
});
} catch (err) {
console.error('Failed to init project in organization', err);
}
return project;
}
export {
initProjectHelper
}
+4 -1
View File
@@ -1 +1,4 @@
export { useGetAuthToken } from './queries';
export {
useGetAuthToken,
useSendMfaToken,
useVerifyMfaToken} from './queries'
+27 -2
View File
@@ -1,14 +1,39 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery';
import { GetAuthTokenAPI } from './types';
import {
GetAuthTokenAPI,
SendMfaTokenDTO,
VerifyMfaTokenDTO,
VerifyMfaTokenRes} from './types';
const authKeys = {
getAuthToken: ['token'] as const
};
export const useSendMfaToken = () => {
return useMutation<{}, {}, SendMfaTokenDTO>({
mutationFn: async ({ email }) => {
const { data } = await apiRequest.post('/api/v2/auth/mfa/send', { email });
return data;
}
});
}
export const useVerifyMfaToken = () => {
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
mutationFn: async ({ email, mfaCode }) => {
const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', {
email,
mfaToken: mfaCode
});
return data;
}
});
}
// Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls
const fetchAuthToken = async () => {
+21
View File
@@ -1,3 +1,24 @@
export type GetAuthTokenAPI = {
token: string;
};
export type SendMfaTokenDTO = {
email: string;
}
export type VerifyMfaTokenDTO = {
email: string;
mfaCode: string;
}
export type VerifyMfaTokenRes = {
encryptionVersion: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
}
@@ -0,0 +1,5 @@
export {
useAddIncidentContact,
useDeleteIncidentContact,
useGetOrgIncidentContact
} from './queries';
@@ -0,0 +1,57 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { AddIncidentContactDTO, DeleteIncidentContactDTO, IncidentContact } from './types';
const incidentContactKeys = {
getAllContact: (orgId: string) => ['org-incident-contacts', { orgId }] as const
};
const fetchOrgIncidentContacts = async (orgId: string) => {
const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>(
`/api/v1/organization/${orgId}/incidentContactOrg`
);
return data.incidentContactsOrg;
};
export const useGetOrgIncidentContact = (orgId: string) =>
useQuery({
queryKey: incidentContactKeys.getAllContact(orgId),
queryFn: () => fetchOrgIncidentContacts(orgId),
enabled: Boolean(orgId)
});
// mutation
export const useAddIncidentContact = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, AddIncidentContactDTO>({
mutationFn: async ({ orgId, email }) => {
const { data } = await apiRequest.post(`/api/v1/organization/${orgId}/incidentContactOrg`, {
email
});
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
}
});
};
export const useDeleteIncidentContact = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteIncidentContactDTO>({
mutationFn: async ({ orgId, email }) => {
const { data } = await apiRequest.delete(`/api/v1/organization/${orgId}/incidentContactOrg`, {
data: { email }
});
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
}
});
};
@@ -0,0 +1,18 @@
export type IncidentContact = {
_id: string;
email: string;
organization: string;
__v: number;
createdAt: Date;
updatedAt: Date;
};
export type DeleteIncidentContactDTO = {
orgId: string;
email: string;
};
export type AddIncidentContactDTO = {
orgId: string;
email: string;
};
+1
View File
@@ -1,4 +1,5 @@
export * from './auth';
export * from './incidentContacts';
export * from './keys';
export * from './organization';
export * from './serviceTokens';
+1 -1
View File
@@ -1 +1 @@
export { useGetOrganization } from './queries';
export { useGetOrganization, useRenameOrg } from './queries';
@@ -1,8 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiRequest } from '@app/config/request';
import { Organization } from './types';
import { Organization, RenameOrgDTO } from './types';
const organizationKeys = {
getUserOrganization: ['organization'] as const
@@ -16,3 +16,16 @@ const fetchUserOrganization = async () => {
export const useGetOrganization = () =>
useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization });
// mutation
export const useRenameOrg = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, RenameOrgDTO>({
mutationFn: ({ newOrgName, orgId }) =>
apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }),
onSuccess: () => {
queryClient.invalidateQueries(organizationKeys.getUserOrganization);
}
});
};
@@ -4,3 +4,8 @@ export type Organization = {
createAt: string;
updatedAt: string;
};
export type RenameOrgDTO = {
orgId: string;
newOrgName: string;
};
+4 -1
View File
@@ -1,14 +1,17 @@
export type { GetAuthTokenAPI } from './auth/types';
export type { IncidentContact } from './incidentContacts/types';
export type { UserWsKeyPair } from './keys/types';
export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
export type { User } from './users/types';
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
export type {
CreateEnvironmentDTO,
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
Workspace,
WorkspaceEnv,
+4 -1
View File
@@ -1,7 +1,10 @@
export {
fetchOrgUsers,
useAddUserToOrg,
useAddUserToWs,
useDeleteOrgMembership,
useGetOrgUsers,
useGetUser,
useLogoutUser
useLogoutUser,
useUpdateOrgUserRole
} from './queries';
+56 -3
View File
@@ -1,4 +1,4 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
decryptAssymmetric,
@@ -8,7 +8,15 @@ import { apiRequest } from '@app/config/request';
import { setAuthToken } from '@app/reactQuery';
import { useUploadWsKey } from '../keys/queries';
import { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './types';
import {
AddUserToOrgDTO,
AddUserToWsDTO,
AddUserToWsRes,
DeletOrgMembershipDTO,
OrgUser,
UpdateOrgUserRoleDTO,
User
} from './types';
const userKeys = {
getUser: ['user'] as const,
@@ -32,7 +40,11 @@ export const fetchOrgUsers = async (orgId: string) => {
};
export const useGetOrgUsers = (orgId: string) =>
useQuery(userKeys.getOrgUsers(orgId), () => fetchOrgUsers(orgId));
useQuery({
queryKey: userKeys.getOrgUsers(orgId),
queryFn: () => fetchOrgUsers(orgId),
enabled: Boolean(orgId)
});
// mutation
export const useAddUserToWs = () => {
@@ -69,6 +81,47 @@ export const useAddUserToWs = () => {
});
};
export const useAddUserToOrg = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, AddUserToOrgDTO>({
mutationFn: (dto) => apiRequest.post(`/api/v1/invite-org/signup`, dto),
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
}
});
};
export const useDeleteOrgMembership = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeletOrgMembershipDTO>({
mutationFn: ({ membershipId, orgId }) =>
apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`),
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
}
});
};
export const useUpdateOrgUserRole = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
mutationFn: ({ organizationId, membershipId, role }) =>
apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
role
}),
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
},
// to remove old states
onError: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
}
});
};
export const useLogoutUser = () =>
useMutation({
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
+31 -7
View File
@@ -1,15 +1,23 @@
import { UserWsKeyPair } from '../keys/types';
export type User = {
seenIps: string[];
_id: string;
email: string;
createdAt: Date;
updatedAt: Date;
__v: number;
firstName: string;
lastName: string;
email?: string;
firstName?: string;
lastName?: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
isMfaEnabled: boolean;
seenIps: string[];
_id: string;
__v: number;
};
export type OrgUser = {
@@ -24,7 +32,7 @@ export type OrgUser = {
inviteEmail: string;
organization: string;
role: 'owner' | 'admin' | 'member';
status: 'invited' | 'accepted';
status: 'invited' | 'accepted' | 'verified' | 'completed';
deniedPermissions: any[];
};
@@ -37,3 +45,19 @@ export type AddUserToWsRes = {
invitee: OrgUser['user'];
latestKey: UserWsKeyPair;
};
export type UpdateOrgUserRoleDTO = {
organizationId: string;
membershipId: string;
role: string;
};
export type DeletOrgMembershipDTO = {
membershipId: string;
orgId: string;
};
export type AddUserToOrgDTO = {
inviteeEmail: string;
organizationId: string;
};
+1 -1
View File
@@ -3,9 +3,9 @@ export {
useCreateWsEnvironment,
useDeleteWorkspace,
useDeleteWsEnvironment,
useGetUserWorkspaceMemberships,
useGetUserWorkspaces,
useGetWorkspaceById,
useRenameWorkspace,
useToggleAutoCapitalization,
useUpdateWsEnvironment} from './queries';
+24 -5
View File
@@ -13,16 +13,18 @@ import {
Workspace
} from './types';
const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
getAllUserWorkspace: ['workspaces'] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ workspace: Workspace }>(`/api/v1/workspace/${workspaceId}`);
return data.workspace;
}
const { data } = await apiRequest.get<{ workspace: Workspace }>(
`/api/v1/workspace/${workspaceId}`
);
return data.workspace;
};
const fetchUserWorkspaces = async () => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>('/api/v1/workspace');
@@ -40,6 +42,21 @@ export const useGetWorkspaceById = (workspaceId: string) => {
export const useGetUserWorkspaces = () =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces);
const fetchUserWorkspaceMemberships = async (orgId: string) => {
const { data } = await apiRequest.get<Record<string, Workspace[]>>(
`/api/v1/organization/${orgId}/workspace-memberships`
);
return data;
};
// to get all userids in an org with the workspace they are part of
export const useGetUserWorkspaceMemberships = (orgId: string) =>
useQuery({
queryKey: workspaceKeys.getWorkspaceMemberships(orgId),
queryFn: () => fetchUserWorkspaceMemberships(orgId),
enabled: Boolean(orgId)
});
// mutation
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
@@ -70,7 +87,9 @@ export const useToggleAutoCapitalization = () => {
return useMutation<{}, {}, ToggleAutoCapitalizationDTO>({
mutationFn: ({ workspaceID, state }) =>
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, { autoCapitalization: state }),
apiRequest.patch(`/api/v2/workspace/${workspaceID}/auto-capitalization`, {
autoCapitalization: state
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
+1 -1
View File
@@ -1,3 +1,3 @@
export { useLeaveConfirm } from './useLeaveConfirm';
export { usePopUp } from './usePopUp';
export { useToggle } from './useToggle';
export { useLeaveConfirm } from './useLeaveConfirm';
+2 -1
View File
@@ -93,6 +93,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
// Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched
useEffect(() => {
// Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
@@ -226,7 +227,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
<Select
defaultValue={currentWorkspace?._id}
value={currentWorkspace?._id}
className="w-full py-2.5 bg-mineshaft-600 font-medium"
className="w-full py-2.5 bg-mineshaft-600 font-medium truncate"
onValueChange={(value) => {
router.push(`/dashboard/${value}`);
}}
@@ -75,8 +75,18 @@ export const Navbar = () => {
const closeApp = async () => {
try {
console.log('Logging out...');
console.log('Logging out...')
await logout.mutateAsync();
localStorage.removeItem('protectedKey');
localStorage.removeItem('protectedKeyIV');
localStorage.removeItem('protectedKeyTag');
localStorage.removeItem('publicKey');
localStorage.removeItem('encryptedPrivateKey');
localStorage.removeItem('iv');
localStorage.removeItem('tag');
localStorage.removeItem('PRIVATE_KEY');
localStorage.removeItem('orgData.id');
localStorage.removeItem('projectData.id');
router.push('/login');
} catch (error) {
console.error(error);
+31 -24
View File
@@ -1,12 +1,15 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
import { apiRequest } from "@app/config/request";
interface Props {
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
clientProof: string;
}
/**
@@ -14,26 +17,30 @@ interface Props {
* @param {*} clientPublicKey
* @returns
*/
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
SecurityClient.fetchCall('/api/v1/password/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
clientProof,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log('Failed to change the password');
return undefined;
const changePassword2 = async ({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
}: Props) => {
const { data } = await apiRequest.post('/api/v1/password/change-password', {
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
return data;
}
export default changePassword2;
@@ -1,15 +1,20 @@
import { apiRequest } from "@app/config/request";
interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
ciphertext: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
organizationName: string;
iv: string;
tag: string;
salt: string;
verifier: string;
token: string;
}
/**
@@ -19,6 +24,9 @@ interface Props {
* @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.protectedKey - protected key in encryption version 2
* @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2
* @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2
* @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
@@ -26,39 +34,40 @@ interface Props {
* @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 = ({
const completeAccountInformationSignup = async ({
email,
firstName,
lastName,
organizationName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
ciphertext,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
token
}: Props) => 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
})
organizationName
}: Props) => {
const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName
});
return data;
}
export default completeAccountInformationSignup;
@@ -1,16 +1,24 @@
import { apiRequest } from "@app/config/request";
interface Props {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
ciphertext: string;
iv: string;
tag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
token: string;
}
// missing token?
// TODO: add to SecurityClient
/**
* This function is called in the end of the signup process.
* It sends all the necessary nformation to the server.
@@ -27,34 +35,36 @@ interface Props {
* @param {string} obj.token - token that confirms a user's identity
* @returns
*/
const completeAccountInformationSignupInvite = ({
const completeAccountInformationSignupInvite = async ({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
ciphertext,
iv,
tag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
token
}: Props) => fetch('/api/v1/signup/complete-account/invite', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ token}`
},
body: JSON.stringify({
email,
firstName,
lastName,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
salt,
verifier
})
verifier
}: Props) => {
const { data } = await apiRequest.post('/api/v2/signup/complete-account/invite', {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
return data;
}
export default completeAccountInformationSignupInvite;
+2 -2
View File
@@ -10,7 +10,7 @@ interface Login1 {
* @returns
*/
const login1 = async (email: string, clientPublicKey: string) => {
const response = await fetch("/api/v1/auth/login1", {
const response = await fetch("/api/v2/auth/login1", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -25,7 +25,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
const data = (await response.json()) as unknown as Login1;
return data;
}
throw new Error("Wrong password");
};
+10 -5
View File
@@ -1,9 +1,14 @@
interface Login2Response {
encryptedPrivateKey: string;
iv: string;
publicKey: string;
tag: string;
mfaEnabled: boolean;
token: string;
encryptionVersion?: number;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
/**
@@ -13,7 +18,7 @@ interface Login2Response {
* @returns
*/
const login2 = async (email: string, clientProof: string) => {
const response = await fetch('/api/v1/auth/login2', {
const response = await fetch('/api/v2/auth/login2', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
+28 -17
View File
@@ -4,27 +4,38 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
* This route logs the user out. Note: the user should authorized to do this.
* We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry
*/
const logout = async () =>
SecurityClient.fetchCall('/api/v1/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
}).then((res) => {
const logout = async () => {
try {
const res = await SecurityClient.fetchCall('/api/v1/auth/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (res?.status === 200) {
SecurityClient.setToken('');
// Delete the cookie by not setting a value; Alternatively clear the local storage
localStorage.setItem('publicKey', '');
localStorage.setItem('encryptedPrivateKey', '');
localStorage.setItem('iv', '');
localStorage.setItem('tag', '');
localStorage.setItem('PRIVATE_KEY', '');
console.log('User logged out', res);
localStorage.removeItem('protectedKey');
localStorage.removeItem('protectedKeyIV');
localStorage.removeItem('protectedKeyTag');
localStorage.removeItem('publicKey');
localStorage.removeItem('encryptedPrivateKey');
localStorage.removeItem('iv');
localStorage.removeItem('tag');
localStorage.removeItem('PRIVATE_KEY');
localStorage.removeItem('orgData.id');
localStorage.removeItem('projectData.id');
return res;
}
console.log('Failed to log out');
return undefined;
});
} catch (error) {
console.log('Error logging out', error);
}
return undefined;
};
export default logout;
@@ -1,10 +1,13 @@
interface Props {
verificationToken: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
iv: string;
tag: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
verificationToken: string;
}
/**
@@ -19,22 +22,28 @@ interface Props {
* @returns
*/
const resetPasswordOnAccountRecovery = ({
verificationToken,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
verifier,
verificationToken,
}: Props) => fetch('/api/v1/password/password-reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ verificationToken}`
Authorization: `Bearer ${verificationToken}`
},
body: JSON.stringify({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv,
tag,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
})
@@ -0,0 +1,25 @@
import { apiRequest } from "@app/config/request";
/**
* Verify MFA token [mfaToken] for user with email [email]
* @param {object} obj
* @param {string} obj.email - email of user
* @param {string} obj.mfaToken - MFA cod/token to verify
* @returns
*/
const verifyMfaToken = async ({
email,
mfaToken
}: {
email: string;
mfaToken: string;
}) => {
const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', {
email,
mfaToken
});
return data;
}
export default verifyMfaToken;
@@ -0,0 +1,38 @@
import { apiRequest } from "@app/config/request";
interface RequestType {
method: string;
secret: {
type: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[];
}
}
const batchSecrets = async ({
workspaceId,
environment,
requests
}: {
workspaceId: string;
environment: string;
requests: RequestType[];
}) => {
const { data } = await apiRequest.post('/api/v2/secrets/batch', {
workspaceId,
environment,
requests
});
return data;
}
export default batchSecrets;
@@ -0,0 +1,32 @@
import SecurityClient from '@app/components/utilities/SecurityClient';
interface Props {
isMfaEnabled: boolean;
}
/**
* Update the user's MFA-enabled status to [isMfaEnabled]
* @param {Object} obj
* @param {Boolean} obj.isMfaEnabled - whether or not MFA status should be set to enabled or not
* @returns {User} user - user with updated MFA-enabled status
*/
const updateMyMfaEnabled = async ({
isMfaEnabled
}: Props) =>
SecurityClient.fetchCall(`/api/v2/users/me/mfa`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
isMfaEnabled,
})
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).user;
}
console.log('Failed to update MFA status');
return undefined;
});
export default updateMyMfaEnabled;
+429 -239
View File
@@ -30,6 +30,7 @@ import DropZone from '@app/components/dashboard/DropZone';
import KeyPair from '@app/components/dashboard/KeyPair';
import SideBar from '@app/components/dashboard/SideBar';
import NavHeader from '@app/components/navigation/NavHeader';
import { decryptAssymmetric, decryptSymmetric } from '@app/components/utilities/cryptography/crypto';
import guidGenerator from '@app/components/utilities/randomId';
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
@@ -41,12 +42,14 @@ import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
import { useLeaveConfirm } from '@app/hooks';
import addSecrets from '../api/files/AddSecrets';
import deleteSecrets from '../api/files/DeleteSecrets';
import updateSecrets from '../api/files/UpdateSecrets';
// import addSecrets from '../api/files/AddSecrets';
// import deleteSecrets from '../api/files/DeleteSecrets';
// import updateSecrets from '../api/files/UpdateSecrets';
import batchSecrets from '../api/files/batchSecrets';
import getUser from '../api/user/getUser';
import checkUserAction from '../api/userActions/checkUserAction';
import registerUserAction from '../api/userActions/registerUserAction';
import getLatestFileKey from '../api/workspace/getLatestFileKey';
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
import getWorkspaces from '../api/workspace/getWorkspaces';
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
@@ -94,6 +97,32 @@ interface SnapshotProps {
}[];
}
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';
tags: Tag[];
}
interface SecretProps {
key: string;
value: string | undefined;
type: 'personal' | 'shared';
comment: string;
id: string;
tags: Tag[];
}
/**
* this function finds the teh duplicates in an array
* @param arr - array of anything (e.g., with secret keys and types (personal/shared))
@@ -188,6 +217,9 @@ export default function Dashboard() {
useEffect(() => {
(async () => {
if (router.isReady && workspaceId === 'undefined') {
router.push('/noprojects');
}
try {
const { push, query } = router
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
@@ -302,7 +334,7 @@ export default function Dashboard() {
valueOverride: undefined,
comment: '',
tags: []
},
}
]);
};
@@ -319,50 +351,50 @@ export default function Dashboard() {
);
};
const modifyValue = (value: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
const modifyValue = (value: string, id: string) => {
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, value } : e)));
setHasUnsavedChanges(true);
};
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
const modifyValueOverride = (valueOverride: string | undefined, id: string) => {
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, valueOverride } : e)));
setHasUnsavedChanges(true);
};
const modifyKey = (key: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
const modifyKey = (key: string, id: string) => {
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, key } : e)));
setHasUnsavedChanges(true);
};
const modifyComment = (comment: string, pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
const modifyComment = (comment: string, id: string) => {
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, comment } : e)));
setHasUnsavedChanges(true);
};
const modifyTags = (tags: Tag[], pos: number) => {
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
const modifyTags = (tags: Tag[], id: string) => {
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, tags } : e)));
setHasUnsavedChanges(true);
};
// For speed purposes and better perforamance, we are using useCallback
const listenChangeValue = useCallback((value: string, pos: number) => {
modifyValue(value, pos);
const listenChangeValue = useCallback((value: string, id: string) => {
modifyValue(value, id);
}, []);
const listenChangeValueOverride = useCallback((value: string | undefined, pos: number) => {
modifyValueOverride(value, pos);
const listenChangeValueOverride = useCallback((value: string | undefined, id: string) => {
modifyValueOverride(value, id);
}, []);
const listenChangeKey = useCallback((value: string, pos: number) => {
modifyKey(value, pos);
const listenChangeKey = useCallback((value: string, id: string) => {
modifyKey(value, id);
}, []);
const listenChangeComment = useCallback((value: string, pos: number) => {
modifyComment(value, pos);
const listenChangeComment = useCallback((value: string, id: string) => {
modifyComment(value, id);
}, []);
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
modifyTags(value, pos);
const listenChangeTags = useCallback((value: Tag[], id: string) => {
modifyTags(value, id);
}, []);
/**
@@ -370,174 +402,335 @@ export default function Dashboard() {
*/
// TODO(akhilmhdh): split and make it small
const savePush = async (dataToPush?: SecretDataProps[]) => {
setSaveLoading(true);
let newData: SecretDataProps[] | null | undefined;
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
if ((dataToPush ?? [])?.length > 0) {
newData = dataToPush;
} else {
newData = data;
}
try {
setSaveLoading(true);
let newData: SecretDataProps[] | null | undefined;
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
if ((dataToPush ?? [])?.length > 0) {
newData = dataToPush;
} else {
newData = data;
}
// Checking if any of the secret keys start with a number - if so, don't do anything
const nameErrors = !newData!
.map((secret) => !Number.isNaN(Number(secret.key.charAt(0))))
.every((v) => v === false);
const duplicatesExist =
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
// Checking if any of the secret keys start with a number - if so, don't do anything
const nameErrors = !newData!
.map((secret) => !Number.isNaN(Number(secret.key.charAt(0))))
.every((v) => v === false);
const duplicatesExist =
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
if (nameErrors) {
setSaveLoading(false);
return createNotification({
text: 'Solve all name errors before saving secrets.',
type: 'error'
});
}
if (nameErrors) {
setSaveLoading(false);
return createNotification({
text: 'Solve all name errors before saving secrets.',
type: 'error'
});
}
if (duplicatesExist) {
setSaveLoading(false);
return createNotification({
text: 'Remove duplicated secret names before saving.',
type: 'error'
});
}
if (duplicatesExist) {
setSaveLoading(false);
return createNotification({
text: 'Remove duplicated secret names before saving.',
type: 'error'
});
}
if (selectedEnv?.isWriteDenied) {
setSaveLoading(false);
return createNotification({
text: 'You are not allowed to edit this environment',
type: 'error'
});
}
if (selectedEnv?.isWriteDenied) {
setSaveLoading(false);
return createNotification({
text: 'You are not allowed to edit this environment',
type: 'error'
});
}
// Once "Save changes" is clicked, disable that button
setHasUnsavedChanges(false);
// Once "Save changes" is clicked, disable that button
setHasUnsavedChanges(false);
const secretsToBeDeleted = initialData!
.filter(
(initDataPoint) =>
!newData!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
)
.map((secret) => secret.id);
console.log('delete', secretsToBeDeleted.length);
const secretsToBeAdded = newData!.filter(
(newDataPoint) =>
!initialData!.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
);
console.log('add', secretsToBeAdded.length);
const secretsToBeUpdated = newData!.filter((newDataPoint) =>
initialData!
.filter(
(initDataPoint) =>
newData!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
initDataPoint.value ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment) ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !==
initDataPoint?.tags
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
);
console.log('update', secretsToBeUpdated.length);
const newOverrides = newData!.filter(
(newDataPoint) => newDataPoint.valueOverride !== undefined
);
const initOverrides = initialData!.filter(
(initDataPoint) => initDataPoint.valueOverride !== undefined
);
const overridesToBeDeleted = initOverrides
.filter(
(initDataPoint) =>
!newOverrides!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
)
.map((secret) => String(secret.idOverride));
console.log('override delete', overridesToBeDeleted.length);
const overridesToBeAdded = newOverrides!
.filter(
(newDataPoint) =>
!initOverrides.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
const overridesToBeUpdated = newOverrides!
.filter((newDataPoint) =>
initOverrides
const secretsToBeDeleted = initialData!
.filter(
(initDataPoint) =>
newOverrides!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.valueOverride !== initDataPoint.valueOverride ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
.comment !== initDataPoint.comment ||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !== initDataPoint?.tags)
!newData!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
.map((secret) => secret.id);
console.log('delete', secretsToBeDeleted.length);
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
}
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
workspaceId,
env: selectedEnv.slug
});
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
}
if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId,
env: selectedEnv.slug
});
if (secrets) await updateSecrets({ secrets });
}
const secretsToBeAdded = newData!.filter(
(newDataPoint) =>
!initialData!.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
);
console.log('add', secretsToBeAdded.length);
setInitialData(structuredClone(newData));
const secretsToBeUpdated = newData!.filter((newDataPoint) =>
initialData!
.filter(
(initDataPoint) =>
newData!.filter((dataPoint) => dataPoint.id)
.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
initDataPoint.value ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
initDataPoint.key ||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
initDataPoint.comment ||
JSON.stringify(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags) !==
JSON.stringify(initDataPoint?.tags))
)
.map((secret) => secret.id)
.includes(newDataPoint.id)
);
console.log('update', secretsToBeUpdated.length);
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
setCheckDocsPopUpVisible(true);
await registerUserAction({ action: 'first_time_secrets_pushed' });
}
const newOverrides = newData!.filter(
(newDataPoint) => newDataPoint.valueOverride !== undefined
);
const initOverrides = initialData!.filter(
(initDataPoint) => initDataPoint.valueOverride !== undefined
);
// increasing the number of project commits
setNumSnapshots((numSnapshots ?? 0) + 1);
setSaveLoading(false);
const overridesToBeDeleted = initOverrides
.filter(
(initDataPoint) =>
!newOverrides!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
)
.map((secret) => String(secret.idOverride));
console.log('override delete', overridesToBeDeleted.length);
const overridesToBeAdded = newOverrides!
.filter(
(newDataPoint) =>
!initOverrides.map((initDataPoint) => initDataPoint.idOverride).includes(newDataPoint.idOverride)
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override add', overridesToBeAdded.length);
const overridesToBeUpdated = newOverrides!
.filter((newDataPoint) =>
initOverrides
.filter(
(initDataPoint) =>
newOverrides!.map((dataPoint) => dataPoint.idOverride)
.includes(initDataPoint.idOverride) &&
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
.valueOverride !== initDataPoint.valueOverride ||
newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0].key !==
initDataPoint.key ||
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
.comment || '') !== (initDataPoint.comment || '')
||
(JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]?.tags) || '') !==
(JSON.stringify(initDataPoint?.tags) || '')
)
)
.map((secret) => secret.idOverride)
.includes(newDataPoint.idOverride)
)
.map((override) => ({
pos: override.pos,
key: override.key,
value: String(override.valueOverride),
valueOverride: override.valueOverride,
comment: '',
id: String(override.idOverride),
idOverride: String(override.idOverride),
tags: override.tags
}));
console.log('override update', overridesToBeUpdated.length);
const requests: any = []; // TODO: fix any
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
secretsToBeDeleted.concat(overridesToBeDeleted).forEach((_id: string) => {
requests.push({
method: 'DELETE',
secret: {
_id
}
});
});
}
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
workspaceId,
env: selectedEnv.slug
});
if (secrets) {
secrets.forEach((secret) => {
requests.push({
method: 'POST',
secret: {
type: secret.type,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
tags: secret.tags
}
})
});
}
}
if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
workspaceId,
env: selectedEnv.slug
});
if (secrets) {
secrets.forEach((secret) => {
requests.push({
method: 'PATCH',
secret: {
_id: secret.id,
type: secret.type,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
secretCommentCiphertext: secret.secretCommentCiphertext,
secretCommentIV: secret.secretCommentIV,
secretCommentTag: secret.secretCommentTag,
tags: secret.tags
}
});
});
}
}
let newSecrets;
if (selectedEnv && requests.length > 0) {
newSecrets = await batchSecrets({
workspaceId,
environment: selectedEnv.slug,
requests
});
}
let formattedNewDecryptedKeys;
if (newSecrets.createdSecrets) {
const latestKey = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
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
newSecrets.createdSecrets.forEach((secret: EncryptedSecretProps) => {
const plainTextKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
let plainTextValue;
if (secret.secretValueCiphertext !== undefined) {
plainTextValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
} else {
plainTextValue = undefined;
}
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,
tags: secret.tags
});
});
}
const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))];
formattedNewDecryptedKeys = secretKeys.map((key, index) => ({
id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
?.id,
idOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'personal'
)[0]?.id,
pos: (newData?.filter(dp => !dp.id?.includes('-'))?.length ?? 0) + index,
key,
value: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.value,
valueOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'personal'
)[0]?.value,
comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.comment,
tags: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === 'shared'
)[0]?.tags
}));
setInitialData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))));
setData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))))
} else {
setInitialData(structuredClone(newData));
}
// If this user has never saved environment variables before, show them a prompt to read docs
if (!hasUserEverPushed) {
setCheckDocsPopUpVisible(true);
await registerUserAction({ action: 'first_time_secrets_pushed' });
}
// increasing the number of project commits
setNumSnapshots((numSnapshots ?? 0) + 1);
setSaveLoading(false);
createNotification({
text: `Successfully saved secrets.`,
type: 'success'
});
} catch (error) {
console.log("Something went wrong while saving secrets: ", error)
createNotification({
text: `Something went wrong while saving secrets.`,
type: 'error'
});
}
return undefined;
};
@@ -581,7 +774,9 @@ export default function Dashboard() {
if (!window.confirm(leaveConfirmDefaultMessage)) return;
}
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
const selectedWorkspaceEnv = workspaceEnvs.find(
({ name }: { name: string }) => envName === name
) || {
name: 'unknown',
slug: 'unknown',
isWriteDenied: false,
@@ -674,16 +869,18 @@ export default function Dashboard() {
</div>
<div className="flex flex-row">
<div className="flex justify-start max-w-sm mt-1 mr-2">
{!selectedEnv?.isReadDenied && <Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true)
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
/>}
{!selectedEnv?.isReadDenied && (
<Button
text={String(`${numSnapshots} ${t('Commits')}`)}
onButtonPressed={() => {
toggleSidebar('None');
togglePITSidebar(true);
}}
color="mineshaft"
size="md"
icon={faClockRotateLeft}
/>
)}
</div>
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
<div className="flex justify-start max-w-sm mt-1">
@@ -717,17 +914,24 @@ export default function Dashboard() {
comment: '',
tags: sv.tags
}));
setData(rolledBackSecrets);
// Perform the rollback globally
performSecretRollback({ workspaceId, version: snapshotData.version });
setSnapshotData(undefined);
createNotification({
text: `Rollback has been performed successfully.`,
type: 'success'
});
setHasUnsavedChanges(false);
const result = await performSecretRollback({ workspaceId, version: snapshotData.version });
if (result === undefined) {
createNotification({
text: `Something went wrong during the rollback.`,
type: 'error'
});
} else {
setData(rolledBackSecrets);
createNotification({
text: `Successfully rolled back secrets.`,
type: 'success'
});
setSnapshotData(undefined);
setHasUnsavedChanges(false);
togglePITSidebar(false);
}
}}
color="primary"
size="md"
@@ -810,16 +1014,14 @@ export default function Dashboard() {
alt="infisical loading indicator"
/>
</div>
) : data?.length !== 0 ? (
) : (data?.length !== 0 || snapshotData?.secretVersions) ? (
<div className="flex flex-col w-full mt-1">
<div
onScroll={onSecretsAreaScroll}
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
>
<div ref={secretsTop} />
<div
className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
>
<div className="group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]">
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
<div className='text-transparent text-xs flex items-center justify-center w-12 h-10 cursor-default'>0</div>
@@ -834,33 +1036,38 @@ export default function Dashboard() {
</IconButton>}
</div>
<div className="w-5/12 border-r border-mineshaft-600">
<div
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
>
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
<div className="flex items-center rounded-lg mt-4 md:mt-0 max-h-10">
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12">
Value
</div>
</div>
</div>
<div className="w-[calc(10%)] border-r border-mineshaft-600">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
<div className="flex items-center max-h-16 overflow-hidden">
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12">
Comment
</div>
</div>
</div>
<div className="w-2/12">
<div className="flex items-center max-h-16">
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12">
Tags
</div>
</div>
</div>
<div
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
/>
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
<div className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center" />
<div className="w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center">
<div
onKeyDown={() => null}
role="none"
onClick={() => { }}
className="invisible group-hover:visible"
>
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
<FontAwesomeIcon
className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible"
icon={faXmark}
/>
</div>
</div>
</div>
@@ -873,7 +1080,7 @@ export default function Dashboard() {
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
.filter((row) => !sharedToHide.includes(row.id))
.filter((row) => row.value !== undefined)
// .filter((row) => row.value !== undefined)
.map((keyPair) => (
<KeyPair
isCapitalized={autoCapitalization}
@@ -901,22 +1108,6 @@ export default function Dashboard() {
?.sort((a, b) => a.key.localeCompare(b.key))
.filter((row) => row.environment === selectedSnapshotEnv?.slug)
.filter((row) => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
.filter(
(row) =>
!snapshotData.secretVersions
?.filter((secretVersion) =>
snapshotData.secretVersions
?.map((item) => item.key)
.filter(
(item, index) =>
index !==
snapshotData.secretVersions?.map((i) => i.key).indexOf(item)
)
.includes(secretVersion.key)
)
?.map((item) => item.id)
.includes(row.id)
)
.map((keyPair) => (
<KeyPair
isCapitalized={autoCapitalization}
@@ -937,15 +1128,15 @@ export default function Dashboard() {
tags={projectTags}
/>
))}
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
<div className='w-10' />
<div className="bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]">
<div className="w-10" />
<button
type="button"
className='text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer'
className="text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer"
onClick={addRowToBottom}
>
<FontAwesomeIcon icon={faPlus} className='mr-3' />
<span className='text-sm'>Add Secret</span>
<FontAwesomeIcon icon={faPlus} className="mr-3" />
<span className="text-sm">Add Secret</span>
</button>
</div>
</div>
@@ -992,8 +1183,7 @@ export default function Dashboard() {
<SideBar
toggleSidebar={toggleSidebar}
data={data.filter(
(row: SecretDataProps) =>
row.id === sidebarSecretId && row.value !== undefined
(row: SecretDataProps) => row.id === sidebarSecretId && row.value !== undefined
)}
modifyKey={listenChangeKey}
modifyValue={listenChangeValue}
+40 -22
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { faSlack } from '@fortawesome/free-brands-svg-icons';
@@ -16,6 +17,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import onboardingCheck from '@app/components/utilities/checks/OnboardingCheck';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { TabsObject } from '@app/components/v2/Tabs';
import registerUserAction from '../api/userActions/registerUserAction';
@@ -43,7 +45,7 @@ const learningItem = ({
<a
target={`${link.includes('https') ? '_blank' : '_self'}`}
rel="noopener noreferrer"
className="w-full"
className={`w-full ${complete && 'opacity-30 hover:opacity-100 duration-200'}`}
href={link}
>
<div
@@ -57,13 +59,13 @@ const learningItem = ({
});
}
}}
className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"
className="relative group bg-bunker-500 hover:bg-mineshaft-700 shadow-xl duration-200 rounded-md border border-mineshaft-600 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden mb-3 cursor-pointer"
>
<div className="flex flex-row items-center mr-4">
<FontAwesomeIcon icon={icon} className="text-4xl mx-2 w-16" />
{complete && (
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
<div className="bg-bunker-500 group-hover:bg-mineshaft-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-primary" />
</div>
)}
<div className="flex flex-col items-start">
@@ -72,11 +74,11 @@ const learningItem = ({
</div>
</div>
<div
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-primary'}`}
>
{complete ? 'Complete!' : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
</div>
</a>
);
@@ -101,7 +103,7 @@ const learningItem = ({
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-11 top-10">
<FontAwesomeIcon
icon={faCheckCircle}
className="absolute text-4xl left-12 top-16 w-5 h-5 text-green"
className="absolute text-4xl left-12 top-16 w-5 h-5 text-primary"
/>
</div>
)}
@@ -110,10 +112,10 @@ const learningItem = ({
<div className="text-sm font-normal mt-0.5">{subText}</div>
</div>
</div>
<div className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}>
<div className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-primary'}`}>
{complete ? 'Complete!' : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
</div>
);
};
@@ -142,16 +144,17 @@ export default function Home() {
}, []);
return (
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll pt-4">
<div className="mx-6 lg:mx-0 w-full pt-4">
<Head>
<title>Infisical Guide</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex flex-col items-center text-gray-300 text-lg mx-auto max-w-2xl lg:max-w-3xl xl:max-w-4xl py-6">
<div className="text-3xl font-bold text-left w-full">Your quick start guide</div>
<div className="text-md text-left w-full pt-2 pb-4 text-bunker-300">
<div className="flex relative flex-col items-center text-gray-300 text-lg mx-auto px-6 max-w-3xl lg:max-w-4xl xl:max-w-5xl py-6">
<div className="text-5xl font-bold text-left w-full mt-12">Your quick start guide</div>
<div className="text-lg text-left w-full pt-2 pb-4 mb-14 text-bunker-300">
Click on the items below and follow the instructions.
</div>
<div className='absolute h-min top-0 right-0 hidden lg:block'><Image src="/images/dragon-book.svg" height={250} width={400} alt="start guise dragon illustration" /></div>
{learningItem({
text: 'Get to know Infisical',
subText: '',
@@ -170,17 +173,32 @@ export default function Home() {
userAction: 'first_time_secrets_pushed',
link: `/dashboard/${router.query.id}`
})}
{learningItem({
text: 'Inject secrets locally',
subText: 'Replace .env files with a more secure and efficient alternative.',
complete: false,
icon: faNetworkWired,
time: '8 min',
link: 'https://infisical.com/docs/getting-started/quickstart'
})}
<div className="relative group bg-bunker-500 shadow-xl duration-200 rounded-md border border-mineshaft-600 pl-2 pr-2 pt-4 pb-2 h-full w-full flex flex-col items-center justify-between overflow-hidden mb-3 cursor-default">
<div className='w-full flex flex-row items-center mb-4 pr-4'>
<div className="flex flex-row items-center mr-4 w-full">
<FontAwesomeIcon icon={faNetworkWired} className="text-4xl mx-2 w-16" />
{false && (
<div className="bg-bunker-500 group-hover:bg-mineshaft-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
</div>
)}
<div className="flex pl-0.5 flex-col items-start">
<div className="text-xl font-semibold mt-0.5">Inject secrets locally</div>
<div className="text-sm font-normal">Replace .env files with a more secure and efficient alternative.</div>
</div>
</div>
<div
className={`pr-4 font-semibold text-sm w-28 text-right ${false && 'text-green'}`}
>
About 2 min
</div>
</div>
<TabsObject/>
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
{learningItem({
text: 'Integrate Infisical with your infrastructure',
subText: 'Only a few integrations are currently available. Many more coming soon!',
subText: 'Connect Infisical to various 3rd party services and platforms.',
complete: false,
icon: faPlug,
time: '15 min',
+31 -104
View File
@@ -4,28 +4,23 @@ import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faWarning } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button';
import Error from '@app/components/basic/Error';
import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox';
import attemptLogin from '@app/components/utilities/attemptLogin';
import LoginStep from '@app/components/login/LoginStep';
import MFAStep from '@app/components/login/MFAStep';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { isLoggedIn } from '@app/reactQuery';
import getWorkspaces from './api/workspace/getWorkspaces';
export default function Login() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
const router = useRouter();
const [step, setStep] = useState(1);
const { t } = useTranslation();
const lang = router.locale ?? 'en';
const setLanguage = async (to: string) => {
router.push('/login', '/login', { locale: to });
@@ -45,29 +40,34 @@ export default function Login() {
}
};
if (isLoggedIn()) {
setIsAlreadyLoggedIn(true);
redirectToDashboard();
}
}, []);
/**
* This function check if the user entered the correct credentials and should be allowed to log in.
*/
const loginCheck = async () => {
if (!email || !password) {
return;
const renderStep = (loginStep: number) => {
// TODO: add MFA step
switch (loginStep) {
case 1:
return (
<LoginStep
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
setStep={setStep}
/>
);
case 2:
// TODO: add MFA step
return (
<MFAStep
email={email}
password={password}
/>
);
default:
return <div />
}
setIsLoading(true);
await attemptLogin(email, password, setErrorLogin, router, false, true).then(() => {
setTimeout(() => {
setIsLoading(false);
}, 2000);
});
};
if (isAlreadyLoggedIn) {
return null
}
return (
@@ -84,80 +84,7 @@ export default function Login() {
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
</div>
</Link>
<form onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}>
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
{t('login:login')}
</p>
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<InputField
label={t('common:email')}
onChangeHandler={setEmail}
type="email"
value={email}
placeholder=""
isRequired
autoComplete="username"
/>
</div>
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
<InputField
label={t('common:password')}
onChangeHandler={setPassword}
type="password"
value={password}
placeholder=""
isRequired
autoComplete="current-password"
id="current-password"
/>
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
<Link href="/verify-email">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:forgot-password')}
</button>
</Link>
</div>
</div>
{!isLoading && errorLogin && <Error text={t('login:error-login') ?? ''} />}
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
<Button
type="submit"
text={t('login:login') ?? ''}
onButtonPressed={loginCheck}
loading={isLoading}
size="lg"
/>
</div>
</div>
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
</div> */}
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t('common:maintenance-alert')}
</div>
)}
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
<p className="text-sm flex justify-center text-gray-400 w-max">
{t('login:need-account')}
</p>
<Link href="/signup">
<button
type="button"
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
>
{t('login:create-account')}
</button>
</Link>
</div>
</form>
{renderStep(step)}
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
<div className="w-48 mx-auto">
<ListBox
@@ -173,4 +100,4 @@ export default function Login() {
);
}
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
export const getStaticProps = getTranslatedStaticProps(['auth', 'login', 'mfa']);
+48 -14
View File
@@ -1,3 +1,5 @@
import crypto from 'crypto';
import { useState } from 'react';
import Image from 'next/image';
import { useRouter } from 'next/router';
@@ -12,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import { deriveArgonKey } from '../components/utilities/cryptography/crypto';
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
@@ -39,6 +42,7 @@ export default function PasswordReset() {
const getEncryptedKeyHandler = async () => {
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey(
Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
@@ -64,14 +68,6 @@ export default function PasswordReset() {
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: privateKey,
secret: newPassword
.slice(0, 32)
.padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
}) as { ciphertext: string; iv: string; tag: string };
client.init(
{
username: email,
@@ -79,13 +75,51 @@ export default function PasswordReset() {
},
async () => {
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
const response = await resetPasswordOnAccountRecovery({
verificationToken,
encryptedPrivateKey: ciphertext,
iv,
tag,
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
verifier: result.verifier
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error('Failed to derive key from password');
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
const response = await resetPasswordOnAccountRecovery({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
verificationToken
});
// if everything works, go the main dashboard page.
+1 -1
View File
@@ -20,7 +20,7 @@ export default function RequestNewInvite() {
doesn&apos;t work, please reach out to us at support@infisical.com
</p>
<div className="">
<Image src="/images/invitation-expired.svg" height={500} width={800} alt="google logo" />
<Image src="/images/invitation-expired.svg" height={500} width={800} alt="invitation expired illustration" />
</div>
</div>
</div>
+2 -2
View File
@@ -31,7 +31,7 @@ export default function SettingsBilling() {
{
key: 2,
name: 'Team',
price: '$7',
price: '$8',
priceExplanation: t('billing:professional.price-explanation')!,
text: 'For teams that want to improve their efficiency and security.',
buttonTextMain: t('billing:upgrade')!,
@@ -41,7 +41,7 @@ export default function SettingsBilling() {
{
key: 3,
name: t('billing:professional.name')!,
price: '$14',
price: '$18',
priceExplanation: t('billing:professional.price-explanation')!,
text: t('billing:enterprise.text')!,
subtext: t('billing:professional.subtext')!,
+4 -358
View File
@@ -1,375 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useEffect, useState } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { faCheck, faMagnifyingGlass, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { plans } from 'public/data/frequentConstants';
import Button from '@app/components/basic/buttons/Button';
import AddIncidentContactDialog from '@app/components/basic/dialog/AddIncidentContactDialog';
import AddUserDialog from '@app/components/basic/dialog/AddUserDialog';
import UpgradePlanModal from '@app/components/basic/dialog/UpgradePlan';
import InputField from '@app/components/basic/InputField';
import UserTable from '@app/components/basic/table/UserTable';
import NavHeader from '@app/components/navigation/NavHeader';
import guidGenerator from '@app/components/utilities/randomId';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import addUserToOrg from '../../api/organization/addUserToOrg';
import deleteIncidentContact from '../../api/organization/deleteIncidentContact';
import getIncidentContacts from '../../api/organization/getIncidentContacts';
import getOrganization from '../../api/organization/GetOrg';
import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
import getOrganizationUsers from '../../api/organization/GetOrgUsers';
import renameOrg from '../../api/organization/renameOrg';
import getUser from '../../api/user/getUser';
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
import getWorkspaces from '../../api/workspace/getWorkspaces';
import { OrgSettingsPage } from '@app/views/Settings/OrgSettingsPage';
export default function SettingsOrg() {
const [buttonReady, setButtonReady] = useState(false);
const router = useRouter();
const host = window.location.origin;
const [orgName, setOrgName] = useState('');
const [emailUser, setEmailUser] = useState('');
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
const [searchUsers, setSearchUsers] = useState('');
const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false);
const [isAddUserOpen, setIsAddUserOpen] = useState(router.asPath.split('?')[1] === 'invite');
const [incidentContacts, setIncidentContacts] = useState<string[]>([]);
const [searchIncidentContact, setSearchIncidentContact] = useState('');
const [userList, setUserList] = useState<any[]>([]);
const [personalEmail, setPersonalEmail] = useState('');
const [email, setEmail] = useState('');
const [currentPlan, setCurrentPlan] = useState('');
const workspaceId = router.query.id as string;
const { t } = useTranslation();
useEffect(() => {
(async () => {
const orgId = localStorage.getItem('orgData.id') as string;
const org = await getOrganization({
orgId
});
setOrgName(org.name);
const incidentContactsData = await getIncidentContacts(
localStorage.getItem('orgData.id') as string
);
setIncidentContacts(incidentContactsData?.map((contact) => contact.email));
const user = await getUser();
setPersonalEmail(user.email);
const orgUsers = await getOrganizationUsers({
orgId
});
setUserList(
orgUsers.map((orgUser) => ({
key: guidGenerator(),
firstName: orgUser.user?.firstName,
lastName: orgUser.user?.lastName,
email: orgUser.user?.email == null ? orgUser.inviteEmail : orgUser.user?.email,
role: orgUser?.role,
status: orgUser?.status,
userId: orgUser.user?._id,
membershipId: orgUser._id,
publicKey: orgUser.user?.publicKey
}))
);
const subscriptions = await getOrganizationSubscriptions({
orgId
});
if (subscriptions) {
setCurrentPlan(subscriptions.data[0].plan.product);
}
})();
}, []);
const modifyOrgName = (newName: string) => {
setButtonReady(true);
setOrgName(newName);
};
const submitChanges = (newOrgName: string) => {
renameOrg(localStorage.getItem('orgData.id') as string, newOrgName);
setButtonReady(false);
};
const closeAddUserModal = () => {
setIsAddUserOpen(false);
};
const closeAddIncidentContactModal = () => {
setIsAddIncidentContactOpen(false);
};
const openAddUserModal = () => {
setIsAddUserOpen(true);
};
const openAddIncidentContactModal = () => {
setIsAddIncidentContactOpen(true);
};
const submitAddUserModal = async (newUserEmail: string) => {
await addUserToOrg(newUserEmail, localStorage.getItem('orgData.id') as string);
setEmail('');
setIsAddUserOpen(false);
router.reload();
};
const deleteIncidentContactFully = (incidentContact: string) => {
setIncidentContacts(incidentContacts.filter((contact) => contact !== incidentContact));
deleteIncidentContact(localStorage.getItem('orgData.id') as string, incidentContact);
};
/**
* This function deleted a workspace.
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another aviable workspace.
*/
const executeDeletingWorkspace = async () => {
const userWorkspaces = await getWorkspaces();
if (userWorkspaces.length > 1) {
if (
userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
workspaceToBeDeletedName
) {
await deleteWorkspace(workspaceId);
const ws = await getWorkspaces();
router.push(`/dashboard/${ws[0]._id}`);
}
}
};
return (
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
<>
<Head>
<title>{t('common:head-title', { title: t('settings-org:title') })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="flex flex-row">
<div className="w-full max-h-screen pb-2">
<NavHeader pageName={t('settings-org:title')} />
<AddIncidentContactDialog
isOpen={isAddIncidentContactOpen}
closeModal={closeAddIncidentContactModal}
incidentContacts={incidentContacts}
setIncidentContacts={setIncidentContacts}
/>
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
<div className="flex flex-col justify-start items-start text-3xl">
<p className="font-semibold mr-4 text-gray-200">{t('settings-org:title')}</p>
<p className="font-normal mr-4 text-gray-400 text-base">
{t('settings-org:description')}
</p>
</div>
</div>
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-8xl">
<div className="flex flex-col">
<div className="min-w-md mt-2 flex flex-col items-end pb-4">
<div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start w-full mb-6">
<div className="max-h-28 w-full max-w-md mr-auto">
<p className="font-semibold mr-4 text-gray-200 text-xl mb-2">
{t('common:display-name')}
</p>
<InputField
label=""
// label="Organization Name"
onChangeHandler={modifyOrgName}
type="varName"
value={orgName}
placeholder=""
isRequired
/>
</div>
<div className="flex justify-start w-full">
<div className="flex justify-start max-w-sm mt-4 mb-2">
<Button
text={t('common:save-changes') as string}
onButtonPressed={() => submitChanges(orgName)}
color="mineshaft"
size="md"
active={buttonReady}
iconDisabled={faCheck}
textDisabled={t('common:saved') as string}
/>
</div>
</div>
</div>
</div>
</div>
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start w-full mb-6">
<p className="font-semibold mr-4 text-white text-xl">
{t('section-members:org-members')}
</p>
<p className="mr-4 text-gray-400 mt-2 mb-2">
{t('section-members:org-members-description')}
</p>
<AddUserDialog
isOpen={isAddUserOpen && (userList.length < 5 || currentPlan !== plans.starter || host !== 'https://app.infisical.com')}
closeModal={closeAddUserModal}
submitModal={submitAddUserModal}
email={emailUser}
setEmail={setEmailUser}
currentPlan={currentPlan}
orgName={orgName}
/>
<UpgradePlanModal
isOpen={isAddUserOpen && userList.length >= 5 && currentPlan === plans.starter && host === 'https://app.infisical.com'}
onClose={closeAddUserModal}
text="You can add more members if you switch to Infisical's Team plan."
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className="pb-1 w-full flex flex-row items-start">
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex-row ">
<FontAwesomeIcon
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
icon={faMagnifyingGlass}
/>
<input
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
value={searchUsers}
onChange={(e) => setSearchUsers(e.target.value)}
placeholder={t('section-members:search-members') as string}
/>
</div>
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start">
<Button
text={t('section-members:add-member') as string}
onButtonPressed={openAddUserModal}
color="mineshaft"
size="md"
icon={faPlus}
/>
</div>
</div>
{userList && (
<div className="overflow-y-auto w-full">
<UserTable
userData={userList}
changeData={setUserList}
myUser={personalEmail}
filter={searchUsers.toLowerCase()}
resendInvite={submitAddUserModal}
isOrg
// onClick={openDeleteModal}
// deleteUser={deleteMembership}
// setUserIdToBeDeleted={setUserIdToBeDeleted}
/>
</div>
)}
</div>
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start w-full mb-6 mt-4">
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
<div className="flex flex-col justify-between w-full max-w-3xl">
<p className="text-xl font-semibold mb-3 min-w-max">
{t('section-incident:incident-contacts')}
</p>
<p className="text-xs text-gray-500 mb-2 min-w-max">
{t('section-incident:incident-contacts-description')}
</p>
</div>
<div className="mt-4 mb-2 min-w-max flex flex-row items-end justify-center">
<Button
text={t('section-incident:add-contact') as string}
onButtonPressed={openAddIncidentContactModal}
color="mineshaft"
size="md"
icon={faPlus}
/>
</div>
</div>
<div className="h-12 w-full max-w-5xl bg-white/5 mt-2 flex items-center rounded-t-md flwex-row">
<FontAwesomeIcon
className="bg-white/5 rounded-tl-md py-4 pl-4 pr-2 text-gray-400"
icon={faMagnifyingGlass}
/>
<input
className="pl-2 text-gray-400 rounded-tr-md bg-white/5 w-full h-full outline-none"
value={searchIncidentContact}
onChange={(e) => setSearchIncidentContact(e.target.value)}
placeholder={t('common:search') as string}
/>
</div>
{incidentContacts?.filter((incidentEmail) =>
incidentEmail.includes(searchIncidentContact)
).length > 0 ? (
incidentContacts
.filter((incidentEmail) => incidentEmail.includes(searchIncidentContact))
.map((contact) => (
<div
key={guidGenerator()}
className="flex flex-row items-center justify-between max-w-5xl px-4 py-3 hover:bg-white/5 border-t border-gray-600 w-full"
>
<p className="text-gray-300">{contact}</p>
<div className="opacity-50 hover:opacity-100 duration-200">
<Button
onButtonPressed={() => deleteIncidentContactFully(contact)}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</div>
))
) : (
<div className="w-full flex flex-row justify-center mt-6 max-w-4xl ml-6">
<p className="text-gray-400">{t('section-incident:no-incident-contacts')}</p>
</div>
)}
</div>
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
<p className="text-xl font-bold text-red">
Danger Zone
</p>
<p className="mt-4 text-md text-gray-400">
As soon as you delete an organization, you will
not be able to undo it. This will immediately
remove all organization members and cancel your
subscription. If you still want to do that,
please enter the name of the organization below.
</p>
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
<InputField
label="Organization to be Deleted"
onChangeHandler={
setWorkspaceToBeDeletedName
}
type="varName"
value={workspaceToBeDeletedName}
placeholder=""
isRequired
/>
</div>
<button
type="button"
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={executeDeletingWorkspace}
>
Delete Project
</button>
<p className="mt-0.5 ml-1 text-xs text-gray-500">
Note: You can only delete a project in case you
have more than one.
</p>
</div> */}
</div>
</div>
</div>
</div>
<OrgSettingsPage />
</>
);
}
@@ -14,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import changePassword from '@app/components/utilities/cryptography/changePassword';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
import getAPIKeys from '../../api/apiKey/getAPIKeys';
@@ -100,6 +101,7 @@ export default function PersonalSettings() {
/>
</div>
</div>
<SecuritySection />
<div className="bg-white/5 rounded-md px-6 flex flex-col items-start w-full mt-2 mb-8 pt-2">
<div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full">
+3 -3
View File
@@ -11,6 +11,7 @@ import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep';
import EnterEmailStep from '@app/components/signup/EnterEmailStep';
import TeamInviteStep from '@app/components/signup/TeamInviteStep';
import UserInfoStep from '@app/components/signup/UserInfoStep';
import SecurityClient from '@app/components/utilities/SecurityClient';
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
@@ -28,7 +29,6 @@ export default function SignUp() {
const [codeError, setCodeError] = useState(false);
const [step, setStep] = useState(1);
const router = useRouter();
const [verificationToken, setVerificationToken] = useState('');
const { t } = useTranslation();
@@ -59,7 +59,8 @@ export default function SignUp() {
// Checking if the code matches the email.
const response = await checkEmailVerificationCode({ email, code });
if (response.status === 200) {
setVerificationToken((await response.json()).token);
const {token} = await response.json();
SecurityClient.setSignupToken(token);
setStep(3);
} else {
setCodeError(true);
@@ -94,7 +95,6 @@ export default function SignUp() {
/>
) : step === 3 ? (
<UserInfoStep
verificationToken={verificationToken}
incrementStep={incrementStep}
email={email}
password={password}
+85 -37
View File
@@ -1,5 +1,7 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unused-vars */
import crypto from 'crypto';
import { useState } from 'react';
import Head from 'next/head';
import Image from 'next/image';
@@ -17,7 +19,12 @@ import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import { saveTokenToLocalStorage } from '@app/components/utilities/saveTokenToLocalStorage';
import SecurityClient from '@app/components/utilities/SecurityClient';
import getOrganizations from '@app/pages/api/organization/getOrgs';
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
import verifySignupInvite from './api/auth/VerifySignupInvite';
@@ -75,17 +82,10 @@ export default function SignupInvite() {
const pair = nacl.box.keyPair();
const secretKeyUint8Array = pair.secretKey;
const publicKeyUint8Array = pair.publicKey;
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
const privateKey = encodeBase64(secretKeyUint8Array);
const publicKey = 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')
});
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
localStorage.setItem('PRIVATE_KEY', privateKey);
client.init(
{
@@ -94,35 +94,83 @@ export default function SignupInvite() {
},
async () => {
client.createVerifier(async (err, result) => {
let response = await completeAccountInformationSignupInvite({
email,
firstName,
lastName,
publicKey: PUBLIC_KEY,
ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
token: verificationToken
});
try {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
// if everything works, go the main dashboard page.
if (!errorCheck && response.status === 200) {
response = await response.json();
if (!derivedKey) throw new Error('Failed to derive key from password');
localStorage.setItem('publicKey', PUBLIC_KEY);
localStorage.setItem('encryptedPrivateKey', ciphertext);
localStorage.setItem('iv', iv);
localStorage.setItem('tag', tag);
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString('hex'),
secret: Buffer.from(derivedKey.hash)
});
const {
token: jwtToken
} = await completeAccountInformationSignupInvite({
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
});
// unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken('');
SecurityClient.setToken(jwtToken);
try {
await attemptLogin(email, password, setErrorLogin, router, false, false);
setStep(3);
} catch (error) {
setIsLoading(false);
console.log('Error', error);
}
saveTokenToLocalStorage({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await getOrganizations();
const orgId = userOrgs[0]._id;
localStorage.setItem('orgData.id', orgId);
setStep(3);
} catch (error) {
setIsLoading(false);
console.error(error);
}
});
}
@@ -152,7 +200,7 @@ export default function SignupInvite() {
// user will have temp token if doesn't have an account
// then continue with account setup workflow
if (res?.token) {
setVerificationToken(res.token);
SecurityClient.setSignupToken(res.token);
setStep(2);
} else {
// user will be redirected to dashboard
+10
View File
@@ -1,6 +1,8 @@
import { QueryClient } from '@tanstack/react-query';
// this is saved in react-query cache
export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ['infisical__signup-temp-token'];
export const MFA_TEMP_TOKEN_CACHE_KEY = ['infisical__mfa-temp-token'];
export const AUTH_TOKEN_CACHE_KEY = ['infisical__auth-token'];
export const queryClient = new QueryClient({
@@ -13,9 +15,17 @@ export const queryClient = new QueryClient({
});
// set token in memory cache
export const setSignupTempToken = (token: string) =>
queryClient.setQueryData(SIGNUP_TEMP_TOKEN_CACHE_KEY, token);
export const setMfaTempToken = (token: string) =>
queryClient.setQueryData(MFA_TEMP_TOKEN_CACHE_KEY, token);
export const setAuthToken = (token: string) =>
queryClient.setQueryData(AUTH_TOKEN_CACHE_KEY, token);
export const getSignupTempToken = () => queryClient.getQueryData(SIGNUP_TEMP_TOKEN_CACHE_KEY) as string;
export const getMfaTempToken = () => queryClient.getQueryData(MFA_TEMP_TOKEN_CACHE_KEY) as string;
export const getAuthToken = () => queryClient.getQueryData(AUTH_TOKEN_CACHE_KEY) as string;
export const isLoggedIn = () => Boolean(getAuthToken());
+56
View File
@@ -0,0 +1,56 @@
import { decryptPrivateKeyHelper } from '@app/helpers/key';
/**
* Class to handle key actions
*/
class KeyService {
/** Return the user's decrypted private key
* @param {Object} obj
* @param {Number} obj.encryptionVersion
* @param {String} obj.encryptedPrivateKey
* @param {String} obj.iv
* @param {String} obj.tag
* @param {String} obj.password
* @param {String} obj.salt
* @param {String} obj.protectedKey
* @param {String} obj.protectedKeyIV
* @param {String} obj.protectedKeyTag
* @returns {String} privateKey - decrypted private key
*/
static async decryptPrivateKey({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
}: {
encryptionVersion: number;
encryptedPrivateKey: string;
iv: string;
tag: string;
password: string;
salt: string;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
}) {
return decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
}
}
export default KeyService;
+26
View File
@@ -0,0 +1,26 @@
import { initProjectHelper } from '@app/helpers/project';
class ProjectService {
/**
* Create and initialize a new project in organization with id [organizationId]
* Note: current user should be a member of the organization
* @param {Object} obj
* @param {String} obj.organizationId - id of organization
* @param {String} obj.projectName - name of new project
* @returns {Project} project - new project
*/
static async initProject({
organizationId,
projectName
}: {
organizationId: string;
projectName: string
}) {
return initProjectHelper({
organizationId,
projectName
});
}
}
export default ProjectService;
@@ -0,0 +1,308 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from 'next-i18next';
import { plans } from 'public/data/frequentConstants';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import NavHeader from '@app/components/navigation/NavHeader';
import {
decryptAssymmetric,
encryptAssymmetric
} from '@app/components/utilities/cryptography/crypto';
import { useOrganization, useSubscription, useUser, useWorkspace } from '@app/context';
import {
useAddIncidentContact,
useAddUserToOrg,
useDeleteIncidentContact,
useDeleteOrgMembership,
useGetOrgIncidentContact,
useGetOrgUsers,
useGetUserWorkspaceMemberships,
useGetUserWsKey,
useRenameOrg,
useUpdateOrgUserRole,
useUploadWsKey
} from '@app/hooks/api';
import { OrgIncidentContactsTable, OrgMembersTable, OrgNameChangeSection } from './components';
export const OrgSettingsPage = () => {
const host = window.location.origin;
const { t } = useTranslation();
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const { user } = useUser();
const { subscriptionPlan } = useSubscription();
const { createNotification } = useNotificationContext();
const orgId = currentOrg?._id || '';
const { data: orgUsers } = useGetOrgUsers(orgId);
const { data: workspaceMemberships } = useGetUserWorkspaceMemberships(orgId);
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
const { data: incidentContact } = useGetOrgIncidentContact(orgId);
const renameOrg = useRenameOrg();
const removeUserOrgMembership = useDeleteOrgMembership();
const addUserToOrg = useAddUserToOrg();
const updateOrgUserRole = useUpdateOrgUserRole();
const uploadWsKey = useUploadWsKey();
const addIncidentContact = useAddIncidentContact();
const removeIncidentContact = useDeleteIncidentContact();
const isMoreUsersNotAllowed =
(orgUsers || []).length >= 5 &&
subscriptionPlan === plans.starter &&
host === 'https://app.infisical.com';
const onRenameOrg = async (name: string) => {
if (!currentOrg?._id) return;
try {
await renameOrg.mutateAsync({ orgId: currentOrg?._id, newOrgName: name });
createNotification({
text: 'Successfully renamed organization',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to rename organization',
type: 'error'
});
}
};
const onRemoveUserOrgMembership = async (membershipId: string) => {
if (!currentOrg?._id) return;
try {
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
createNotification({
text: 'Successfully removed used from org',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to remove user from org',
type: 'error'
});
}
};
const onAddUserToOrg = async (email: string) => {
if (!currentOrg?._id) return;
try {
await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
createNotification({
text: 'Successfully invited user to org',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to invite user to org',
type: 'error'
});
}
};
const onUpdateOrgUserRole = async (membershipId: string, role: string) => {
if (!currentOrg?._id) return;
try {
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?._id, membershipId, role });
createNotification({
text: 'Successfully updated user role',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to update user role',
type: 'error'
});
}
};
const onGrantUserAccess = async (userId: string, publicKey: string) => {
try {
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
if (!PRIVATE_KEY || !wsKey) return;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: wsKey.encryptedKey,
nonce: wsKey.nonce,
publicKey: wsKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
await uploadWsKey.mutateAsync({
userId,
nonce,
encryptedKey: ciphertext,
workspaceId: currentWorkspace?._id || ''
});
} catch (err) {
console.error(err);
createNotification({
text: 'Failed to grant access to user',
type: 'error'
});
}
};
const onAddIncidentContact = async (email: string) => {
if (!currentOrg?._id) return;
try {
await addIncidentContact.mutateAsync({ orgId, email });
createNotification({
text: 'Successfully added incident contact',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to add incident contact',
type: 'error'
});
}
};
const onRemoveIncidentContact = async (email: string) => {
if (!currentOrg?._id) return;
try {
await removeIncidentContact.mutateAsync({ orgId, email });
createNotification({
text: 'Successfully removed incident contact',
type: 'success'
});
} catch (error) {
console.error(error);
createNotification({
text: 'Failed to remove incident contact',
type: 'error'
});
}
};
/**
* This function deleted a workspace.
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
* It then deletes the workspace and forwards the user to another aviable workspace.
*/
// const executeDeletingWorkspace = async () => {
// const userWorkspaces = await getWorkspaces();
//
// if (userWorkspaces.length > 1) {
// if (
// userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
// workspaceToBeDeletedName
// ) {
// await deleteWorkspace(workspaceId);
// const ws = await getWorkspaces();
// router.push(`/dashboard/${ws[0]._id}`);
// }
// }
// };
//
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<NavHeader pageName={t('settings-org:title')} />
<div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
<div className="flex flex-col items-start justify-start text-3xl">
<p className="mr-4 font-semibold text-gray-200">{t('settings-org:title')}</p>
<p className="mr-4 text-base font-normal text-gray-400">
{t('settings-org:description')}
</p>
</div>
</div>
<div className="max-w-8xl ml-6 mr-6 flex flex-col text-mineshaft-50">
<OrgNameChangeSection orgName={currentOrg?.name} onOrgNameChange={onRenameOrg} />
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
<p className="mr-4 text-xl font-semibold text-white">
{t('section-members:org-members')}
</p>
<p className="mr-4 mt-2 mb-2 text-gray-400">
{t('section-members:org-members-description')}
</p>
<OrgMembersTable
isMoreUserNotAllowed={isMoreUsersNotAllowed}
orgName={currentOrg?.name || ''}
members={orgUsers}
workspaceMemberships={workspaceMemberships}
onInviteMember={onAddUserToOrg}
userId={user?._id || ''}
onRemoveMember={onRemoveUserOrgMembership}
onRoleChange={onUpdateOrgUserRole}
onGrantAccess={onGrantUserAccess}
/>
</div>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
<div className="flex w-full max-w-3xl flex-col justify-between">
<p className="mb-3 min-w-max text-xl font-semibold">
{t('section-incident:incident-contacts')}
</p>
<p className="mb-2 min-w-max text-xs text-gray-500">
{t('section-incident:incident-contacts-description')}
</p>
</div>
</div>
<div className="w-full">
<OrgIncidentContactsTable
contacts={incidentContact}
onRemoveContact={onRemoveIncidentContact}
onAddContact={onAddIncidentContact}
/>
</div>
</div>
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
<p className="text-xl font-bold text-red">
Danger Zone
</p>
<p className="mt-4 text-md text-gray-400">
As soon as you delete an organization, you will
not be able to undo it. This will immediately
remove all organization members and cancel your
subscription. If you still want to do that,
please enter the name of the organization below.
</p>
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
<InputField
label="Organization to be Deleted"
onChangeHandler={
setWorkspaceToBeDeletedName
}
type="varName"
value={workspaceToBeDeletedName}
placeholder=""
isRequired
/>
</div>
<button
type="button"
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={executeDeletingWorkspace}
>
Delete Project
</button>
<p className="mt-0.5 ml-1 text-xs text-gray-500">
Note: You can only delete a project in case you
have more than one.
</p>
</div> */}
</div>
</div>
);
};
@@ -0,0 +1,176 @@
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tr
} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { IncidentContact } from '@app/hooks/api/types';
type Props = {
contacts?: IncidentContact[];
onRemoveContact: (email: string) => Promise<void>;
onAddContact: (email: string) => Promise<void>;
};
const addContactFormSchema = yup.object({
email: yup.string().email().required().label('Email').trim()
});
type TAddContactForm = yup.InferType<typeof addContactFormSchema>;
export const OrgIncidentContactsTable = ({
contacts = [],
onAddContact,
onRemoveContact
}: Props) => {
const [searchContact, setSearchContact] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'addContact',
'removeContact'
] as const);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddContactForm>({ resolver: yupResolver(addContactFormSchema) });
const onAddIncidentContact = ({ email }: TAddContactForm) => {
onAddContact(email);
handlePopUpClose('addContact');
reset();
};
const onRemoveIncidentContact = async () => {
const incidentContactEmail = (popUp?.removeContact?.data as { email: string })?.email;
await onRemoveContact(incidentContactEmail);
handlePopUpClose('removeContact');
};
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchContact}
onChange={(e) => setSearchContact(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search incident contact by email..."
/>
</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen('addContact')}
>
Add Contact
</Button>
</div>
</div>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Email</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{contacts
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
?.map(({ email }) => (
<Tr key={email}>
<Td className="w-full">{email}</Td>
<Td className="mr-4">
<IconButton
ariaLabel="delete"
colorSchema="danger"
onClick={() => handlePopUpOpen('removeContact', { email })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Td>
</Tr>
))}
</TBody>
</Table>
{contacts
?.filter(({ email }) => email.toLocaleLowerCase().includes(searchContact))
?.length === 0 && (
<div className='py-4 bg-bunker-800 text-sm text-center text-bunker-400 w-full mx-auto flex justify-center'>No incident contacts found</div>
)}
</TableContainer>
</div>
<Modal
isOpen={popUp?.addContact?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle('addContact', isOpen);
reset();
}}
>
<ModalContent
title="Add an Incident Contact"
subTitle="This contact will be notified in the unlikely event of a severe incident."
>
<form onSubmit={handleSubmit(onAddIncidentContact)}>
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Incident Contact
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose('addContact')}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeContact.isOpen}
deleteKey="remove"
title="Do you want to remove this email from incident contact?"
onChange={(isOpen) => handlePopUpToggle('removeContact', isOpen)}
onDeleteApproved={onRemoveIncidentContact}
/>
</div>
);
};
@@ -0,0 +1 @@
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
@@ -0,0 +1,275 @@
import { useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { faMagnifyingGlass, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import {
Button,
DeleteActionModal,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem,
Table,
TableContainer,
Tag,
TBody,
Td,
Th,
THead,
Tr,
UpgradePlanModal
} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { OrgUser, Workspace } from '@app/hooks/api/types';
type Props = {
members?: OrgUser[];
workspaceMemberships?: Record<string, Workspace[]>;
orgName: string;
isMoreUserNotAllowed: boolean;
onRemoveMember: (userId: string) => Promise<void>;
onInviteMember: (email: string) => Promise<void>;
onRoleChange: (membershipId: string, role: string) => Promise<void>;
onGrantAccess: (userId: string, publicKey: string) => Promise<void>;
// the current user id to block remove org button
userId: string;
};
const addMemberFormSchema = yup.object({
email: yup.string().email().required().label('Email').trim()
});
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
export const OrgMembersTable = ({
members = [],
workspaceMemberships = {},
orgName,
isMoreUserNotAllowed,
onRemoveMember,
onInviteMember,
onGrantAccess,
onRoleChange,
userId
}: Props) => {
const [searchMemberFilter, setSearchMemberFilter] = useState('');
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
'addMember',
'removeMember',
'upgradePlan'
] as const);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
const onAddMember = ({ email }: TAddMemberForm) => {
onInviteMember(email);
handlePopUpClose('addMember');
reset();
};
const onRemoveOrgMemberApproved = async () => {
const orgMembershipId = (popUp?.removeMember?.data as { id: string })?.id;
await onRemoveMember(orgMembershipId);
handlePopUpClose('removeMember');
};
const isIamOwner = useMemo(
() => members.find(({ user }) => userId === user?._id)?.role === 'owner',
[userId, members]
);
const filterdUser = useMemo(
() =>
members.filter(
({ user, inviteEmail }) =>
user?.firstName?.toLowerCase().includes(searchMemberFilter) ||
user?.lastName?.toLowerCase().includes(searchMemberFilter) ||
user?.email?.toLowerCase().includes(searchMemberFilter) ||
inviteEmail?.includes(searchMemberFilter)
),
[members, searchMemberFilter]
);
return (
<div className="w-full">
<div className="mb-4 flex">
<div className="mr-4 flex-1">
<Input
value={searchMemberFilter}
onChange={(e) => setSearchMemberFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isMoreUserNotAllowed) {
handlePopUpOpen('upgradePlan');
} else {
handlePopUpOpen('addMember');
}
}}
>
Add Member
</Button>
</div>
</div>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Email</Th>
<Th>Role</Th>
<Th>Projects</Th>
<Th aria-label="actions" />
</Tr>
</THead>
<TBody>
{filterdUser.map(({ user, inviteEmail, role, _id: orgMembershipId, status }) => {
const name = user ? `${user.firstName} ${user.lastName}` : '-';
const email = user?.email || inviteEmail;
const userWs = workspaceMemberships?.[user?._id];
return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td>
<Td>{email}</Td>
<Td>
{status === 'accepted' && (
<Select
defaultValue={role}
isDisabled={userId === user?._id}
className="w-full bg-mineshaft-600"
onValueChange={(selectedRole) =>
onRoleChange(orgMembershipId, selectedRole)
}
>
{(isIamOwner || role === 'owner') && (
<SelectItem value="owner">owner</SelectItem>
)}
<SelectItem value="admin">admin</SelectItem>
<SelectItem value="member">member</SelectItem>
</Select>
)}
{(status === 'invited' || status === 'verified') && (
<Button colorSchema="secondary" onClick={() => onInviteMember(email)}>
Resent Invite
</Button>
)}
{status === 'completed' && (
<Button
colorSchema="secondary"
onClick={() => onGrantAccess(user?._id, user?.publicKey)}
>
Grant Access
</Button>
)}
</Td>
<Td>
{userWs ? (
userWs?.map(({ name: wsName, _id }) => (
<Tag key={`user-${user._id}-workspace-${_id}`} className="my-1">
{wsName}
</Tag>
))
) : (
<Tag colorSchema="red">This user isn&apos;t part of any projects yet</Tag>
)}
</Td>
<Td>
{userId !== user?._id && <IconButton
ariaLabel="delete"
colorSchema="danger"
isDisabled={userId === user?._id}
onClick={() => handlePopUpOpen('removeMember', { id: orgMembershipId })}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>}
</Td>
</Tr>
);
})}
</TBody>
</Table>
{filterdUser.length === 0 && <tr className='bg-bunker-800 text-sm py-4 text-center text-bunker-400 w-full mx-auto flex justify-center'><td className='col-span-5'>No project members found</td></tr>}
</TableContainer>
</div>
<Modal
isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle('addMember', isOpen);
reset();
}}
>
<ModalContent
title={`Invite others to ${orgName}`}
subTitle={
<>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</>
}
>
<form onSubmit={handleSubmit(onAddMember)}>
<Controller
control={control}
defaultValue=""
name="email"
render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Add Member
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose('addMember')}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.removeMember.isOpen}
deleteKey="remove"
title="Do you want to remove this user from the org?"
onChange={(isOpen) => handlePopUpToggle('removeMember', isOpen)}
onDeleteApproved={onRemoveOrgMemberApproved}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
</div>
);
};
@@ -0,0 +1 @@
export { OrgMembersTable } from './OrgMembersTable';
@@ -0,0 +1,68 @@
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { Button, FormControl, Input } from '@app/components/v2';
type Props = {
orgName?: string;
onOrgNameChange: (name: string) => Promise<void>;
};
const formSchema = yup.object({
name: yup.string().required().label('Project Name')
});
type FormData = yup.InferType<typeof formSchema>;
export const OrgNameChangeSection = ({ onOrgNameChange, orgName }: Props): JSX.Element => {
const {
handleSubmit,
control,
reset,
formState: { isDirty, isSubmitting }
} = useForm<FormData>({ resolver: yupResolver(formSchema) });
const { t } = useTranslation();
useEffect(() => {
reset({ name: orgName });
}, [orgName]);
const onFormSubmit = async ({ name }: FormData) => {
await onOrgNameChange(name);
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-3">
<p className="mb-4 mt-2 text-xl font-semibold">{t('common:display-name')}</p>
<div className="mb-2 w-full max-w-lg">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input placeholder="Type your org name" {...field} />
</FormControl>
)}
control={control}
name="name"
/>
</div>
<Button
isLoading={isSubmitting}
color="mineshaft"
size="sm"
type="submit"
isDisabled={!isDirty || isSubmitting}
leftIcon={<FontAwesomeIcon icon={faCheck} />}
>
{t('common:save-changes')}
</Button>
</div>
</form>
);
};
@@ -0,0 +1 @@
export { OrgNameChangeSection } from './OrgNameChangeSection';
@@ -0,0 +1,3 @@
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
export { OrgMembersTable } from './OrgMembersTable';
export { OrgNameChangeSection } from './OrgNameChangeSection';
@@ -0,0 +1 @@
export { OrgSettingsPage } from './OrgSettingsPage';
@@ -0,0 +1,63 @@
import { useEffect, useState } from 'react';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import { Checkbox } from '@app/components/v2';
import { useGetUser } from '../../../../hooks/api';
import { User } from '../../../../hooks/api/types';
import updateMyMfaEnabled from '../../../../pages/api/user/updateMyMfaEnabled';
export const SecuritySection = () => {
const [isMfaEnabled, setIsMfaEnabled] = useState(false);
const { data: user } = useGetUser();
const { createNotification } = useNotificationContext();
useEffect(() => {
if (user && typeof user.isMfaEnabled !== 'undefined') {
setIsMfaEnabled(user.isMfaEnabled);
}
}, [user]);
const toggleMfa = async (state: boolean) => {
try {
const newUser: User = await updateMyMfaEnabled({
isMfaEnabled: state
});
if (newUser) {
setIsMfaEnabled(newUser.isMfaEnabled);
}
createNotification({
text: `${newUser.isMfaEnabled ? 'Successfully turned on two-factor authentication.' : 'Successfully turned off two-factor authentication.'}`,
type: 'success'
});
} catch (err) {
createNotification({
text: 'Something went wrong while toggling the two-factor authentication.',
type: 'error'
});
console.error(err);
}
}
return (
<form>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">
Two-factor Authentication
</p>
<Checkbox
className="data-[state=checked]:bg-primary"
id="isTwoFAEnabled"
isChecked={isMfaEnabled}
onCheckedChange={(state) => {
toggleMfa(state as boolean);
}}
>
Enable 2-factor authentication via your personal email.
</Checkbox>
</div>
</form>
);
};
@@ -0,0 +1 @@
export { SecuritySection } from './SecuritySection';
@@ -58,7 +58,7 @@ export const ProjectSettingsPage = () => {
const renameWorkspace = useRenameWorkspace();
const toggleAutoCapitalization = useToggleAutoCapitalization();
const deleteWorkspace = useDeleteWorkspace();
// env crud operation
const createWsEnv = useCreateWsEnvironment();
@@ -97,7 +97,7 @@ export const ProjectSettingsPage = () => {
}
};
const onAutoCapitalizationToggle = async (state: boolean) => {
const onAutoCapitalizationToggle = async (state: boolean) => {
try {
await toggleAutoCapitalization.mutateAsync({
workspaceID,
@@ -123,6 +123,9 @@ export const ProjectSettingsPage = () => {
await deleteWorkspace.mutateAsync({ workspaceID });
// redirect user to first workspace user is part of
const ws = workspaces.find(({ _id }) => _id !== workspaceID);
if (!ws) {
router.push('/noprojects');
}
router.push(`/dashboard/${ws?._id}`);
createNotification({
text: 'Successfully deleted workspace',
@@ -247,7 +250,7 @@ export const ProjectSettingsPage = () => {
const res = await createWsTag.mutateAsync({
workspaceID,
tagName: name,
tagSlug: name.replace(" ", "_")
tagSlug: name.replace(' ', '_')
});
createNotification({
text: 'Successfully created a tag',
@@ -314,10 +317,6 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name}
onProjectNameChange={onRenameWorkspace}
/>
<AutoCapitalizationSection
workspaceAutoCapitalization={currentWorkspace?.autoCapitalization}
onAutoCapitalizationChange={onAutoCapitalizationToggle}
/>
<CopyProjectIDSection workspaceID={currentWorkspace?._id || ''} />
<EnvironmentSection
environments={currentWorkspace?.environments || []}
@@ -339,6 +338,10 @@ export const ProjectSettingsPage = () => {
workspaceName={currentWorkspace?.name || ''}
onCreateTag={onCreateWsTag}
/>
<AutoCapitalizationSection
workspaceAutoCapitalization={currentWorkspace?.autoCapitalization}
onAutoCapitalizationChange={onAutoCapitalizationToggle}
/>
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md border-l border-red bg-white/5 px-6 pl-6 pb-4 pt-4">
<p className="text-xl font-bold text-red">{t('settings-project:danger-zone')}</p>
<p className="text-md mt-2 text-gray-400">{t('settings-project:danger-zone-note')}</p>
@@ -14,7 +14,7 @@ export const AutoCapitalizationSection = ({
const { t } = useTranslation();
return (
<form>
<div className="mb-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<p className="mb-4 mt-2 text-xl font-semibold">
{t('settings-project:auto-capitalization')}
</p>