Begin marking endpoints for deprecation, clean unused frontend code

This commit is contained in:
Tuan Dang
2023-08-09 16:45:52 +07:00
parent 11833ccf0f
commit 2a1665a2c3
37 changed files with 135 additions and 1929 deletions

View File

@@ -260,22 +260,6 @@ export const createOrganizationPortalSession = async (
}
};
/**
* Return organization subscriptions
* @param req
* @param res
* @returns
*/
export const getOrganizationSubscriptions = async (
req: Request,
res: Response
) => {
return res.status(200).send({
subscriptions: []
});
};
/**
* Given a org id, return the projects each member of the org belongs to
* @param req

View File

@@ -148,20 +148,6 @@ router.post(
organizationController.createOrganizationPortalSession
);
router.get(
"/:organizationId/subscriptions", // TODO endpoint: deprecate
requireAuth({
acceptedAuthModes: [AuthMode.JWT],
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED],
}),
param("organizationId").exists().trim(),
validateRequest,
organizationController.getOrganizationSubscriptions
);
router.get(
"/:organizationId/workspace-memberships",
requireAuth({

View File

@@ -1,271 +0,0 @@
import crypto from "crypto";
import { Fragment, useState } from "react";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Dialog, Transition } from "@headlessui/react";
import addServiceToken from "@app/pages/api/serviceToken/addServiceToken";
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
import { decryptAssymmetric, encryptSymmetric } from "../../utilities/cryptography/crypto";
import Button from "../buttons/Button";
import InputField from "../InputField";
import ListBox from "../Listbox";
const expiryMapping = {
"1 day": 86400,
"7 days": 604800,
"1 month": 2592000,
"6 months": 15552000,
"12 months": 31104000
};
type Props = {
isOpen: boolean;
closeModal: () => void;
workspaceId: string;
workspaceName: string;
serviceTokens: any[];
environments: Array<{ name: string; slug: string }>;
setServiceTokens: (arg: any[]) => void;
};
const AddServiceTokenDialog = ({
isOpen,
closeModal,
workspaceId,
workspaceName,
serviceTokens,
environments,
setServiceTokens
}: Props) => {
const [serviceToken, setServiceToken] = useState("");
const [serviceTokenName, setServiceTokenName] = useState("");
const [selectedServiceTokenEnv, setSelectedServiceTokenEnv] = useState(environments?.[0]);
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day");
const [serviceTokenCopied, setServiceTokenCopied] = useState(false);
const { t } = useTranslation();
const generateServiceToken = async () => {
const latestFileKey = await getLatestFileKey({ workspaceId });
const key = decryptAssymmetric({
ciphertext: latestFileKey.latestKey.encryptedKey,
nonce: latestFileKey.latestKey.nonce,
publicKey: latestFileKey.latestKey.sender.publicKey,
privateKey: localStorage.getItem("PRIVATE_KEY") as string
});
const randomBytes = crypto.randomBytes(16).toString("hex");
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: key,
key: randomBytes
});
console.log(
1234,
selectedServiceTokenEnv,
environments,
selectedServiceTokenEnv?.slug ? selectedServiceTokenEnv.slug : environments[0]?.slug
);
const newServiceToken = await addServiceToken({
name: serviceTokenName,
workspaceId,
environment: selectedServiceTokenEnv?.slug
? selectedServiceTokenEnv.slug
: environments[0]?.slug,
expiresIn: expiryMapping[serviceTokenExpiresIn as keyof typeof expiryMapping],
encryptedKey: ciphertext,
iv,
tag
});
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
setServiceToken(`${newServiceToken.serviceToken}.${randomBytes}`);
};
function copyToClipboard() {
// Get the text field
const copyText = document.getElementById("serviceToken") as HTMLInputElement;
// Select the text field
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
// Copy the text inside the text field
navigator.clipboard.writeText(copyText.value);
setServiceTokenCopied(true);
setTimeout(() => setServiceTokenCopied(false), 2000);
// Alert the copied text
// alert("Copied the text: " + copyText.value);
}
const closeAddServiceTokenModal = () => {
closeModal();
setServiceTokenName("");
setServiceToken("");
};
return (
<div className="z-50">
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
{serviceToken === "" ? (
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400"
>
{t("section.token.add-dialog.title", {
target: workspaceName
})}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
{t("section.token.add-dialog.description")}
</p>
</div>
</div>
<div className="mb-2 max-h-28">
<InputField
label={t("section.token.add-dialog.name")}
onChangeHandler={setServiceTokenName}
type="varName"
value={serviceTokenName}
placeholder=""
isRequired
/>
</div>
<div className="mb-2 max-h-28">
<ListBox
isSelected={
selectedServiceTokenEnv?.name
? selectedServiceTokenEnv?.name
: environments[0]?.name
}
data={environments.map(({ name }) => name)}
onChange={(envName) =>
setSelectedServiceTokenEnv(
environments.find(({ name }) => envName === name) || {
name: "unknown",
slug: "unknown"
}
)
}
isFull
text={`${t("common.environment")}: `}
/>
</div>
<div className="max-h-28">
<ListBox
isSelected={serviceTokenExpiresIn}
onChange={setServiceTokenExpiresIn}
data={["1 day", "7 days", "1 month", "6 months", "12 months"]}
isFull
text={`${t("common.expired-in")}: `}
/>
</div>
<div className="max-w-max">
<div className="mt-6 flex w-max flex-col justify-start">
<Button
onButtonPressed={() => generateServiceToken()}
color="mineshaft"
text={t("section.token.add-dialog.add") as string}
textDisabled={t("section.token.add-dialog.add") as string}
size="md"
active={serviceTokenName !== ""}
/>
</div>
</div>
</Dialog.Panel>
) : (
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400"
>
{t("section.token.add-dialog.copy-service-token")}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
{t("section.token.add-dialog.copy-service-token-description")}
</p>
</div>
</div>
<div className="w-full">
<div className="mt-2 mr-2 flex h-20 w-full items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
<input
type="text"
value={serviceToken}
id="serviceToken"
className="invisible w-full min-w-full bg-white/0 py-2 px-2 text-gray-400 outline-none"
/>
<div className="w-full max-w-md break-words bg-white/0 py-2 pl-14 pr-2 text-sm text-gray-400 outline-none">
{serviceToken}
</div>
<div className="group relative inline-block h-full font-normal text-gray-400 underline duration-200 hover:text-primary">
<button
onClick={copyToClipboard}
type="button"
className="h-full border-l border-white/20 py-2 pl-3.5 pr-4 duration-200 hover:bg-white/[0.12]"
>
{serviceTokenCopied ? (
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-chicago-900 px-3 py-2 text-center text-sm text-gray-400 duration-300 group-hover:flex group-hover:animate-popup">
{t("common.click-to-copy")}
</span>
</div>
</div>
</div>
<div className="mt-6 flex w-max flex-col justify-start">
<Button
onButtonPressed={() => closeAddServiceTokenModal()}
color="mineshaft"
text="Close"
size="md"
/>
</div>
</Dialog.Panel>
)}
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
export default AddServiceTokenDialog;

View File

@@ -1,184 +0,0 @@
import { useEffect, useState } from "react";
import { faPencil, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
import { plans } from "public/data/frequentConstants";
import { usePopUp } from "../../../hooks/usePopUp";
import getOrganizationSubscriptions from "../../../pages/api/organization/GetOrgSubscription";
import Button from "../buttons/Button";
import { AddUpdateEnvironmentDialog } from "../dialog/AddUpdateEnvironmentDialog";
import DeleteActionModal from "../dialog/DeleteActionModal";
import UpgradePlanModal from "../dialog/UpgradePlan";
type Env = { name: string; slug: string };
type Props = {
data: Env[];
onCreateEnv: (arg0: Env) => Promise<void>;
onUpdateEnv: (oldSlug: string, arg0: Env) => Promise<void>;
onDeleteEnv: (slug: string) => Promise<void>;
};
const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"createUpdateEnv",
"deleteEnv",
"upgradePlan"
] as const);
const [plan, setPlan] = useState("");
const host = window.location.origin;
useEffect(() => {
// on initial load - run auth check
(async () => {
const orgId = localStorage.getItem("orgData.id") as string;
const subscriptions = await getOrganizationSubscriptions({
orgId
});
if (subscriptions) {
setPlan(subscriptions.data[0].plan.product)
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onEnvCreateCB = async (env: Env) => {
try {
await onCreateEnv(env);
handlePopUpClose("createUpdateEnv");
} catch (error) {
console.error(error);
}
};
const onEnvUpdateCB = async (env: Env) => {
try {
await onUpdateEnv((popUp.createUpdateEnv?.data as Pick<Env, "slug">)?.slug, env);
handlePopUpClose("createUpdateEnv");
} catch (error) {
console.error(error);
}
};
const onEnvDeleteCB = async () => {
try {
await onDeleteEnv((popUp.deleteEnv?.data as Pick<Env, "slug">)?.slug);
handlePopUpClose("deleteEnv");
} catch (error) {
console.error(error);
}
};
return (
<>
<div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">Project Environments</p>
<p className="mb-4 text-base text-gray-400">
Choose which environments will show up in your dashboard like development, staging,
production
</p>
<p className="mr-1 self-start text-sm text-gray-500">
Note: the text in slugs shows how these environmant should be accessed in CLI.
</p>
</div>
<div className="w-48">
<Button
text="Add New Environment"
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com" || localStorage.getItem("projectData.id") === "63ea8121b6e2b0543ba79616") {
handlePopUpOpen("createUpdateEnv");
} else {
handlePopUpOpen("upgradePlan");
}
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<div className="table-container relative mb-6 mt-1 w-full rounded-md border border-mineshaft-700 bg-bunker">
<div className="absolute h-12 w-full rounded-t-md bg-white/5" />
<table className="my-1 w-full">
<thead className="text-bunker-300">
<tr>
<th className="pl-6 pt-2.5 pb-2 text-left">Name</th>
<th className="pl-6 pt-2.5 pb-2 text-left">Slug</th>
<th aria-label="buttons" />
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data.map(({ name, slug }) => (
<tr key={name} className="bg-bunker-800 duration-100 hover:bg-bunker-800/5">
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{name}</td>
<td className="border-t border-mineshaft-700 py-2 pl-6 text-gray-300">{slug}</td>
<td className="flex justify-end border-t border-mineshaft-700 py-2">
<div className="mr-2 flex items-center duration-200 hover:opacity-100">
<Button
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen("createUpdateEnv", { name, slug });
} else {
handlePopUpOpen("upgradePlan");
}
}}
color="mineshaft"
size="icon-sm"
icon={faPencil}
/>
</div>
<div className="mr-6 flex items-center opacity-50 duration-200 hover:opacity-100">
<Button
onButtonPressed={() => {
if (plan !== plans.starter || host !== "https://app.infisical.com") {
handlePopUpOpen("deleteEnv", { name, slug });
} else {
handlePopUpOpen("upgradePlan");
}
}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan={4} className="pt-7 pb-4 text-center text-bunker-400">
No environments found
</td>
</tr>
)}
</tbody>
</table>
<DeleteActionModal
isOpen={popUp.deleteEnv.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteEnv?.data as { name: string })?.name || " "
}?`}
deleteKey={(popUp?.deleteEnv?.data as { slug: string })?.slug || ""}
onClose={() => handlePopUpClose("deleteEnv")}
onSubmit={onEnvDeleteCB}
/>
<AddUpdateEnvironmentDialog
isOpen={popUp.createUpdateEnv.isOpen}
isEditMode={Boolean(popUp.createUpdateEnv?.data)}
initialValues={popUp?.createUpdateEnv?.data as any}
onClose={() => handlePopUpClose("createUpdateEnv")}
onCreateSubmit={onEnvCreateCB}
onEditSubmit={onEnvUpdateCB}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onClose={() => handlePopUpClose("upgradePlan")}
text="You can add custom environments if you switch to Infisical's Team plan."
/>
</div>
</>
);
};
export default EnvironmentTable;

View File

@@ -1,98 +0,0 @@
import { faX } from "@fortawesome/free-solid-svg-icons";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken";
import guidGenerator from "../../utilities/randomId";
import Button from "../buttons/Button";
interface TokenProps {
_id: string;
name: string;
environment: string;
expiresAt: string;
}
interface ServiceTokensProps {
data: TokenProps[];
workspaceName: string;
setServiceTokens: (value: TokenProps[]) => void;
}
/**
* This is the component that we utilize for the service token table
* #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {object} obj
* @param {any[]} obj.data - current state of the service token table
* @param {string} obj.workspaceName - name of the current project
* @param {function} obj.setServiceTokens - updating the state of the service token table
* @returns
*/
const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTokensProps) => {
const { createNotification } = useNotificationContext();
return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
<table className="w-full my-1">
<thead className="text-bunker-300 text-sm font-light">
<tr>
<th className="text-left pl-6 pt-2.5 pb-2">TOKEN NAME</th>
<th className="text-left pl-6 pt-2.5 pb-2">PROJECT</th>
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th>
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th>
<th aria-label="button" />
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data?.map((row) => (
<tr
key={guidGenerator()}
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{workspaceName}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.environment}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}
</td>
<td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button
onButtonPressed={() => {
deleteServiceToken({ serviceTokenId: row._id });
setServiceTokens(data.filter((token) => token._id !== row._id));
createNotification({
text: `'${row.name}' token has been revoked.`,
type: "error"
});
}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
No service tokens yet
</td>
</tr>
)}
</tbody>
</table>
</div>
);
};
export default ServiceTokenTable;

