mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 15:29:46 +00:00
Merge branch 'main' into issue-318
This commit is contained in:
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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']);
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { Tag } from './Tag';
|
||||
@@ -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';
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
export { useGetAuthToken } from './queries';
|
||||
export {
|
||||
useGetAuthToken,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken} from './queries'
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
export * from './auth';
|
||||
export * from './incidentContacts';
|
||||
export * from './keys';
|
||||
export * from './organization';
|
||||
export * from './serviceTokens';
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
export {
|
||||
fetchOrgUsers,
|
||||
useAddUserToOrg,
|
||||
useAddUserToWs,
|
||||
useDeleteOrgMembership,
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
useLogoutUser
|
||||
useLogoutUser,
|
||||
useUpdateOrgUserRole
|
||||
} from './queries';
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@ export {
|
||||
useCreateWsEnvironment,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWorkspaces,
|
||||
useGetWorkspaceById,
|
||||
useRenameWorkspace,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateWsEnvironment} from './queries';
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
export { useLeaveConfirm } from './useLeaveConfirm';
|
||||
export { usePopUp } from './usePopUp';
|
||||
export { useToggle } from './useToggle';
|
||||
export { useLeaveConfirm } from './useLeaveConfirm';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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']);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function RequestNewInvite() {
|
||||
doesn'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>
|
||||
|
||||
@@ -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')!,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+176
@@ -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>
|
||||
);
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
|
||||
+275
@@ -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'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';
|
||||
+68
@@ -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>
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user