mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Begin marking endpoints for deprecation, clean unused frontend code
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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'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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export {
|
||||
useCreateCustomerPortalSession,
|
||||
useDeleteOrgPmtMethod,
|
||||
useDeleteOrgTaxId,
|
||||
useGetOrganization,
|
||||
useGetOrganizations,
|
||||
useGetOrgBillingDetails,
|
||||
useGetOrgInvoices,
|
||||
useGetOrgLicenses,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user