View File

@@ -1,234 +0,0 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { faX } from "@fortawesome/free-solid-svg-icons";
import changeUserRoleInOrganization from "@app/pages/api/organization/changeUserRoleInOrganization";
import deleteUserFromOrganization from "@app/pages/api/organization/deleteUserFromOrganization";
import getOrganizationProjectMemberships from "@app/pages/api/organization/GetOrgProjectMemberships";
import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace";
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
import uploadKeys from "@app/pages/api/workspace/uploadKeys";
import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto";
import guidGenerator from "../../utilities/randomId";
import Button from "../buttons/Button";
import Listbox from "../Listbox";
// const roles = ['admin', 'user'];
// TODO: Set type for this
type Props = {
userData: any[];
changeData: (users: any[]) => void;
myUser: string;
filter: string;
resendInvite: (email: string) => void;
isOrg: boolean;
};
/**
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too.
* #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {*} props
* @returns
*/
const UserTable = ({ userData, changeData, myUser, filter, resendInvite, isOrg }: Props) => {
const [roleSelected, setRoleSelected] = useState(
Array(userData?.length).fill(userData.map((user) => user.role))
);
const router = useRouter();
const [myRole, setMyRole] = useState("member");
const [userProjectMemberships, setUserProjectMemberships] = useState<any[]>([]);
const workspaceId = router.query.id as string;
// Delete the row in the table (e.g. a user)
// #TODO: Add a pop-up that warns you that the user is going to be deleted.
const handleDelete = (membershipId: string, index: number) => {
// setUserIdToBeDeleted(userId);
// onClick();
if (isOrg) {
deleteUserFromOrganization(membershipId);
} else {
deleteUserFromWorkspace(membershipId);
}
changeData(userData.filter((v, i) => i !== index));
setRoleSelected([
...roleSelected.slice(0, index),
...roleSelected.slice(index + 1, userData?.length)
]);
};
// Update the role of a certain user
const handleRoleUpdate = (index: number, e: string) => {
changeUserRoleInOrganization(String(localStorage.getItem("orgData.id")), userData[index].membershipId, e);
changeData([
...userData.slice(0, index),
...[
{
key: userData[index].key,
firstName: userData[index].firstName,
lastName: userData[index].lastName,
email: userData[index].email,
role: e,
status: userData[index].status,
userId: userData[index].userId,
membershipId: userData[index].membershipId,
publicKey: userData[index].publicKey
}
],
...userData.slice(index + 1, userData?.length)
]);
};
useEffect(() => {
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
(async () => {
const result = await getOrganizationProjectMemberships({ orgId: String(localStorage.getItem("orgData.id"))})
setUserProjectMemberships(result);
})();
}, [userData, myUser]);
const grantAccess = async (id: string, publicKey: string) => {
const result = await getLatestFileKey({ workspaceId });
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: key,
publicKey,
privateKey: PRIVATE_KEY
});
uploadKeys(workspaceId, id, ciphertext, nonce);
router.reload();
};
const deleteMembershipAndResendInvite = (email: string) => {
// deleteUserFromWorkspace(membershipId);
resendInvite(email);
};
return (
<div className="table-container bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1 min-w-max w-full">
<div className="absolute rounded-t-md w-full h-[3.25rem] bg-white/5" />
<table className="w-full my-0.5">
<thead className="text-gray-400 text-sm font-light">
<tr>
<th className="text-left pl-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>
<th className="text-left pl-6 pr-10 py-3.5">PROJECTS</th>
<th aria-label="buttons" />
</tr>
</thead>
<tbody>
{userData?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
).length > 0 &&
userData
?.filter(
(user) =>
user.firstName?.toLowerCase().includes(filter) ||
user.lastName?.toLowerCase().includes(filter) ||
user.email?.toLowerCase().includes(filter)
)
.map((row, index) => (
<tr key={guidGenerator()} className="bg-bunker-800 hover:bg-bunker-700">
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
{row.firstName} {row.lastName}
</td>
<td className="pl-4 py-2 border-mineshaft-700 border-t 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">
{row.status === "accepted" &&
((myRole === "admin" && row.role !== "owner") || myRole === "owner") &&
(myUser !== row.email) ? (
<Listbox
isSelected={row.role}
onChange={(e) => handleRoleUpdate(index, e)}
data={
myRole === "owner" ? ["owner", "admin", "member"] : ["admin", "member"]
}
/>
) : (
row.status !== "invited" &&
row.status !== "verified" && (
<Listbox
isSelected={row.role}
onChange={() => {
throw new Error("Function not implemented.");
}}
data={null}
/>
)
)}
{(row.status === "invited" || row.status === "verified") && (
<div className="w-full pr-20">
<Button
onButtonPressed={() => deleteMembershipAndResendInvite(row.email)}
color="mineshaft"
text="Resend Invite"
size="md"
/>
</div>
)}
{row.status === "completed" && myUser !== row.email && (
<div className="border border-mineshaft-700 rounded-md bg-white/5 hover:bg-primary text-white hover:text-black duration-200">
<Button
onButtonPressed={() => grantAccess(row.userId, row.publicKey)}
color="mineshaft"
text="Grant Access"
size="md"
/>
</div>
)}
</div>
</td>
<td className="pl-4 py-2 border-mineshaft-700 border-t text-gray-300">
<div className="flex items-center max-h-16 overflow-x-auto w-full max-w-xl break-all">
{userProjectMemberships[row.userId]
? userProjectMemberships[row.userId]?.map((project: any) => (
<div key={project._id} className='mx-1 min-w-max px-1.5 bg-mineshaft-500 rounded-sm text-sm text-bunker-200 flex items-center'>
<span className='mb-0.5 cursor-default'>{project.name}</span>
</div>
))
: <span className='ml-1 text-bunker-100 rounded-sm px-1 py-0.5 text-sm bg-red/80'>This user isn&apos;t part of any projects yet.</span>}
</div>
</td>
<td className="flex flex-row justify-end pl-8 pr-8 py-2 border-t border-0.5 border-mineshaft-700">
{myUser !== row.email &&
// row.role !== "admin" &&
myRole !== "member" ? (
<div className="opacity-50 hover:opacity-100 flex items-center mt-0.5">
<Button
onButtonPressed={() => handleDelete(row.membershipId, index)}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
) : (
<div className="w-9 h-9" />
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default UserTable;

View File

@@ -1,88 +0,0 @@
import { SetStateAction, useEffect, useState } from "react";
import Image from "next/image";
import { WorkspaceEnv } from "@app/hooks/api/types";
import getSecretsForProject from "../utilities/secrets/getSecretsForProject";
import { Modal, ModalContent } from "../v2";
interface Secrets {
label: string;
secret: string;
}
interface CompareSecretsModalProps {
compareModal: boolean;
setCompareModal: React.Dispatch<SetStateAction<boolean>>;
selectedEnv: WorkspaceEnv;
workspaceEnvs: WorkspaceEnv[];
workspaceId: string;
currentSecret: {
key: string;
value: string;
};
}
const CompareSecretsModal = ({
compareModal,
setCompareModal,
selectedEnv,
workspaceEnvs,
workspaceId,
currentSecret
}: CompareSecretsModalProps) => {
const [secrets, setSecrets] = useState<Secrets[]>([]);
const getEnvSecrets = async () => {
const workspaceEnvironments = workspaceEnvs?.filter((env) => env !== selectedEnv);
const newSecrets = await Promise.all(
workspaceEnvironments.map(async (env) => {
// #TODO: optimize this query somehow...
const allSecrets = await getSecretsForProject({ env: env.slug, workspaceId });
const secret =
allSecrets.find((item) => item.key === currentSecret.key)?.value ?? "Not found";
return { label: env.name, secret };
})
);
setSecrets([{ label: selectedEnv.name, secret: currentSecret.value }, ...newSecrets]);
};
useEffect(() => {
if (compareModal) {
(async () => {
await getEnvSecrets();
})();
}
}, [compareModal]);
return (
<Modal isOpen={compareModal} onOpenChange={setCompareModal}>
<ModalContent title={currentSecret?.key} onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="space-y-4">
{secrets.length === 0 ? (
<div className="flex items-center bg-bunker-900 justify-center h-full py-4 rounded-md">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
/>
</div>
) : (
secrets.map((item) => (
<div key={`${currentSecret.key}${item.label}`} className="space-y-0.5">
<p className="text-sm text-bunker-300">{item.label}</p>
<input
defaultValue={item.secret}
className="h-no-capture border border-mineshaft-500 text-md min-w-16 no-scrollbar::-webkit-scrollbar peer z-10 w-full rounded-md bg-bunker-800 px-2 py-1.5 font-mono text-gray-400 caret-white outline-none duration-200 no-scrollbar focus:ring-2 focus:ring-primary/50 "
readOnly
/>
</div>
))
)}
</div>
</ModalContent>
</Modal>
);
};
export default CompareSecretsModal;

View File

@@ -1,222 +0,0 @@
/* eslint-disable react/no-unused-prop-types */
import { useState } from "react";
import { useTranslation } from "react-i18next";
import Image from "next/image";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import SecretVersionList from "@app/ee/components/SecretVersionList";
import { WorkspaceEnv } from "@app/hooks/api/types";
import Button from "../basic/buttons/Button";
import Toggle from "../basic/Toggle";
import CommentField from "./CommentField";
import CompareSecretsModal from "./CompareSecretsModal";
import DashboardInputField from "./DashboardInputField";
import { DeleteActionButton } from "./DeleteActionButton";
import GenerateSecretMenu from "./GenerateSecretMenu";
interface SecretProps {
key: string;
value: string | undefined;
valueOverride: string | undefined;
pos: number;
id: string;
comment: string;
}
export interface DeleteRowFunctionProps {
ids: string[];
secretName: string;
}
interface SideBarProps {
toggleSidebar: (value: string) => void;
data: SecretProps[];
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[];
setSharedToHide: (values: string[]) => void;
deleteRow: (props: DeleteRowFunctionProps) => void;
workspaceEnvs: WorkspaceEnv[];
selectedEnv: WorkspaceEnv;
workspaceId: string;
}
/**
* @param {object} obj
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
* @param {SecretProps[]} obj.data - data of a certain key valeu pair
* @param {function} obj.modifyKey - function that modifies the secret key
* @param {function} obj.modifyValue - function that modifies the secret value
* @param {function} obj.modifyValueOverride - function that modifies the secret value if it is an override
* @param {boolean} obj.buttonReady - is the button for saving chagnes active
* @param {function} obj.savePush - save changes andp ush secrets
* @param {function} obj.deleteRow - a function to delete a certain keyPair
* @returns the sidebar with 'secret's settings'
*/
const SideBar = ({
toggleSidebar,
data,
modifyKey,
modifyValue,
modifyValueOverride,
modifyComment,
buttonReady,
savePush,
deleteRow,
workspaceEnvs,
selectedEnv,
workspaceId
}: SideBarProps) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoading, setIsLoading] = useState(false);
const [overrideEnabled, setOverrideEnabled] = useState(data[0]?.valueOverride !== undefined);
const [compareModal, setCompareModal] = useState(false);
const { t } = useTranslation();
return (
<div className="min-w-sm absolute sticky top-0 right-0 z-[70] flex h-full w-full max-w-sm flex-col justify-between border-l border-mineshaft-500 bg-bunker shadow-xl">
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<Image
src="/images/loading/loading.gif"
height={60}
width={100}
alt="infisical loading indicator"
/>
</div>
) : (
<div className="h-min w-full overflow-y-auto">
<div className="flex flex-row items-center justify-between border-b border-mineshaft-500 px-4 py-3">
<p className="text-lg font-semibold text-bunker-200">{t("dashboard.sidebar.secret")}</p>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
className="p-1"
onClick={() => toggleSidebar("None")}
>
<FontAwesomeIcon icon={faXmark} className="h-4 w-4 cursor-pointer text-bunker-300" />
</div>
</div>
<div className="pointer-events-none mt-4 px-4">
<p className="text-sm text-bunker-300">{t("dashboard.sidebar.key")}</p>
<div className="overflow-hidden rounded-md border border-mineshaft-600 bg-white/5">
<DashboardInputField
onChangeHandler={modifyKey}
type="varName"
id={data[0]?.id}
value={data[0]?.key}
isDuplicate={false}
blurred={false}
/>
</div>
</div>
{data[0]?.value || data[0]?.value === "" ? (
<div
className={`relative mt-2 px-4 ${
overrideEnabled && "pointer-events-none opacity-40"
} duration-200`}
>
<p className="text-sm text-bunker-300">{t("dashboard.sidebar.value")}</p>
<div className="overflow-hidden rounded-md border border-mineshaft-600 bg-white/5">
<DashboardInputField
onChangeHandler={modifyValue}
type="value"
id={data[0].id}
value={data[0]?.value}
isDuplicate={false}
blurred
/>
</div>
<div className="absolute right-[1.07rem] top-[1.6rem] z-50 bg-bunker-800">
<GenerateSecretMenu modifyValue={modifyValue} id={data[0]?.id} />
</div>
</div>
) : (
<div className="px-4 pt-4 text-sm text-bunker-300">
<span className="mr-1 rounded-md bg-primary-200/10 py-0.5 px-1">
{t("common.note")}:
</span>
{t("dashboard.sidebar.personal-explanation")}
</div>
)}
<div className="mt-4 px-4">
{(data[0]?.value || data[0]?.value === "") && (
<div className="my-2 flex flex-row items-center justify-between pl-1 pr-2">
<p className="text-sm text-bunker-300">{t("dashboard.sidebar.override")}</p>
<Toggle
enabled={overrideEnabled}
setEnabled={setOverrideEnabled}
addOverride={modifyValueOverride}
id={data[0]?.id}
/>
</div>
)}
<div
className={`relative ${
!overrideEnabled && "pointer-events-none opacity-40"
} duration-200`}
>
<div className="overflow-hidden rounded-md border border-mineshaft-600 bg-white/5">
<DashboardInputField
onChangeHandler={modifyValueOverride}
type="value"
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} id={data[0]?.id} />
</div>
</div>
</div>
<SecretVersionList secretId={data[0]?.id} />
<CommentField comment={data[0]?.comment} modifyComment={modifyComment} id={data[0]?.id} />
</div>
)}
<div className="mt-full mt-4 mb-4 flex w-96 max-w-sm flex-col justify-start space-y-2 px-4">
<div>
<Button
text="Compare secret across environments"
color="mineshaft"
size="md"
onButtonPressed={() => setCompareModal(true)}
/>
<CompareSecretsModal
compareModal={compareModal}
setCompareModal={setCompareModal}
currentSecret={{ key: data[0]?.key, value: data[0]?.value ?? "" }}
workspaceEnvs={workspaceEnvs}
selectedEnv={selectedEnv}
workspaceId={workspaceId}
/>
</div>
<div className="flex">
<Button
text={String(t("common.save-changes"))}
onButtonPressed={savePush}
color="primary"
size="md"
active={buttonReady}
textDisabled="Saved"
/>
<DeleteActionButton
onSubmit={() =>
deleteRow({ ids: data.map((secret) => secret.id), secretName: data[0]?.key })
}
/>
</div>
</div>
</div>
);
};
export default SideBar;

View File

@@ -1,160 +0,0 @@
import { Tag } from "public/data/frequentInterfaces";
import getSecrets from "@app/pages/api/files/GetSecrets";
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
import { decryptAssymmetric, decryptSymmetric } from "../cryptography/crypto";
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[];
}
interface FunctionProps {
env: string;
setIsKeyAvailable?: any;
setData?: any;
workspaceId: string;
}
/**
* Gets the secrets for a certain project
* @param {object} obj
* @param {string} obj.env - environment for which we are getting secrets
* @param {boolean} obj.isKeyAvailable - if a person is able to create new key pairs
* @param {function} obj.setData - state function that manages the state of secrets in the dashboard
* @param {string} obj.workspaceId - id of a workspace for which we are getting secrets
*/
const getSecretsForProject = async ({
env,
setIsKeyAvailable,
setData,
workspaceId
}: FunctionProps) => {
try {
let encryptedSecrets;
try {
encryptedSecrets = await getSecrets(workspaceId, env);
} catch (error) {
console.log("ERROR: Not able to access the latest version of secrets");
}
const latestKey = await getLatestFileKey({ workspaceId });
// This is called isKeyAvailable but what it really means is if a person is able to create new key pairs
if (typeof setIsKeyAvailable === "function") {
setIsKeyAvailable(!latestKey ? encryptedSecrets.length === 0 : true);
}
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
encryptedSecrets.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))];
const result = secretKeys.map((key, index) => ({
id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === "shared")[0]
?.id,
idOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === "personal"
)[0]?.id,
pos: index,
key,
value: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === "shared"
)[0]?.value,
valueOverride: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === "personal"
)[0]?.value,
comment: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === "shared"
)[0]?.comment,
tags: tempDecryptedSecrets.filter(
(secret) => secret.key === key && secret.type === "shared"
)[0]?.tags
}));
if (typeof setData === "function") {
setData(result);
}
return result;
} catch (error) {
console.log("Something went wrong during accessing or decripting secrets.");
}
return [];
};
export default getSecretsForProject;

View File

@@ -1,6 +1,6 @@
import { createContext, ReactNode, useContext, useMemo } from "react";
import { useGetOrganization } from "@app/hooks/api";
import { useGetOrganizations } from "@app/hooks/api";
import { Organization } from "@app/hooks/api/types";
@@ -17,8 +17,8 @@ type Props = {
};
export const OrgProvider = ({ children }: Props): JSX.Element => {
const { data: userOrgs, isLoading } = useGetOrganization();
const { data: userOrgs, isLoading } = useGetOrganizations();
// const currentWsOrgID = currentWorkspace?.organization;
const currentWsOrgID = localStorage.getItem("orgData.id");

View File

@@ -2,9 +2,9 @@ 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 { createSecret } from "@app/hooks/api/secrets/queries";
import { createWorkspace } from "@app/hooks/api/workspace/queries";
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 = [
@@ -95,10 +95,12 @@ const initProjectHelper = async ({
try {
// create new project
project = await createWorkspace({
const { data: { workspace } } = await createWorkspace({
workspaceName: projectName,
organizationId
});
project = workspace;
// create and upload new (encrypted) project key
const randomBytes = crypto.randomBytes(16).toString("hex");
@@ -116,19 +118,33 @@ const initProjectHelper = async ({
await uploadKeys(project._id, user._id, ciphertext, nonce);
const workspaceId = project._id;
// encrypt and upload secrets to new project
const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded,
workspaceId: project._id,
workspaceId,
env: "dev"
});
await addSecrets({
secrets: secrets ?? [],
env: "dev",
workspaceId: project._id
secrets?.forEach((secret) => {
createSecret({
workspaceId,
environment: secret.environment,
type: secret.type,
secretKey: secret.secretName,
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,
secretPath: "/"
});
});
} catch (err) {
console.error("Failed to init project in organization", err);
}

View File

@@ -8,29 +8,26 @@ const queryKeys = {
getBot: (workspaceId: string) => [{ workspaceId }, "bot"] as const
};
const fetchWorkspaceBot = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`);
return data.bot;
};
export const useGetWorkspaceBot = (workspaceId: string) =>
useQuery({
queryKey: queryKeys.getBot(workspaceId),
queryFn: () => fetchWorkspaceBot(workspaceId),
queryFn: async () => {
const { data: { bot } } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`);
return bot;
},
enabled: Boolean(workspaceId)
});
// mutation
export const useUpdateBotActiveStatus = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TSetBotActiveStatusDto>({
mutationFn: ({ botId, isActive, botKey }) =>
apiRequest.patch(`/api/v1/bot/${botId}/active`, {
mutationFn: ({ botId, isActive, botKey }) => {
return apiRequest.patch(`/api/v1/bot/${botId}/active`, {
isActive,
botKey
}),
});
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(queryKeys.getBot(workspaceId));
}

View File

@@ -4,7 +4,7 @@ export {
useCreateCustomerPortalSession,
useDeleteOrgPmtMethod,
useDeleteOrgTaxId,
useGetOrganization,
useGetOrganizations,
useGetOrgBillingDetails,
useGetOrgInvoices,
useGetOrgLicenses,

View File

@@ -12,10 +12,11 @@ import {
PmtMethod,
ProductsTable,
RenameOrgDTO,
TaxID} from "./types";
TaxID
} from "./types";
const organizationKeys = {
getUserOrganization: ["organization"] as const,
getUserOrganizations: ["organization"] as const,
getOrgPlanBillingInfo: (orgId: string) => [{ orgId }, "organization-plan-billing"] as const,
getOrgPlanTable: (orgId: string) => [{ orgId }, "organization-plan-table"] as const,
getOrgPlansTable: (orgId: string, billingCycle: "monthly" | "yearly") => [{ orgId, billingCycle }, "organization-plans-table"] as const,
@@ -26,13 +27,12 @@ const organizationKeys = {
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const
};
export const useGetOrganization = () => {
export const useGetOrganizations = () => {
return useQuery({
queryKey: organizationKeys.getUserOrganization,
queryKey: organizationKeys.getUserOrganizations,
queryFn: async () => {
const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization");
return data.organizations;
const { data: { organizations } } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization");
return organizations;
}
});
}
@@ -44,7 +44,7 @@ export const useRenameOrg = () => {
mutationFn: ({ newOrgName, orgId }) =>
apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }),
onSuccess: () => {
queryClient.invalidateQueries(organizationKeys.getUserOrganization);
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
}
});
};

View File

@@ -11,13 +11,13 @@ import { apiRequest } from "@app/config/request";
import { secretSnapshotKeys } from "../secretSnapshots/queries";
import {
BatchSecretDTO,
CreateSecretDTO,
DecryptedSecret,
EncryptedSecret,
EncryptedSecretVersion,
GetProjectSecretsDTO,
GetSecretVersionsDTO,
TGetProjectSecretsAllEnvDTO
} from "./types";
TGetProjectSecretsAllEnvDTO} from "./types";
export const secretKeys = {
// this is also used in secretSnapshot part
@@ -324,3 +324,24 @@ export const useBatchSecretsOp = () => {
}
});
};
export const createSecret = async (dto: CreateSecretDTO) => {
const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto);
return data;
}
export const useCreateSecret = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, CreateSecretDTO>({
mutationFn: async (dto) => {
const data = createSecret(dto);
return data;
},
onSuccess: (_, dto) => {
queryClient.invalidateQueries(
secretKeys.getProjectSecret(dto.workspaceId, dto.environment)
);
}
});
};

View File

@@ -147,3 +147,22 @@ export type TDeleteSecretsV3DTO = {
secretPath: string;
secretName: string;
};
// --- v3
export type CreateSecretDTO = {
workspaceId: string;
environment: string;
type: "shared" | "personal";
secretKey: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
secretPath: string;
}

View File

@@ -23,12 +23,13 @@ const fetchWorkspaceServiceTokens = async (workspaceID: string) => {
type UseGetWorkspaceServiceTokensProps = { workspaceID: string };
export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServiceTokensProps) =>
useQuery({
export const useGetUserWsServiceTokens = ({ workspaceID }: UseGetWorkspaceServiceTokensProps) => {
return useQuery({
queryKey: serviceTokenKeys.getAllWorkspaceServiceToken(workspaceID),
queryFn: () => fetchWorkspaceServiceTokens(workspaceID),
enabled: Boolean(workspaceID)
});
}
// mutation
export const useCreateServiceToken = () => {
@@ -36,6 +37,7 @@ export const useCreateServiceToken = () => {
return useMutation<CreateServiceTokenRes, {}, CreateServiceTokenDTO>({
mutationFn: async (body) => {
console.log("useCreateServiceToken");
const { data } = await apiRequest.post("/api/v2/service-token/", body);
data.serviceToken += `.${body.randomBytes}`;
return data;
@@ -51,6 +53,7 @@ export const useDeleteServiceToken = () => {
return useMutation<DeleteServiceTokenRes, {}, string>({
mutationFn: async (serviceTokenId) => {
console.log("useDeleteServiceToken");
const { data } = await apiRequest.delete(`/api/v2/service-token/${serviceTokenId}`);
return data;
},

View File

@@ -166,13 +166,21 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
enabled: Boolean(workspaceId)
});
// mutation
export const createWorkspace = ({
organizationId,
workspaceName
}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => {
return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId });
}
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ organizationId, workspaceName }) =>
apiRequest.post("/api/v1/workspace", { workspaceName, organizationId }),
mutationFn: async ({ organizationId, workspaceName }) => createWorkspace({
organizationId,
workspaceName
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
@@ -183,8 +191,9 @@ export const useRenameWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, RenameWorkspaceDTO>({
mutationFn: ({ workspaceID, newWorkspaceName }) =>
apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName }),
mutationFn: ({ workspaceID, newWorkspaceName }) => {
return apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName });
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
@@ -220,11 +229,12 @@ export const useCreateWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, CreateEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentName, environmentSlug }) =>
apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, {
mutationFn: ({ workspaceID, environmentName, environmentSlug }) => {
return apiRequest.post(`/api/v2/workspace/${workspaceID}/environments`, {
environmentName,
environmentSlug
}),
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
@@ -235,12 +245,13 @@ export const useUpdateWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) =>
apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, {
mutationFn: ({ workspaceID, environmentName, environmentSlug, oldEnvironmentSlug }) => {
return apiRequest.put(`/api/v2/workspace/${workspaceID}/environments`, {
environmentName,
environmentSlug,
oldEnvironmentSlug
}),
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
@@ -251,10 +262,11 @@ export const useDeleteWsEnvironment = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteEnvironmentDTO>({
mutationFn: ({ workspaceID, environmentSlug }) =>
apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, {
mutationFn: ({ workspaceID, environmentSlug }) => {
return apiRequest.delete(`/api/v2/workspace/${workspaceID}/environments`, {
data: { environmentSlug }
}),
});
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}

View File

@@ -1,27 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
workspaceId: string;
}
/**
* This function fetches the bot for a project
* @param {Object} obj
* @param {String} obj.workspaceId
* @returns
*/
const getBot = async ({ workspaceId }: Props) =>
SecurityClient.fetchCall(`/api/v1/bot/${workspaceId}`, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).bot;
}
console.log("Failed to get bot for project");
return undefined;
});
export default getBot;

View File

@@ -1,42 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface BotKey {
encryptedKey: string;
nonce: string;
}
interface Props {
botId: string;
isActive: boolean;
botKey?: BotKey;
}
/**
* This function sets the active status of a bot and shares a copy of
* the project key (encrypted under the bot's public key) with the
* project's bot
* @param {Object} obj
* @param {String} obj.botId
* @param {String} obj.isActive
* @param {Object} obj.botKey
* @returns
*/
const setBotActiveStatus = async ({ botId, isActive, botKey }: Props) =>
SecurityClient.fetchCall(`/api/v1/bot/${botId}/active`, {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
isActive,
botKey
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to get bot for project");
return undefined;
});
export default setBotActiveStatus;

View File

@@ -1,28 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
type NewEnvironmentInfo = {
environmentSlug: string;
environmentName: string;
};
/**
* This route deletes a specified workspace.
* @param {*} workspaceId
* @returns
*/
const createEnvironment = (workspaceId: string, newEnv: NewEnvironmentInfo) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(newEnv)
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log("Failed to create environment");
return undefined;
});
export default createEnvironment;

View File

@@ -1,22 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This route deletes a specified env.
* @param {*} workspaceId
* @returns
*/
const deleteEnvironment = (workspaceId: string, environmentSlug: string) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ environmentSlug })
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log("Failed to delete environment");
return undefined;
});
export default deleteEnvironment;

View File

@@ -1,29 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
type EnvironmentInfo = {
oldEnvironmentSlug: string;
environmentSlug: string;
environmentName: string;
};
/**
* This route updates a specified environment.
* @param {*} workspaceId
* @returns
*/
const updateEnvironment = (workspaceId: string, env: EnvironmentInfo) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/environments`, {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(env)
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log("Failed to update environment");
return undefined;
});
export default updateEnvironment;

View File

@@ -1,54 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
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";
}
/**
* This function adds secrets to a certain project
* @param {object} obj
* @param {EncryptedSecretProps} obj.secrets - the ids of secrets that we want to add
* @param {string} obj.env - the environment to which we are adding secrets
* @param {string} obj.workspaceId - the project to which we are adding secrets
* @returns
*/
const addSecrets = async ({
secrets,
env,
workspaceId
}: {
secrets: EncryptedSecretProps[];
env: string;
workspaceId: string;
}) =>
SecurityClient.fetchCall("/api/v2/secrets", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
environment: env,
workspaceId,
secrets
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to add certain project secrets");
return undefined;
});
export default addSecrets;

View File

@@ -1,25 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This function deletes certain secrets from a certain project
* @param {string[]} secretIds - the ids of secrets that we want to be deleted
* @returns
*/
const deleteSecrets = async ({ secretIds }: { secretIds: string[] }) =>
SecurityClient.fetchCall("/api/v2/secrets", {
method: "DELETE",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
secretIds
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to delete certain project secrets");
return undefined;
});
export default deleteSecrets;

View File

@@ -1,29 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This function fetches the encrypted secrets for a certain project
* @param {string} workspaceId - project is for which a user is trying to get secrets
* @param {string} env - environment of a project for which a user is trying ot get secrets
* @returns
*/
const getSecrets = async (workspaceId: string, env: string) =>
SecurityClient.fetchCall(
`/api/v2/secrets?${new URLSearchParams({
environment: env,
workspaceId
})}`,
{
method: "GET",
headers: {
"Content-Type": "application/json"
}
}
).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).secrets;
}
console.log("Failed to get project secrets");
return undefined;
});
export default getSecrets;

View File

@@ -1,42 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
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";
}
/**
* This function updates certain secrets in a certain project
* @param {object} obj
* @param {EncryptedSecretProps[]} obj.secrets - the ids of secrets that we want to update
* @returns
*/
const updateSecrets = async ({ secrets }: { secrets: EncryptedSecretProps[] }) =>
SecurityClient.fetchCall("/api/v2/secrets", {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
secrets
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to update certain project secrets");
return undefined;
});
export default updateSecrets;

View File

@@ -1,39 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
workspaceId: string;
secrets: any;
keys: string;
environment: string;
}
/**
* This function uploads the encrypted .env file
* @param {object} obj
* @param {string} obj.workspaceId
* @param {} obj.secrets
* @param {} obj.keys
* @param {string} obj.environment
* @returns
*/
const uploadSecrets = async ({ workspaceId, secrets, keys, environment }: Props) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/secrets`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
secrets,
keys,
environment,
channel: "web"
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log("Failed to push secrets");
return undefined;
});
export default uploadSecrets;

View File

@@ -1,38 +0,0 @@
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;

View File

@@ -1,23 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This route lets us get the current subscription of an org.
* @param {*} req
* @param {*} res
* @returns
*/
const getOrganizationSubscriptions = (req: { orgId: string }) =>
SecurityClient.fetchCall(`/api/v1/organization/${req.orgId}/subscriptions`, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).subscriptions;
}
console.log("Failed to get org subscriptions");
return undefined;
});
export default getOrganizationSubscriptions;

View File

@@ -4,18 +4,20 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
* This route lets us get the all the orgs of a certain user.
* @returns
*/
const getOrganizations = () =>
SecurityClient.fetchCall("/api/v1/organization", {
const getOrganizations = () => {
return SecurityClient.fetchCall("/api/v1/organization", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).organizations;
const {organizations} = await res.json();
return organizations;
}
console.log("Failed to get orgs of a user");
return undefined;
});
}
export default getOrganizations;

View File

@@ -1,56 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
name: string;
workspaceId: string;
environment: string;
expiresIn: number;
encryptedKey: string;
iv: string;
tag: string;
}
/**
* This route adds a service token for a specific user in a project
* @param {object} obj
* @param {string} obj.name - name of the service token
* @param {string} obj.workspaceId - workspace for which we are issuing the token
* @param {string} obj.environment - environment for which we are issuing the token
* @param {string} obj.expiresIn - how soon the service token expires in ms
* @param {string} obj.encryptedKey - encrypted project key through random symmetric encryption
* @param {string} obj.iv - obtained through symmetric encryption
* @param {string} obj.tag - obtained through symmetric encryption
* @returns
*/
const addServiceToken = ({
name,
workspaceId,
environment,
expiresIn,
encryptedKey,
iv,
tag
}: Props) =>
SecurityClient.fetchCall("/api/v2/service-token/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name,
workspaceId,
environment,
expiresIn,
encryptedKey,
iv,
tag
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to add service tokens");
return undefined;
});
export default addServiceToken;

View File

@@ -1,27 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
serviceTokenId: string;
}
/**
* This route revokes a specific service token
* @param {object} obj
* @param {string} obj.serviceTokenId - id of a cervice token that we want to delete
* @returns
*/
const deleteServiceToken = ({ serviceTokenId }: Props) =>
SecurityClient.fetchCall(`/api/v2/service-token/${serviceTokenId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to delete a service token");
return undefined;
});
export default deleteServiceToken;

View File

@@ -1,22 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This route gets service tokens for a specific user in a project
* @param {*} param0
* @returns
*/
const getServiceTokens = ({ workspaceId }: { workspaceId: string }) =>
SecurityClient.fetchCall(`/api/v2/workspace/${workspaceId}/service-token-data`, {
method: "GET",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).serviceTokenData;
}
console.log("Failed to get service tokens");
return undefined;
});
export default getServiceTokens;

View File

@@ -1,33 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This route creates a new workspace for a user within a certain organization.
* @param {string} workspaceName - project Name
* @param {string} organizationId - org ID
* @returns
*/
const createWorkspace = ({
workspaceName,
organizationId
}: {
workspaceName: string;
organizationId: string;
}) =>
SecurityClient.fetchCall("/api/v1/workspace", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
workspaceName,
organizationId
})
}).then(async (res) => {
if (res?.status === 200) {
return (await res.json()).workspace;
}
console.log("Failed to create a project");
return undefined;
});
export default createWorkspace;

View File

@@ -1,26 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This route lets us rename a certain workspace.
* @param {*} req
* @param {*} res
* @returns
*/
const renameWorkspace = (workspaceId: string, newWorkspaceName: string) =>
SecurityClient.fetchCall(`/api/v1/workspace/${workspaceId}/name`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name: newWorkspaceName
})
}).then(async (res) => {
if (res && res.status === 200) {
return res;
}
console.log("Failed to rename a project");
return undefined;
});
export default renameWorkspace;

View File

@@ -1,30 +1,17 @@
import { useEffect, useState } from "react";
import {
decryptAssymmetric,
encryptAssymmetric
} from "@app/components/utilities/cryptography/crypto";
import { Checkbox } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useGetWorkspaceBot, useUpdateBotActiveStatus } from "@app/hooks/api";
import getBot from "../../../../../pages/api/bot/getBot";
import setBotActiveStatus from "../../../../../pages/api/bot/setBotActiveStatus";
import getLatestFileKey from "../../../../../pages/api/workspace/getLatestFileKey";
export const E2EESection = () => {
const { currentWorkspace } = useWorkspace();
const [bot, setBot] = useState<any>(null);
useEffect(() => {
(async () => {
if (currentWorkspace) {
// get project bot
setBot(await getBot({
workspaceId: currentWorkspace._id
}));
}
})();
}, [currentWorkspace]);
const { data: bot } = useGetWorkspaceBot(currentWorkspace?._id ?? "");
const { mutateAsync: updateBotActiveStatus } = useUpdateBotActiveStatus();
/**
* Activate bot for project by performing the following steps:
@@ -69,22 +56,20 @@ export const E2EESection = () => {
encryptedKey: ciphertext,
nonce
};
const botx = await setBotActiveStatus({
botId: bot._id,
await updateBotActiveStatus({
workspaceId: currentWorkspace._id,
botKey,
isActive: true,
botKey
botId: bot._id
});
setBot(botx.bot);
} else {
// bot is active -> deactivate bot
const botx = await setBotActiveStatus({
await updateBotActiveStatus({
isActive: false,
botId: bot._id,
isActive: false
workspaceId: currentWorkspace._id
});
setBot(botx.bot);
}
}
} catch (err) {