mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 20:29:17 +00:00
Merge branch 'main' into add-bull-queue
This commit is contained in:
@@ -1,99 +0,0 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import addIncidentContact from "@app/pages/api/organization/addIncidentContact";
|
||||
|
||||
import Button from "../buttons/Button";
|
||||
import InputField from "../InputField";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
closeModal: () => void;
|
||||
incidentContacts: string[];
|
||||
setIncidentContacts: (arg: string[]) => void;
|
||||
};
|
||||
|
||||
const AddIncidentContactDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
incidentContacts,
|
||||
setIncidentContacts
|
||||
}: Props) => {
|
||||
const [incidentContactEmail, setIncidentContactEmail] = useState("");
|
||||
const { t } = useTranslation();
|
||||
|
||||
const submit = () => {
|
||||
setIncidentContacts(
|
||||
incidentContacts?.length > 0
|
||||
? incidentContacts.concat([incidentContactEmail])
|
||||
: [incidentContactEmail]
|
||||
);
|
||||
addIncidentContact(localStorage.getItem("orgData.id") as string, incidentContactEmail);
|
||||
closeModal();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-70" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-400">
|
||||
{t("section.incident.add-dialog.title")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("section.incident.add-dialog.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
<InputField
|
||||
label={t("common.email")}
|
||||
onChangeHandler={setIncidentContactEmail}
|
||||
type="varName"
|
||||
value={incidentContactEmail}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 max-w-max">
|
||||
<Button
|
||||
onButtonPressed={submit}
|
||||
color="mineshaft"
|
||||
text={t("section.incident.add-dialog.add-incident") as string}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddIncidentContactDialog;
|
||||
@@ -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;
|
||||
@@ -4,13 +4,13 @@ import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { Select, SelectItem } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { useSubscription, useWorkspace } from "@app/context";
|
||||
import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission";
|
||||
import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace";
|
||||
import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace";
|
||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||
import getProjectInfo from "@app/pages/api/workspace/getProjectInfo";
|
||||
import uploadKeys from "@app/pages/api/workspace/uploadKeys";
|
||||
import {
|
||||
useDeleteUserFromWorkspace,
|
||||
useGetUserWsKey,
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUploadWsKey} from "@app/hooks/api";
|
||||
|
||||
import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto";
|
||||
import guidGenerator from "../../utilities/randomId";
|
||||
@@ -39,10 +39,16 @@ type EnvironmentProps = {
|
||||
* @returns
|
||||
*/
|
||||
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { subscription } = useSubscription();
|
||||
const [roleSelected, setRoleSelected] = useState(
|
||||
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||
);
|
||||
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
|
||||
const { mutateAsync: deleteUserFromWorkspaceMutateAsync } = useDeleteUserFromWorkspace();
|
||||
const { mutateAsync: uploadWsKeyMutateAsync } = useUploadWsKey();
|
||||
const { mutateAsync: updateUserWorkspaceRoleMutateAsync } = useUpdateUserWorkspaceRole();
|
||||
// const [roleSelected, setRoleSelected] = useState(
|
||||
// Array(userData?.length).fill(userData.map((user) => user.role))
|
||||
// );
|
||||
const router = useRouter();
|
||||
const [myRole, setMyRole] = useState("member");
|
||||
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
|
||||
@@ -52,38 +58,15 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
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();
|
||||
deleteUserFromWorkspace(membershipId);
|
||||
changeData(userData.filter((v, i) => i !== index));
|
||||
setRoleSelected([
|
||||
...roleSelected.slice(0, index),
|
||||
...roleSelected.slice(index + 1, userData?.length)
|
||||
]);
|
||||
const handleDelete = async (membershipId: string) => {
|
||||
await deleteUserFromWorkspaceMutateAsync(membershipId);
|
||||
};
|
||||
|
||||
// Update the rold of a certain user
|
||||
const handleRoleUpdate = (index: number, e: string) => {
|
||||
changeUserRoleInWorkspace(userData[index].membershipId, e.toLowerCase());
|
||||
changeData([
|
||||
...userData.slice(0, index),
|
||||
...[
|
||||
{
|
||||
key: userData[index].key,
|
||||
firstName: userData[index].firstName,
|
||||
lastName: userData[index].lastName,
|
||||
email: userData[index].email,
|
||||
role: e.toLocaleLowerCase(),
|
||||
status: userData[index].status,
|
||||
userId: userData[index].userId,
|
||||
membershipId: userData[index].membershipId,
|
||||
publicKey: userData[index].publicKey,
|
||||
deniedPermissions: userData[index].deniedPermissions
|
||||
}
|
||||
],
|
||||
...userData.slice(index + 1, userData?.length)
|
||||
]);
|
||||
const handleRoleUpdate = async (index: number, e: string) => {
|
||||
await updateUserWorkspaceRoleMutateAsync({
|
||||
membershipId: userData[index].membershipId,
|
||||
role: e.toLowerCase()
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully changed user role.",
|
||||
type: "success"
|
||||
@@ -163,32 +146,38 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
useEffect(() => {
|
||||
setMyRole(userData.filter((user) => user.email === myUser)[0]?.role);
|
||||
(async () => {
|
||||
const result = await getProjectInfo({ projectId: workspaceId });
|
||||
setWorkspaceEnvs(result.environments);
|
||||
if (currentWorkspace) {
|
||||
setWorkspaceEnvs(currentWorkspace.environments);
|
||||
}
|
||||
})();
|
||||
}, [userData, myUser]);
|
||||
}, [userData, myUser, currentWorkspace]);
|
||||
|
||||
const grantAccess = async (id: string, publicKey: string) => {
|
||||
const result = await getLatestFileKey({ workspaceId });
|
||||
if (wsKey) {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(workspaceId, id, ciphertext, nonce);
|
||||
router.reload();
|
||||
await uploadWsKeyMutateAsync({
|
||||
workspaceId,
|
||||
userId: id,
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
});
|
||||
router.reload();
|
||||
}
|
||||
};
|
||||
|
||||
const closeUpgradeModal = () => {
|
||||
@@ -372,7 +361,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
myRole !== "member" ? (
|
||||
<div className="mt-0.5 flex items-center opacity-50 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() => handleDelete(row.membershipId, index)}
|
||||
onButtonPressed={() => handleDelete(row.membershipId)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
|
||||
@@ -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,122 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import deleteIntegrationAuth from "../../pages/api/integrations/DeleteIntegrationAuth";
|
||||
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOption: IntegrationOption;
|
||||
setSelectedIntegrationOption: (cloudIntegration: IntegrationOption) => void;
|
||||
integrationOptionPress: (cloudIntegrationOption: IntegrationOption) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||
}
|
||||
|
||||
const CloudIntegration = ({
|
||||
cloudIntegrationOption,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths,
|
||||
handleDeleteIntegrationAuth
|
||||
}: Props) => {
|
||||
return integrationAuths ? (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={`relative ${
|
||||
cloudIntegrationOption.isAvailable
|
||||
? "cursor-pointer duration-200 hover:bg-mineshaft-700"
|
||||
: "opacity-50"
|
||||
} flex h-32 flex-row items-center rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4`}
|
||||
onClick={() => {
|
||||
if (!cloudIntegrationOption.isAvailable) return;
|
||||
setSelectedIntegrationOption(cloudIntegrationOption);
|
||||
integrationOptionPress(cloudIntegrationOption);
|
||||
}}
|
||||
key={cloudIntegrationOption.name}
|
||||
>
|
||||
<Image
|
||||
src={`/images/integrations/${cloudIntegrationOption.image}`}
|
||||
height={70}
|
||||
width={70}
|
||||
alt="integration logo"
|
||||
/>
|
||||
{cloudIntegrationOption.name.split(" ").length > 2 ? (
|
||||
<div className="ml-4 max-w-xs text-3xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
<div>{cloudIntegrationOption.name.split(" ")[0]}</div>
|
||||
<div className="text-base">
|
||||
{cloudIntegrationOption.name.split(" ")[1]} {cloudIntegrationOption.name.split(" ")[2]}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-4 max-w-xs text-xl font-semibold text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{cloudIntegrationOption.name}
|
||||
</div>
|
||||
)}
|
||||
{cloudIntegrationOption.isAvailable &&
|
||||
integrationAuths
|
||||
.map((authorization) => authorization?.integration)
|
||||
.includes(cloudIntegrationOption.slug) && (
|
||||
<div className="group absolute top-0 right-0 z-40 flex flex-row">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
const deletedIntegrationAuth = await deleteIntegrationAuth({
|
||||
integrationAuthId: integrationAuths
|
||||
.filter(
|
||||
(authorization) => authorization.integration === cloudIntegrationOption.slug
|
||||
)
|
||||
.map((authorization) => authorization._id)[0]
|
||||
});
|
||||
|
||||
handleDeleteIntegrationAuth({
|
||||
integrationAuth: deletedIntegrationAuth
|
||||
});
|
||||
}}
|
||||
className="flex w-max cursor-pointer flex-row items-center rounded-bl-md bg-red py-0.5 px-2 text-xs opacity-30 duration-200 group-hover:opacity-100"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="mr-2 text-xs" />
|
||||
Revoke
|
||||
</div>
|
||||
<div className="flex w-max flex-row items-center rounded-tr-md bg-primary py-0.5 px-2 text-xs text-black opacity-70 duration-200 group-hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2 text-xs" />
|
||||
Authorized
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!cloudIntegrationOption.isAvailable && (
|
||||
<div className="group absolute top-0 right-0 z-50 flex flex-row">
|
||||
<div className="flex w-max flex-row items-center rounded-bl-md rounded-tr-md bg-yellow py-0.5 px-2 text-xs text-black opacity-90">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudIntegration;
|
||||
@@ -1,63 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import CloudIntegration from "./CloudIntegration";
|
||||
|
||||
interface IntegrationOption {
|
||||
clientId: string;
|
||||
clientSlug?: string; // vercel-integration specific
|
||||
docsLink: string;
|
||||
image: string;
|
||||
isAvailable: boolean;
|
||||
name: string;
|
||||
slug: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface IntegrationAuth {
|
||||
_id: string;
|
||||
integration: string;
|
||||
workspace: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
cloudIntegrationOptions: IntegrationOption[];
|
||||
setSelectedIntegrationOption: () => void;
|
||||
integrationOptionPress: (integrationOption: IntegrationOption) => void;
|
||||
integrationAuths: IntegrationAuth[];
|
||||
handleDeleteIntegrationAuth: (args: { integrationAuth: IntegrationAuth }) => void;
|
||||
}
|
||||
|
||||
const CloudIntegrationSection = ({
|
||||
cloudIntegrationOptions,
|
||||
setSelectedIntegrationOption,
|
||||
integrationOptionPress,
|
||||
integrationAuths,
|
||||
handleDeleteIntegrationAuth
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="m-4 mt-7 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">{t("integrations.cloud-integrations")}</h1>
|
||||
<p className="text-base text-gray-400">{t("integrations.click-to-start")}</p>
|
||||
</div>
|
||||
<div className="mx-6 grid max-w-5xl grid-cols-4 grid-rows-2 gap-4">
|
||||
{cloudIntegrationOptions.map((cloudIntegrationOption) => (
|
||||
<CloudIntegration
|
||||
cloudIntegrationOption={cloudIntegrationOption}
|
||||
setSelectedIntegrationOption={setSelectedIntegrationOption}
|
||||
integrationOptionPress={integrationOptionPress}
|
||||
integrationAuths={integrationAuths}
|
||||
handleDeleteIntegrationAuth={handleDeleteIntegrationAuth}
|
||||
key={`cloud-integration-${cloudIntegrationOption.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudIntegrationSection;
|
||||
@@ -1,36 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
interface Framework {
|
||||
name: string;
|
||||
slug: string;
|
||||
image: string;
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
const FrameworkIntegration = ({ framework }: { framework: Framework }) => (
|
||||
<a
|
||||
href={framework.docsLink}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="relative flex flex-row justify-center duration-200 h-32 rounded-md p-0.5 items-center cursor-pointer"
|
||||
>
|
||||
<div
|
||||
className={`hover:bg-mineshaft-700 cursor-pointer font-semibold bg-mineshaft-800 border border-mineshaft-600 flex flex-col items-center justify-center h-full w-full rounded-md text-gray-300 group-hover:text-gray-200 duration-200 ${
|
||||
framework?.name?.split(" ").length > 1 ? "text-sm px-1" : "text-xl px-2"
|
||||
} text-center w-full max-w-xs`}
|
||||
>
|
||||
{framework?.image && (
|
||||
<Image
|
||||
src={`/images/integrations/${framework.image}.png`}
|
||||
height={framework?.name ? 60 : 90}
|
||||
width={framework?.name ? 60 : 90}
|
||||
alt="integration logo"
|
||||
/>
|
||||
)}
|
||||
{framework?.name && framework?.image && <div className="h-2" />}
|
||||
{framework?.name && framework.name}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
|
||||
export default FrameworkIntegration;
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import FrameworkIntegration from "./FrameworkIntegration";
|
||||
|
||||
interface Framework {
|
||||
name: string;
|
||||
image: string;
|
||||
link: string;
|
||||
slug: string;
|
||||
docsLink: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
frameworks: [Framework];
|
||||
}
|
||||
|
||||
const FrameworkIntegrationSection = ({ frameworks }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mx-4 mt-12 mb-4 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">{t("integrations.framework-integrations")}</h1>
|
||||
<p className="text-base text-gray-400">{t("integrations.click-to-setup")}</p>
|
||||
</div>
|
||||
<div className="mx-6 mt-4 grid max-w-5xl grid-cols-7 grid-rows-2 gap-4">
|
||||
{frameworks.map((framework) => (
|
||||
<FrameworkIntegration
|
||||
framework={framework}
|
||||
key={`framework-integration-${framework.slug}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FrameworkIntegrationSection;
|
||||
@@ -1,313 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faArrowRight, faCheck, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
// TODO: This needs to be moved from public folder
|
||||
import {
|
||||
contextNetlifyMapping,
|
||||
integrationSlugNameMapping,
|
||||
reverseContextNetlifyMapping
|
||||
} from "public/data/frequentConstants";
|
||||
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
import ListBox from "@app/components/basic/Listbox";
|
||||
|
||||
import deleteIntegration from "../../pages/api/integrations/DeleteIntegration";
|
||||
import getIntegrationApps from "../../pages/api/integrations/GetIntegrationApps";
|
||||
import updateIntegration from "../../pages/api/integrations/updateIntegration";
|
||||
|
||||
interface Integration {
|
||||
_id: string;
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
path: string | null;
|
||||
region: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
interface IntegrationApp {
|
||||
name: string;
|
||||
appId?: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
integration: Integration;
|
||||
integrations: Integration[];
|
||||
setIntegrations: any;
|
||||
bot: any;
|
||||
setBot: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||
};
|
||||
|
||||
// TODO: refactor
|
||||
const IntegrationTile = ({
|
||||
integration,
|
||||
integrations,
|
||||
bot,
|
||||
setBot,
|
||||
setIntegrations,
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
const [integrationEnvironment, setIntegrationEnvironment] = useState<Props["environments"][0]>(
|
||||
environments.find(({ slug }) => slug === integration?.environment) || {
|
||||
name: "",
|
||||
slug: ""
|
||||
}
|
||||
);
|
||||
const router = useRouter();
|
||||
const [apps, setApps] = useState<IntegrationApp[]>([]); // integration app objects
|
||||
const [integrationApp, setIntegrationApp] = useState(""); // integration app name
|
||||
const [integrationTargetEnvironment, setIntegrationTargetEnvironment] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const loadIntegration = async () => {
|
||||
const tempApps: [IntegrationApp] = await getIntegrationApps({
|
||||
integrationAuthId: integration?.integrationAuth
|
||||
});
|
||||
|
||||
setApps(tempApps);
|
||||
|
||||
if (integration?.app) {
|
||||
setIntegrationApp(integration.app);
|
||||
} else if (integration?.path && integration?.region) {
|
||||
setIntegrationApp(`${integration.path} (${integration.region})`);
|
||||
} else if (tempApps.length > 0) {
|
||||
setIntegrationApp(tempApps[0].name);
|
||||
} else {
|
||||
setIntegrationApp("");
|
||||
}
|
||||
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
setIntegrationTargetEnvironment(
|
||||
integration?.targetEnvironment
|
||||
? integration.targetEnvironment.charAt(0).toUpperCase() +
|
||||
integration.targetEnvironment.substring(1)
|
||||
: "Development"
|
||||
);
|
||||
break;
|
||||
case "netlify":
|
||||
setIntegrationTargetEnvironment(
|
||||
integration?.targetEnvironment
|
||||
? contextNetlifyMapping[integration.targetEnvironment]
|
||||
: "Local development"
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
loadIntegration();
|
||||
}, []);
|
||||
|
||||
const handleStartIntegration = async () => {
|
||||
const reformatTargetEnvironment = (targetEnvironment: string) => {
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
return targetEnvironment.toLowerCase();
|
||||
case "netlify":
|
||||
return reverseContextNetlifyMapping[targetEnvironment];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const siteApp = apps.find((app) => app.name === integrationApp); // obj or undefined
|
||||
const appId = siteApp?.appId ?? null;
|
||||
const owner = siteApp?.owner ?? null;
|
||||
|
||||
// return updated integration
|
||||
const updatedIntegration = await updateIntegration({
|
||||
integrationId: integration._id,
|
||||
environment: integrationEnvironment.slug,
|
||||
isActive: true,
|
||||
app: integrationApp,
|
||||
appId,
|
||||
targetEnvironment: reformatTargetEnvironment(integrationTargetEnvironment),
|
||||
owner
|
||||
});
|
||||
|
||||
setIntegrations(
|
||||
integrations.map((i) => (i._id === updatedIntegration._id ? updatedIntegration : i))
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const renderIntegrationSpecificParams = (integration: Integration) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case "vercel":
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 w-60 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={!integration.isActive ? ["Development", "Preview", "Production"] : null}
|
||||
isSelected={integrationTargetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
isFull
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "netlify":
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">CONTEXT</div>
|
||||
<ListBox
|
||||
data={
|
||||
!integration.isActive
|
||||
? ["Production", "Deploy previews", "Branch deploys", "Local development"]
|
||||
: null
|
||||
}
|
||||
isSelected={integrationTargetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "railway":
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={
|
||||
!integration.isActive
|
||||
? ["Production", "Deploy previews", "Branch deploys", "Local development"]
|
||||
: null
|
||||
}
|
||||
isSelected={integration.targetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case "gitlab":
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</div>
|
||||
<ListBox
|
||||
data={null}
|
||||
isSelected={integration.targetEnvironment}
|
||||
onChange={setIntegrationTargetEnvironment}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <div />;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
if (!integrationApp && integration.integration !== "checkly") return <div />;
|
||||
|
||||
const isSelected =
|
||||
integration.integration === "hashicorp-vault"
|
||||
? `${integration.app} - path: ${integration.path}`
|
||||
: integrationApp;
|
||||
|
||||
return (
|
||||
<div className="mx-6 mb-8 flex max-w-5xl justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
|
||||
<div className="flex">
|
||||
<div>
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">ENVIRONMENT</p>
|
||||
<ListBox
|
||||
data={!integration.isActive ? environments.map(({ name }) => name) : null}
|
||||
isSelected={integrationEnvironment.name}
|
||||
onChange={(envName) =>
|
||||
setIntegrationEnvironment(
|
||||
environments.find(({ name }) => envName === name) || {
|
||||
name: "unknown",
|
||||
slug: "unknown"
|
||||
}
|
||||
)
|
||||
}
|
||||
isFull
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">SECRET PATH</p>
|
||||
<div className="cursor-default rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
|
||||
{integration.secretPath}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<FontAwesomeIcon icon={faArrowRight} className="mx-4 mt-8 text-gray-400" />
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<p className="mb-2 text-xs font-semibold text-gray-400">INTEGRATION</p>
|
||||
<div className="cursor-default rounded-md bg-white/[.07] py-2.5 pl-4 pr-10 text-sm font-semibold text-gray-300">
|
||||
{/* {integration.integration.charAt(0).toUpperCase() + integration.integration.slice(1)} */}
|
||||
{integrationSlugNameMapping[integration.integration]}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-2">
|
||||
<div className="mb-2 text-xs font-semibold text-gray-400">APP</div>
|
||||
{integrationApp ? (
|
||||
<div title={integrationApp}>
|
||||
<ListBox
|
||||
data={!integration.isActive ? apps.map((app) => app.name) : null}
|
||||
isSelected={isSelected}
|
||||
onChange={(app) => {
|
||||
setIntegrationApp(app);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-10 w-52 animate-pulse rounded-md bg-mineshaft-600 px-4 py-2 font-bold">
|
||||
-
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{renderIntegrationSpecificParams(integration)}
|
||||
</div>
|
||||
<div className="flex cursor-default items-end">
|
||||
{integration.isActive ? (
|
||||
<div className="flex max-w-5xl flex-row items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 p-[0.44rem] px-4">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2.5 text-lg text-primary" />
|
||||
<div className="font-semibold text-gray-300">In Sync</div>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
text="Start Integration"
|
||||
onButtonPressed={() => handleStartIntegration()}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
/>
|
||||
)}
|
||||
<div className="ml-2 opacity-80 duration-200 hover:opacity-100">
|
||||
<Button
|
||||
onButtonPressed={() =>
|
||||
handleDeleteIntegration({
|
||||
integration
|
||||
})
|
||||
}
|
||||
color="red"
|
||||
size="icon-md"
|
||||
icon={faXmark}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationTile;
|
||||
@@ -1,63 +0,0 @@
|
||||
import IntegrationTile from "./Integration";
|
||||
|
||||
interface Props {
|
||||
integrations: any;
|
||||
setIntegrations: any;
|
||||
bot: any;
|
||||
setBot: any;
|
||||
environments: Array<{ name: string; slug: string }>;
|
||||
handleDeleteIntegration: (args: { integration: Integration }) => void;
|
||||
}
|
||||
|
||||
interface Integration {
|
||||
_id: string;
|
||||
isActive: boolean;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
path: string | null;
|
||||
region: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
environment: string;
|
||||
integration: string;
|
||||
targetEnvironment: string;
|
||||
workspace: string;
|
||||
integrationAuth: string;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
const ProjectIntegrationSection = ({
|
||||
integrations,
|
||||
setIntegrations,
|
||||
bot,
|
||||
setBot,
|
||||
environments = [],
|
||||
handleDeleteIntegration
|
||||
}: Props) => {
|
||||
return integrations.length > 0 ? (
|
||||
<div className="mb-12">
|
||||
<div className="mx-4 mb-4 mt-6 flex max-w-5xl flex-col items-start justify-between px-2 text-xl">
|
||||
<h1 className="text-3xl font-semibold">Current Integrations</h1>
|
||||
<p className="text-base text-bunker-300">Manage integrations with third-party services.</p>
|
||||
</div>
|
||||
{integrations.map((integration: Integration) => {
|
||||
return (
|
||||
<IntegrationTile
|
||||
key={`integration-${integration?._id.toString()}`}
|
||||
integration={integration}
|
||||
integrations={integrations}
|
||||
bot={bot}
|
||||
setBot={setBot}
|
||||
setIntegrations={setIntegrations}
|
||||
environments={environments}
|
||||
handleDeleteIntegration={handleDeleteIntegration}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectIntegrationSection;
|
||||
@@ -3,7 +3,9 @@ import React, { useState } from "react";
|
||||
import ReactCodeInput from "react-code-input";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import sendVerificationEmail from "@app/pages/api/auth/SendVerificationEmail";
|
||||
import {
|
||||
useSendVerificationEmail
|
||||
} from "@app/hooks/api";
|
||||
|
||||
import Error from "../basic/Error";
|
||||
import { Button } from "../v2";
|
||||
@@ -70,6 +72,7 @@ export default function CodeInputStep({
|
||||
codeError,
|
||||
isCodeInputCheckLoading
|
||||
}: CodeInputStepProps): JSX.Element {
|
||||
const { mutateAsync } = useSendVerificationEmail();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
@@ -77,7 +80,7 @@ export default function CodeInputStep({
|
||||
const resendVerificationEmail = async () => {
|
||||
setIsResendingVerificationEmail(true);
|
||||
setIsLoading(true);
|
||||
sendVerificationEmail(email);
|
||||
await mutateAsync({ email });
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
setIsResendingVerificationEmail(false);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
|
||||
import sendVerificationEmail from "@app/pages/api/auth/SendVerificationEmail";
|
||||
import { useSendVerificationEmail } from "@app/hooks/api";
|
||||
|
||||
import { Button, Input } from "../v2";
|
||||
|
||||
@@ -25,13 +25,14 @@ export default function EnterEmailStep({
|
||||
setEmail,
|
||||
incrementStep
|
||||
}: DownloadBackupPDFStepProps): JSX.Element {
|
||||
const { mutateAsync } = useSendVerificationEmail();
|
||||
const [emailError, setEmailError] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
/**
|
||||
* Verifies if the entered email "looks" correct
|
||||
*/
|
||||
const emailCheck = () => {
|
||||
const emailCheck = async () => {
|
||||
let emailCheckBool = false;
|
||||
if (!email) {
|
||||
setEmailError(true);
|
||||
@@ -45,7 +46,7 @@ export default function EnterEmailStep({
|
||||
|
||||
// If everything is correct, go to the next step
|
||||
if (!emailCheckBool) {
|
||||
sendVerificationEmail(email);
|
||||
await mutateAsync({ email });
|
||||
incrementStep();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useAddUserToOrg } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import addUserToOrg from "@app/pages/api/organization/addUserToOrg";
|
||||
|
||||
import { Button, EmailServiceSetupModal } from "../v2";
|
||||
|
||||
@@ -12,10 +12,12 @@ import { Button, EmailServiceSetupModal } from "../v2";
|
||||
* This is the last step of the signup flow. People can optionally invite their teammates here.
|
||||
*/
|
||||
export default function TeamInviteStep(): JSX.Element {
|
||||
const [emails, setEmails] = useState("");
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const [emails, setEmails] = useState("");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
const { mutateAsync } = useAddUserToOrg();
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
|
||||
|
||||
// Redirect user to the getting started page
|
||||
@@ -27,7 +29,12 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
inviteEmails
|
||||
.split(",")
|
||||
.map((email) => email.trim())
|
||||
.map(async (email) => addUserToOrg(email, String(localStorage.getItem("orgData.id"))));
|
||||
.map(async (email) => {
|
||||
mutateAsync({
|
||||
inviteeEmail: email,
|
||||
organizationId: String(localStorage.getItem("orgData.id"))
|
||||
});
|
||||
});
|
||||
|
||||
await redirectToHome();
|
||||
};
|
||||
|
||||
@@ -9,8 +9,8 @@ import nacl from "tweetnacl";
|
||||
import { encodeBase64 } from "tweetnacl-util";
|
||||
|
||||
import { useGetCommonPasswords } from "@app/hooks/api";
|
||||
import completeAccountInformationSignup from "@app/pages/api/auth/CompleteAccountInformationSignup";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import { completeAccountSignup } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import ProjectService from "@app/services/ProjectService";
|
||||
|
||||
import InputField from "../basic/InputField";
|
||||
@@ -159,7 +159,7 @@ export default function UserInfoStep({
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
const response = await completeAccountInformationSignup({
|
||||
const response = await completeAccountSignup({
|
||||
email,
|
||||
firstName: name.split(" ")[0],
|
||||
lastName: name.split(" ").slice(1).join(" "),
|
||||
@@ -190,7 +190,8 @@ export default function UserInfoStep({
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
const orgId = userOrgs[0]?._id;
|
||||
const project = await ProjectService.initProject({
|
||||
organizationId: orgId,
|
||||
|
||||
@@ -3,8 +3,9 @@ import crypto from "crypto";
|
||||
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import changePassword2 from "@app/pages/api/auth/ChangePassword2";
|
||||
import SRP1 from "@app/pages/api/auth/SRP1";
|
||||
import {
|
||||
changePassword,
|
||||
srp1} from "@app/hooks/api/auth/queries";
|
||||
|
||||
import Aes256Gcm from "./cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "./cryptography/crypto";
|
||||
@@ -27,7 +28,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
|
||||
try {
|
||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||
|
||||
const res = await SRP1({ clientPublicKey });
|
||||
const res = await srp1({ clientPublicKey });
|
||||
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
@@ -71,7 +72,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
await changePassword2({
|
||||
await changePassword({
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import login1 from "@app/pages/api/auth/Login1";
|
||||
import login2 from "@app/pages/api/auth/Login2";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
import { login1, login2 } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import Telemetry from "./telemetry/Telemetry";
|
||||
@@ -125,13 +124,11 @@ const attemptLogin = async (
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgs = await fetchOrganizations();
|
||||
const orgId = userOrgs[0]._id;
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId
|
||||
});
|
||||
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
|
||||
|
||||
if (orgUserProjects.length > 0) {
|
||||
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import login1 from "@app/pages/api/auth/Login1";
|
||||
import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
|
||||
// import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
@@ -65,7 +65,7 @@ const attemptLoginMfa = async ({
|
||||
tag
|
||||
} = await verifyMfaToken({
|
||||
email,
|
||||
mfaToken
|
||||
mfaCode: mfaToken
|
||||
});
|
||||
|
||||
// unset temporary (MFA) JWT token and set JWT token
|
||||
@@ -96,13 +96,11 @@ const attemptLoginMfa = async ({
|
||||
// TODO: in the future - move this logic elsewhere
|
||||
// because this function is about logging the user in
|
||||
// and not initializing the login details
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgs = await fetchOrganizations();
|
||||
const orgId = userOrgs[0]._id;
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId
|
||||
});
|
||||
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
|
||||
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
|
||||
|
||||
resolve({
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import login1 from "@app/pages/api/auth/Login1";
|
||||
import login2 from "@app/pages/api/auth/Login2";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
import { login1, login2 } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import Telemetry from "./telemetry/Telemetry";
|
||||
@@ -36,7 +35,6 @@ const attemptLogin = async (
|
||||
providerAuthToken?: string;
|
||||
}
|
||||
): Promise<IsLoginSuccessful> => {
|
||||
|
||||
const telemetry = new Telemetry().getInstance();
|
||||
return new Promise((resolve, reject) => {
|
||||
client.init(
|
||||
@@ -47,12 +45,13 @@ const attemptLogin = async (
|
||||
async () => {
|
||||
try {
|
||||
const clientPublicKey = client.getPublicKey();
|
||||
|
||||
const { serverPublicKey, salt } = await login1({
|
||||
email,
|
||||
clientPublicKey,
|
||||
providerAuthToken,
|
||||
});
|
||||
|
||||
|
||||
client.setSalt(salt);
|
||||
client.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = client.getProof(); // called M1
|
||||
@@ -124,14 +123,12 @@ const attemptLogin = async (
|
||||
// TODO: in the future - move this logic elsewhere
|
||||
// because this function is about logging the user in
|
||||
// and not initializing the login details
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
const orgId = userOrgs[0]._id;
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId
|
||||
});
|
||||
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
|
||||
|
||||
if (orgUserProjects.length > 0) {
|
||||
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import login1 from "@app/pages/api/auth/Login1";
|
||||
import verifyMfaToken from "@app/pages/api/auth/verifyMfaToken";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
import { login1 , verifyMfaToken } from "@app/hooks/api/auth/queries";
|
||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
import { fetchMyOrganizationProjects } from "@app/hooks/api/users/queries";
|
||||
import KeyService from "@app/services/KeyService";
|
||||
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
@@ -56,7 +55,7 @@ const attemptLoginMfa = async ({
|
||||
tag
|
||||
} = await verifyMfaToken({
|
||||
email,
|
||||
mfaToken
|
||||
mfaCode: mfaToken
|
||||
});
|
||||
|
||||
// unset temporary (MFA) JWT token and set JWT token
|
||||
@@ -87,13 +86,11 @@ const attemptLoginMfa = async ({
|
||||
// TODO: in the future - move this logic elsewhere
|
||||
// because this function is about logging the user in
|
||||
// and not initializing the login details
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrgs = await fetchOrganizations();
|
||||
const orgId = userOrgs[0]._id;
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId
|
||||
});
|
||||
const orgUserProjects = await fetchMyOrganizationProjects(orgId);
|
||||
localStorage.setItem("projectData.id", orgUserProjects[0]._id);
|
||||
|
||||
resolve(true);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import getOrganizationUsers from "@app/pages/api/organization/GetOrgUsers";
|
||||
import checkUserAction from "@app/pages/api/userActions/checkUserAction";
|
||||
import { fetchOrgUsers,fetchUserAction } from "@app/hooks/api/users/queries";
|
||||
|
||||
interface OnboardingCheckProps {
|
||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||
@@ -20,34 +19,31 @@ const onboardingCheck = async ({
|
||||
setUsersInOrg
|
||||
}: OnboardingCheckProps) => {
|
||||
let countActions = 0;
|
||||
const userActionSlack = await checkUserAction({
|
||||
action: "slack_cta_clicked"
|
||||
});
|
||||
const userActionSlack = await fetchUserAction(
|
||||
"slack_cta_clicked"
|
||||
);
|
||||
|
||||
if (userActionSlack) {
|
||||
countActions += 1;
|
||||
}
|
||||
if (setHasUserClickedSlack) setHasUserClickedSlack(!!userActionSlack);
|
||||
|
||||
const userActionSecrets = await checkUserAction({
|
||||
action: "first_time_secrets_pushed"
|
||||
});
|
||||
const userActionSecrets = await fetchUserAction("first_time_secrets_pushed");
|
||||
|
||||
if (userActionSecrets) {
|
||||
countActions += 1;
|
||||
}
|
||||
if (setHasUserPushedSecrets) setHasUserPushedSecrets(!!userActionSecrets);
|
||||
|
||||
const userActionIntro = await checkUserAction({
|
||||
action: "intro_cta_clicked"
|
||||
});
|
||||
const userActionIntro = await fetchUserAction("intro_cta_clicked");
|
||||
if (userActionIntro) {
|
||||
countActions += 1;
|
||||
}
|
||||
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||
|
||||
const orgId = localStorage.getItem("orgData.id");
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: orgId || ""
|
||||
});
|
||||
const orgUsers = await fetchOrgUsers(orgId || "");
|
||||
|
||||
if (orgUsers.length > 1) {
|
||||
countActions += 1;
|
||||
}
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
/* eslint-disable new-cap */
|
||||
import crypto from "crypto";
|
||||
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import changePassword2 from "@app/pages/api/auth/ChangePassword2";
|
||||
import SRP1 from "@app/pages/api/auth/SRP1";
|
||||
|
||||
import { saveTokenToLocalStorage } from "../saveTokenToLocalStorage";
|
||||
import Aes256Gcm from "./aes-256-gcm";
|
||||
import { deriveArgonKey } from "./crypto";
|
||||
|
||||
const clientOldPassword = new jsrp.client();
|
||||
const clientNewPassword = new jsrp.client();
|
||||
|
||||
/**
|
||||
* This function loggs in the user (whether it's right after signup, or a normal login)
|
||||
* @param {*} email
|
||||
* @param {*} password
|
||||
* @param {*} setErrorLogin
|
||||
* @param {*} router
|
||||
* @param {*} isSignUp
|
||||
* @returns
|
||||
*/
|
||||
const changePassword = async (
|
||||
email: string,
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
setCurrentPasswordError: (arg: boolean) => void,
|
||||
setPasswordChanged: (arg: boolean) => void,
|
||||
setCurrentPassword: (arg: string) => void,
|
||||
setNewPassword: (arg: string) => void
|
||||
) => {
|
||||
try {
|
||||
setPasswordChanged(false);
|
||||
setCurrentPasswordError(false);
|
||||
|
||||
clientOldPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: currentPassword
|
||||
},
|
||||
async () => {
|
||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||
|
||||
let serverPublicKey;
|
||||
let salt;
|
||||
try {
|
||||
const res = await SRP1({
|
||||
clientPublicKey
|
||||
});
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
} catch (err) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log("Wrong current password", err, 1);
|
||||
}
|
||||
|
||||
clientOldPassword.setSalt(salt);
|
||||
clientOldPassword.setServerPublicKey(serverPublicKey);
|
||||
const clientProof = clientOldPassword.getProof(); // called M1
|
||||
|
||||
clientNewPassword.init(
|
||||
{
|
||||
username: email,
|
||||
password: newPassword
|
||||
},
|
||||
async () => {
|
||||
clientNewPassword.createVerifier(async (err, result) => {
|
||||
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password: newPassword,
|
||||
salt: result.salt,
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
if (!derivedKey) throw new Error("Failed to derive key from password");
|
||||
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: localStorage.getItem("PRIVATE_KEY") as string,
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString("hex"),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
try {
|
||||
await changePassword2({
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
});
|
||||
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
|
||||
window.location.href = "/login";
|
||||
|
||||
// move to login page
|
||||
} catch (error) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("Something went wrong during changing the password");
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export default changePassword;
|
||||
@@ -3,8 +3,9 @@ import crypto from "crypto";
|
||||
|
||||
import jsrp from "jsrp";
|
||||
|
||||
import issueBackupPrivateKey from "@app/pages/api/auth/IssueBackupPrivateKey";
|
||||
import SRP1 from "@app/pages/api/auth/SRP1";
|
||||
import { issueBackupPrivateKey ,
|
||||
srp1
|
||||
} from "@app/hooks/api/auth/queries";
|
||||
|
||||
import generateBackupPDF from "../generateBackupPDF";
|
||||
import Aes256Gcm from "./aes-256-gcm";
|
||||
@@ -51,7 +52,7 @@ const issueBackupKey = async ({
|
||||
let serverPublicKey;
|
||||
let salt;
|
||||
try {
|
||||
const res = await SRP1({
|
||||
const res = await srp1({
|
||||
clientPublicKey
|
||||
});
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
@@ -61,8 +62,8 @@ const issueBackupKey = async ({
|
||||
console.log("Wrong current password", err, 1);
|
||||
}
|
||||
|
||||
clientPassword.setSalt(salt);
|
||||
clientPassword.setServerPublicKey(serverPublicKey);
|
||||
clientPassword.setSalt(salt as string);
|
||||
clientPassword.setServerPublicKey(serverPublicKey as string);
|
||||
const clientProof = clientPassword.getProof(); // called M1
|
||||
|
||||
const generatedKey = crypto.randomBytes(16).toString("hex");
|
||||
@@ -80,24 +81,25 @@ const issueBackupKey = async ({
|
||||
secret: generatedKey
|
||||
});
|
||||
|
||||
const res = await issueBackupPrivateKey({
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
clientProof
|
||||
});
|
||||
try {
|
||||
await issueBackupPrivateKey({
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
clientProof
|
||||
});
|
||||
|
||||
if (res?.status === 400) {
|
||||
setBackupKeyError(true);
|
||||
} else if (res?.status === 200) {
|
||||
generateBackupPDF({
|
||||
personalName,
|
||||
personalEmail: email,
|
||||
generatedKey
|
||||
});
|
||||
setBackupKeyIssued(true);
|
||||
|
||||
} catch {
|
||||
setBackupKeyError(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from "crypto";
|
||||
|
||||
import { SecretDataProps, Tag } from "public/data/frequentInterfaces";
|
||||
|
||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||
import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
|
||||
import { decryptAssymmetric, encryptSymmetric } from "../cryptography/crypto";
|
||||
|
||||
@@ -42,19 +42,21 @@ const encryptSecrets = async ({
|
||||
}) => {
|
||||
let secrets;
|
||||
try {
|
||||
const sharedKey = await getLatestFileKey({ workspaceId });
|
||||
// const sharedKey = await getLatestFileKey({ workspaceId });
|
||||
const wsKey = await fetchUserWsKey(workspaceId);
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
|
||||
let randomBytes: string;
|
||||
if (Object.keys(sharedKey).length > 0) {
|
||||
if (wsKey) {
|
||||
// case: a (shared) key exists for the workspace
|
||||
randomBytes = decryptAssymmetric({
|
||||
ciphertext: sharedKey.latestKey.encryptedKey,
|
||||
nonce: sharedKey.latestKey.nonce,
|
||||
publicKey: sharedKey.latestKey.sender.publicKey,
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
} else {
|
||||
// case: a (shared) key does not exist for the workspace
|
||||
randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
@@ -114,6 +116,7 @@ const encryptSecrets = async ({
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error while encrypting secrets");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import { DatePicker } from "./DatePicker";
|
||||
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
title: "Components/DatePicker",
|
||||
component: DatePicker,
|
||||
tags: ["v2"],
|
||||
argTypes: {}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DatePicker>;
|
||||
|
||||
// More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args
|
||||
export const Primary: Story = {
|
||||
args: {}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DayPicker, DayPickerProps } from "react-day-picker";
|
||||
import { faCalendar } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2";
|
||||
|
||||
export type DatePickerProps = Omit<DayPickerProps, "selected"> & {
|
||||
value?: Date;
|
||||
onChange: (date?: Date) => void;
|
||||
popUpProps: PopoverProps;
|
||||
popUpContentProps: PopoverContentProps;
|
||||
};
|
||||
|
||||
// Doc: https://react-day-picker.js.org/
|
||||
export const DatePicker = ({
|
||||
value,
|
||||
onChange,
|
||||
popUpProps,
|
||||
popUpContentProps,
|
||||
...props
|
||||
}: DatePickerProps) => {
|
||||
return (
|
||||
<Popover {...popUpProps}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline_bg" leftIcon={<FontAwesomeIcon icon={faCalendar} />}>
|
||||
{value ? format(value, "PPP") : "Pick a date"}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-fit p-2" {...popUpContentProps}>
|
||||
<DayPicker {...props} mode="single" selected={value} onSelect={onChange} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { DatePickerProps } from "./DatePicker";
|
||||
export { DatePicker } from "./DatePicker";
|
||||
@@ -49,6 +49,7 @@ export type DropdownMenuItemProps<T extends ElementType> =
|
||||
icon?: ReactNode;
|
||||
as?: T;
|
||||
inputRef?: Ref<T>;
|
||||
iconPos?: "left" | "right";
|
||||
};
|
||||
|
||||
export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
@@ -57,6 +58,7 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
className,
|
||||
icon,
|
||||
as: Item = "button",
|
||||
iconPos = "left",
|
||||
...props
|
||||
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T>) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
@@ -67,8 +69,9 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
)}
|
||||
>
|
||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||
{icon && <span className="flex items-center mr-2">{icon}</span>}
|
||||
{icon && iconPos === "left" && <span className="flex items-center mr-2">{icon}</span>}
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
{icon && iconPos === "right" && <span className="flex items-center ml-2">{icon}</span>}
|
||||
</Item>
|
||||
</DropdownMenuPrimitive.Item>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
"fixed top-1/2 left-1/2 z-[90] max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
"fixed top-1/2 left-1/2 z-[90] dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// eslint-disable-next-line
|
||||
import { useArgs } from "@storybook/client-api";
|
||||
import type { Meta } from "@storybook/react";
|
||||
|
||||
import { Pagination, PaginationProps } from "./Pagination";
|
||||
|
||||
const meta: Meta<typeof Pagination> = {
|
||||
title: "Components/Pagination",
|
||||
component: Pagination,
|
||||
tags: ["v2"],
|
||||
args: {
|
||||
count: 50
|
||||
}
|
||||
};
|
||||
|
||||
export default meta;
|
||||
// type Story = StoryObj<typeof Pagination>;
|
||||
|
||||
export const Primary = (args: PaginationProps) => {
|
||||
const [, updateArgs] = useArgs();
|
||||
|
||||
return (
|
||||
<Pagination
|
||||
{...args}
|
||||
onChangePage={(page) => updateArgs({ page })}
|
||||
onChangePerPage={(perPage) => updateArgs({ perPage, page: 1 })}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
faCaretDown,
|
||||
faCheck,
|
||||
faChevronLeft,
|
||||
faChevronRight
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "../Dropdown";
|
||||
import { IconButton } from "../IconButton";
|
||||
|
||||
export type PaginationProps = {
|
||||
count: number;
|
||||
page: number;
|
||||
perPage?: number;
|
||||
onChangePage: (pageNumber: number) => void;
|
||||
onChangePerPage: (newRows: number) => void;
|
||||
className?: string;
|
||||
perPageList?: number[];
|
||||
};
|
||||
|
||||
export const Pagination = ({
|
||||
count,
|
||||
page = 1,
|
||||
perPage = 20,
|
||||
onChangePage,
|
||||
onChangePerPage,
|
||||
perPageList = [10, 20, 50, 100],
|
||||
className
|
||||
}: PaginationProps) => {
|
||||
const prevPageNumber = Math.max(1, page - 1);
|
||||
const canGoPrev = page > 1;
|
||||
|
||||
const upperLimit = Math.ceil(count / perPage);
|
||||
const nextPageNumber = Math.min(upperLimit, page + 1);
|
||||
const canGoNext = page + 1 <= upperLimit;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center justify-end text-white w-full py-3 px-4 bg-mineshaft-800",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="mr-6 flex items-center space-x-2">
|
||||
<div className="text-xs">
|
||||
{(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton ariaLabel="per-page-list" variant="plain">
|
||||
<FontAwesomeIcon className="text-xs" icon={faCaretDown} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="min-w-fit">
|
||||
{perPageList.map((perPageOption) => (
|
||||
<DropdownMenuItem
|
||||
key={`pagination-per-page-options-${perPageOption}`}
|
||||
icon={perPage === perPageOption && <FontAwesomeIcon size="sm" icon={faCheck} />}
|
||||
iconPos="right"
|
||||
onClick={() => onChangePerPage(perPageOption)}
|
||||
>
|
||||
{perPageOption} rows per page
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="pagination-prev"
|
||||
onClick={() => onChangePage(prevPageNumber)}
|
||||
isDisabled={!canGoPrev}
|
||||
>
|
||||
<FontAwesomeIcon className="text-xs" icon={faChevronLeft} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="plain"
|
||||
ariaLabel="pagination-next"
|
||||
onClick={() => onChangePage(nextPageNumber)}
|
||||
isDisabled={!canGoNext}
|
||||
>
|
||||
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export type { PaginationProps } from "./Pagination";
|
||||
export { Pagination } from "./Pagination";
|
||||
@@ -16,6 +16,7 @@ type Props = {
|
||||
position?: "item-aligned" | "popper";
|
||||
isDisabled?: boolean;
|
||||
icon?: IconProp;
|
||||
isMulti?: boolean;
|
||||
};
|
||||
|
||||
export type SelectProps = Omit<SelectPrimitive.SelectProps, "disabled"> & Props;
|
||||
@@ -40,7 +41,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
`inline-flex items-center justify-between rounded-md
|
||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-gray-500`,
|
||||
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200`,
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./Button";
|
||||
export * from "./Card";
|
||||
export * from "./Checkbox";
|
||||
export * from "./DatePicker";
|
||||
export * from "./DeleteActionModal";
|
||||
export * from "./Drawer";
|
||||
export * from "./Dropdown";
|
||||
@@ -12,6 +13,7 @@ export * from "./IconButton";
|
||||
export * from "./Input";
|
||||
export * from "./Menu";
|
||||
export * from "./Modal";
|
||||
export * from "./Pagination";
|
||||
export * from "./Popoverv2";
|
||||
export * from "./SecretInput";
|
||||
export * from "./Select";
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ const updateUserProjectPermission = async ({
|
||||
permissions: denials
|
||||
})
|
||||
}).then(async (res) => {
|
||||
console.log({
|
||||
permissions: denials
|
||||
}, res)
|
||||
// console.log({
|
||||
// permissions: denials
|
||||
// }, res)
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: deprecate in favor of new audit logs
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
@@ -7,7 +8,9 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import getActionData from "@app/ee/api/secrets/GetActionData";
|
||||
import patienceDiff from "@app/ee/utilities/findTextDifferences";
|
||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||
import {
|
||||
useGetUserWsKey
|
||||
} from "@app/hooks/api";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
@@ -58,25 +61,24 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
const [actionData, setActionData] = useState<DecryptedSecretProps[]>();
|
||||
const [actionMetaData, setActionMetaData] = useState<ActionProps>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { data: wsKey } = useGetUserWsKey(String(router.query.id));
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const tempActionData = await getActionData({ actionId: currentAction });
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
// #TODO: make this a separate function and reuse across the app
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
if (wsKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = tempActionData.payload.secretVersions.map(
|
||||
(encryptedSecretVersion: {
|
||||
@@ -121,9 +123,10 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
setActionData(decryptedSecretVersions);
|
||||
setActionMetaData({ name: tempActionData.name });
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
getLogData();
|
||||
}, [currentAction]);
|
||||
}, [currentAction, wsKey]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: deprecate in favor of new audit logs
|
||||
|
||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Tag } from "public/data/frequentInterfaces";
|
||||
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import getProjectSecretShanpshots from "@app/ee/api/secrets/GetProjectSercetShanpshots";
|
||||
import getSecretSnapshotData from "@app/ee/api/secrets/GetSecretSnapshotData";
|
||||
import timeSince from "@app/ee/utilities/timeSince";
|
||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
export interface SecretDataProps {
|
||||
pos: number;
|
||||
key: string;
|
||||
value: string;
|
||||
type: string;
|
||||
id: string;
|
||||
environment: string;
|
||||
}
|
||||
|
||||
interface SideBarProps {
|
||||
toggleSidebar: (value: boolean) => void;
|
||||
setSnapshotData: (value: any) => void;
|
||||
chosenSnapshot: string;
|
||||
}
|
||||
|
||||
interface SnaphotProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretVersions: string[];
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
environment: string;
|
||||
type: "personal" | "shared";
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} obj
|
||||
* @param {function} obj.toggleSidebar - function that opens or closes the sidebar
|
||||
* @param {function} obj.setSnapshotData - state manager for snapshot data
|
||||
* @param {string} obj.chosenSnaphshot - the snapshot id which is currently selected
|
||||
* @returns the sidebar with the options for point-in-time recovery (commits)
|
||||
*/
|
||||
const PITRecoverySidebar = ({ toggleSidebar, setSnapshotData, chosenSnapshot }: SideBarProps) => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [secretSnapshotsMetadata, setSecretSnapshotsMetadata] = useState<SnaphotProps[]>([]);
|
||||
const [currentOffset, setCurrentOffset] = useState(0);
|
||||
const currentLimit = 15;
|
||||
|
||||
const loadMoreSnapshots = () => {
|
||||
setCurrentOffset(currentOffset + currentLimit);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getLogData = async () => {
|
||||
setIsLoading(true);
|
||||
const results = await getProjectSecretShanpshots({
|
||||
workspaceId: String(router.query.id),
|
||||
limit: currentLimit,
|
||||
offset: currentOffset
|
||||
});
|
||||
setSecretSnapshotsMetadata(secretSnapshotsMetadata.concat(results));
|
||||
setIsLoading(false);
|
||||
};
|
||||
getLogData();
|
||||
}, [currentOffset]);
|
||||
|
||||
const exploreSnapshot = async ({ snapshotId }: { snapshotId: string }) => {
|
||||
const secretSnapshotData = await getSecretSnapshotData({ secretSnapshotId: snapshotId });
|
||||
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = secretSnapshotData.secretVersions
|
||||
.filter(
|
||||
(sv: EncrypetedSecretVersionListProps) =>
|
||||
sv.type !== undefined && sv.environment !== undefined
|
||||
)
|
||||
.map((encryptedSecretVersion: EncrypetedSecretVersionListProps, pos: number) => ({
|
||||
id: encryptedSecretVersion._id,
|
||||
pos,
|
||||
type: encryptedSecretVersion.type,
|
||||
environment: encryptedSecretVersion.environment,
|
||||
tags: encryptedSecretVersion.tags,
|
||||
key: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretKeyCiphertext,
|
||||
iv: encryptedSecretVersion.secretKeyIV,
|
||||
tag: encryptedSecretVersion.secretKeyTag,
|
||||
key: decryptedLatestKey
|
||||
}),
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
}));
|
||||
|
||||
const secretKeys = [
|
||||
...new Set(
|
||||
decryptedSecretVersions
|
||||
.filter((dsv: any) => dsv.type !== undefined || dsv.environemnt !== undefined)
|
||||
.map((secret: SecretDataProps) => secret.key)
|
||||
)
|
||||
];
|
||||
|
||||
const result = secretKeys.map((key, index) =>
|
||||
decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "shared"
|
||||
)[0]?.id
|
||||
? {
|
||||
id: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "shared"
|
||||
)[0].id,
|
||||
pos: index,
|
||||
key,
|
||||
environment: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "shared"
|
||||
)[0].environment,
|
||||
tags: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "shared"
|
||||
)[0].tags,
|
||||
value: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "shared"
|
||||
)[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "personal"
|
||||
)[0]?.value
|
||||
}
|
||||
: {
|
||||
id: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "personal"
|
||||
)[0].id,
|
||||
pos: index,
|
||||
key,
|
||||
environment: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "personal"
|
||||
)[0].environment,
|
||||
tags: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "personal"
|
||||
)[0].tags,
|
||||
value: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "shared"
|
||||
)[0]?.value,
|
||||
valueOverride: decryptedSecretVersions.filter(
|
||||
(secret: SecretDataProps) => secret.key === key && secret.type === "personal"
|
||||
)[0]?.value
|
||||
}
|
||||
);
|
||||
|
||||
setSnapshotData({
|
||||
id: secretSnapshotData._id,
|
||||
version: secretSnapshotData.version,
|
||||
createdAt: secretSnapshotData.createdAt,
|
||||
secretVersions: result,
|
||||
comment: ""
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`min-w-sm absolute w-full max-w-sm border-l border-mineshaft-500 ${
|
||||
isLoading ? "bg-bunker-800" : "bg-bunker"
|
||||
} fixed sticky right-0 top-0 z-[40] flex h-full flex-col justify-between shadow-xl`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="mb-8 flex h-full items-center justify-center">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-min">
|
||||
<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">Point In Recovery</p>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="p-1"
|
||||
onClick={() => toggleSidebar(false)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="h-4 w-4 cursor-pointer text-bunker-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-[calc(100vh-115px)] w-96 flex-col overflow-y-auto border-l border-mineshaft-600 bg-bunker px-2 py-2">
|
||||
<span className="px-2 pb-2 text-sm text-bunker-200">
|
||||
Note: This will recover secrets for all enviroments in this project.
|
||||
</span>
|
||||
{secretSnapshotsMetadata?.map((snapshot: SnaphotProps, id: number) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={snapshot._id}
|
||||
onClick={() => exploreSnapshot({ snapshotId: snapshot._id })}
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === "")
|
||||
? "pointer-events-none bg-primary text-black"
|
||||
: "cursor-pointer bg-mineshaft-700 duration-200 hover:bg-mineshaft-500"
|
||||
} mb-2 flex flex-row items-center justify-between rounded-md py-3 px-4`}
|
||||
>
|
||||
<div className="flex flex-row items-start">
|
||||
<div
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === "")
|
||||
? "text-bunker-800"
|
||||
: "text-bunker-200"
|
||||
} mr-1.5 text-sm`}
|
||||
>
|
||||
{timeSince(new Date(snapshot.createdAt))}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === "")
|
||||
? "text-bunker-900"
|
||||
: "text-bunker-300"
|
||||
} text-sm `}
|
||||
>{` - ${snapshot.secretVersions.length} Secrets`}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
chosenSnapshot === snapshot._id || (id === 0 && chosenSnapshot === "")
|
||||
? "pointer-events-none text-bunker-800"
|
||||
: "cursor-pointer text-bunker-200 duration-200 hover:text-primary"
|
||||
} text-sm`}
|
||||
>
|
||||
{id === 0
|
||||
? "Current Version"
|
||||
: chosenSnapshot === snapshot._id
|
||||
? "Currently Viewing"
|
||||
: "Explore"}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mb-14 flex w-full justify-center">
|
||||
<div className="w-40 items-center">
|
||||
<Button
|
||||
text="View More"
|
||||
textDisabled="End of History"
|
||||
active={secretSnapshotsMetadata.length % 15 === 0}
|
||||
onButtonPressed={loadMoreSnapshots}
|
||||
size="md"
|
||||
color="mineshaft"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PITRecoverySidebar;
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCircle, faDotCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import getSecretVersions from "@app/ee/api/secrets/GetSecretVersions";
|
||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||
|
||||
interface DecryptedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface EncrypetedSecretVersionListProps {
|
||||
createdAt: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} secretId - the id of a secret for which are querying version history
|
||||
* @returns a list of versions for a specific secret
|
||||
*/
|
||||
const SecretVersionList = ({ secretId }: { secretId: string }) => {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [secretVersions, setSecretVersions] = useState<DecryptedSecretVersionListProps[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const getSecretVersionHistory = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const encryptedSecretVersions = await getSecretVersions({ secretId, offset: 0, limit: 10 });
|
||||
const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
let decryptedLatestKey: string;
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
decryptedLatestKey = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: String(PRIVATE_KEY)
|
||||
});
|
||||
}
|
||||
|
||||
const decryptedSecretVersions = encryptedSecretVersions?.secretVersions.map(
|
||||
(encryptedSecretVersion: EncrypetedSecretVersionListProps) => ({
|
||||
createdAt: encryptedSecretVersion.createdAt,
|
||||
value: decryptSymmetric({
|
||||
ciphertext: encryptedSecretVersion.secretValueCiphertext,
|
||||
iv: encryptedSecretVersion.secretValueIV,
|
||||
tag: encryptedSecretVersion.secretValueTag,
|
||||
key: decryptedLatestKey
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
setSecretVersions(decryptedSecretVersions);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
getSecretVersionHistory();
|
||||
}, [secretId]);
|
||||
|
||||
return (
|
||||
<div className="min-w-40 overflow-x-none dark mt-4 h-[12.4rem] w-full px-4 text-sm text-bunker-300">
|
||||
<p className="">{t("dashboard.sidebar.version-history")}</p>
|
||||
<div className="overflow-x-none h-full rounded-md border border-mineshaft-500 bg-bunker-800 py-0.5 pl-1">
|
||||
{isLoading ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Image
|
||||
src="/images/loading/loading.gif"
|
||||
height={60}
|
||||
width={100}
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-none h-48 overflow-y-auto dark:[color-scheme:dark]">
|
||||
{secretVersions ? (
|
||||
secretVersions
|
||||
?.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
||||
.map((version: DecryptedSecretVersionListProps, index: number) => (
|
||||
<div key={`${version.createdAt}.${index + 1}`} className="flex flex-row">
|
||||
<div className="flex flex-col items-center pr-1">
|
||||
<div className="p-1">
|
||||
<FontAwesomeIcon icon={index === 0 ? faDotCircle : faCircle} />
|
||||
</div>
|
||||
<div className="mt-1 h-full w-0 border-l border-bunker-300" />
|
||||
</div>
|
||||
<div className="flex w-full max-w-[calc(100%-2.3rem)] flex-col">
|
||||
<div className="pr-2 text-bunker-300/90">
|
||||
{new Date(version.createdAt).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit"
|
||||
})}
|
||||
</div>
|
||||
<div className="">
|
||||
<p className="ph-no-capture break-words">
|
||||
<span className="mr-1.5 rounded-sm bg-primary-500/30 py-0.5 px-1">
|
||||
Value:
|
||||
</span>
|
||||
<span className="font-mono">{version.value}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-bunker-400">
|
||||
No version history yet.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecretVersionList;
|
||||
@@ -2,10 +2,10 @@ import crypto from "crypto";
|
||||
|
||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets";
|
||||
import addSecrets from "@app/pages/api/files/AddSecrets";
|
||||
import getUser from "@app/pages/api/user/getUser";
|
||||
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
|
||||
import uploadKeys from "@app/pages/api/workspace/uploadKeys";
|
||||
import { uploadWsKey } from "@app/hooks/api/keys/queries";
|
||||
import { createSecret } from "@app/hooks/api/secrets/queries";
|
||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
||||
|
||||
const secretsToBeAdded = [
|
||||
{
|
||||
@@ -91,49 +91,63 @@ const initProjectHelper = async ({
|
||||
organizationId: string;
|
||||
projectName: string;
|
||||
}) => {
|
||||
let project;
|
||||
try {
|
||||
// create new project
|
||||
const { data: { workspace } } = await createWorkspace({
|
||||
workspaceName: projectName,
|
||||
organizationId
|
||||
});
|
||||
|
||||
// create and upload new (encrypted) project key
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
if (!PRIVATE_KEY) throw new Error("Failed to find private key");
|
||||
|
||||
// create new project
|
||||
project = await createWorkspace({
|
||||
workspaceName: projectName,
|
||||
organizationId
|
||||
});
|
||||
const user = await fetchUserDetails();
|
||||
|
||||
// create and upload new (encrypted) project key
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
|
||||
|
||||
if (!PRIVATE_KEY) throw new Error("Failed to find private key");
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const user = await getUser();
|
||||
await uploadWsKey({
|
||||
workspaceId: workspace._id,
|
||||
userId: user._id,
|
||||
encryptedKey: ciphertext,
|
||||
nonce
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadKeys(project._id, user._id, ciphertext, nonce);
|
||||
|
||||
// encrypt and upload secrets to new project
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeAdded,
|
||||
workspaceId: project._id,
|
||||
env: "dev"
|
||||
});
|
||||
|
||||
await addSecrets({
|
||||
secrets: secrets ?? [],
|
||||
env: "dev",
|
||||
workspaceId: project._id
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error("Failed to init project in organization", err);
|
||||
}
|
||||
|
||||
return project;
|
||||
// encrypt and upload secrets to new project
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeAdded,
|
||||
workspaceId: workspace._id,
|
||||
env: "dev"
|
||||
});
|
||||
|
||||
secrets?.forEach((secret) => {
|
||||
createSecret({
|
||||
workspaceId: workspace._id,
|
||||
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: "/",
|
||||
metadata: {
|
||||
source: "signup"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return workspace;
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { EventType, UserAgentType } from "./enums";
|
||||
|
||||
export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.GET_SECRETS]: "List secrets",
|
||||
[EventType.GET_SECRET]: "Read secret",
|
||||
[EventType.CREATE_SECRET]: "Create secret",
|
||||
[EventType.UPDATE_SECRET]: "Update secret",
|
||||
[EventType.DELETE_SECRET]: "Delete secret",
|
||||
[EventType.GET_WORKSPACE_KEY]: "Read project key",
|
||||
[EventType.AUTHORIZE_INTEGRATION]: "Authorize integration",
|
||||
[EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration",
|
||||
[EventType.CREATE_INTEGRATION]: "Create integration",
|
||||
[EventType.DELETE_INTEGRATION]: "Delete integration",
|
||||
[EventType.ADD_TRUSTED_IP]: "Add trusted IP",
|
||||
[EventType.UPDATE_TRUSTED_IP]: "Update trusted IP",
|
||||
[EventType.DELETE_TRUSTED_IP]: "Delete trusted IP",
|
||||
[EventType.CREATE_SERVICE_TOKEN]: "Create service token",
|
||||
[EventType.DELETE_SERVICE_TOKEN]: "Delete service token",
|
||||
[EventType.CREATE_ENVIRONMENT]: "Create environment",
|
||||
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
|
||||
[EventType.DELETE_ENVIRONMENT]: "Delete environment",
|
||||
[EventType.ADD_WORKSPACE_MEMBER]: "Add member",
|
||||
[EventType.REMOVE_WORKSPACE_MEMBER]: "Remove member",
|
||||
[EventType.CREATE_FOLDER]: "Create folder",
|
||||
[EventType.UPDATE_FOLDER]: "Update folder",
|
||||
[EventType.DELETE_FOLDER]: "Delete folder",
|
||||
[EventType.CREATE_WEBHOOK]: "Create webhook",
|
||||
[EventType.UPDATE_WEBHOOK_STATUS]: "Update webhook status",
|
||||
[EventType.DELETE_WEBHOOK]: "Delete webhook",
|
||||
[EventType.GET_SECRET_IMPORTS]: "List secret imports",
|
||||
[EventType.CREATE_SECRET_IMPORT]: "Create secret import",
|
||||
[EventType.UPDATE_SECRET_IMPORT]: "Update secret import",
|
||||
[EventType.DELETE_SECRET_IMPORT]: "Delete secret import",
|
||||
[EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS]: "Update denied permissions",
|
||||
[EventType.UPDATE_USER_WORKSPACE_ROLE]: "Update user role"
|
||||
};
|
||||
|
||||
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
|
||||
[UserAgentType.WEB]: "Web",
|
||||
[UserAgentType.CLI]: "CLI",
|
||||
[UserAgentType.K8_OPERATOR]: "K8s operator",
|
||||
[UserAgentType.OTHER]: "Other",
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
export enum ActorType {
|
||||
USER = "user",
|
||||
SERVICE = "service"
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
WEB = "web",
|
||||
CLI = "cli",
|
||||
K8_OPERATOR = "k8-operator",
|
||||
OTHER = "other"
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
GET_SECRETS = "get-secrets",
|
||||
GET_SECRET = "get-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
CREATE_INTEGRATION = "create-integration",
|
||||
DELETE_INTEGRATION = "delete-integration",
|
||||
ADD_TRUSTED_IP = "add-trusted-ip",
|
||||
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
||||
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
||||
CREATE_SERVICE_TOKEN = "create-service-token",
|
||||
DELETE_SERVICE_TOKEN = "delete-service-token",
|
||||
CREATE_ENVIRONMENT = "create-environment",
|
||||
UPDATE_ENVIRONMENT = "update-environment",
|
||||
DELETE_ENVIRONMENT = "delete-environment",
|
||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
||||
CREATE_FOLDER = "create-folder",
|
||||
UPDATE_FOLDER = "update-folder",
|
||||
DELETE_FOLDER = "delete-folder",
|
||||
CREATE_WEBHOOK = "create-webhook",
|
||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||
DELETE_WEBHOOK = "delete-webhook",
|
||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./queries";
|
||||
@@ -0,0 +1,59 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import {
|
||||
Actor,
|
||||
AuditLog,
|
||||
AuditLogFilters
|
||||
} from "./types";
|
||||
|
||||
export const workspaceKeys = {
|
||||
getAuditLogs: (workspaceId: string, filters: AuditLogFilters) => [{ workspaceId, filters }, "audit-logs"] as const,
|
||||
getAuditLogActorFilterOpts: (workspaceId: string) => [{ workspaceId }, "audit-log-actor-filters"] as const
|
||||
}
|
||||
|
||||
export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getAuditLogs(workspaceId, filters),
|
||||
queryFn: async () => {
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (filters.eventType) {
|
||||
params.append("eventType", filters.eventType);
|
||||
}
|
||||
|
||||
if (filters.userAgentType) {
|
||||
params.append("userAgentType", filters.userAgentType);
|
||||
}
|
||||
|
||||
if (filters.actor) {
|
||||
params.append("actor", filters.actor);
|
||||
}
|
||||
|
||||
if (filters.startDate) {
|
||||
params.append("startDate", filters.startDate.toISOString());
|
||||
}
|
||||
|
||||
if (filters.endDate) {
|
||||
params.append("endDate", filters.endDate.toISOString());
|
||||
}
|
||||
|
||||
params.append("offset", String(filters.offset));
|
||||
params.append("limit", String(filters.limit));
|
||||
|
||||
const { data } = await apiRequest.get<{ auditLogs: AuditLog[], totalCount: number }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params });
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getAuditLogActorFilterOpts(workspaceId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ actors: Actor[] }>(`/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`);
|
||||
return data.actors;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
import {
|
||||
ActorType,
|
||||
EventType,
|
||||
UserAgentType
|
||||
} from "./enums";
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ServiceActorMetadata {
|
||||
serviceId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
||||
interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
}
|
||||
|
||||
export interface ServiceActor {
|
||||
type: ActorType.SERVICE;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor =
|
||||
| UserActor
|
||||
| ServiceActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretEvent {
|
||||
type: EventType.GET_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretEvent {
|
||||
type: EventType.CREATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateSecretEvent {
|
||||
type: EventType.UPDATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteSecretEvent {
|
||||
type: EventType.DELETE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface GetWorkspaceKeyEvent {
|
||||
type: EventType.GET_WORKSPACE_KEY,
|
||||
metadata: {
|
||||
keyId: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface AuthorizeIntegrationEvent {
|
||||
type: EventType.AUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface UnauthorizeIntegrationEvent {
|
||||
type: EventType.UNAUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateIntegrationEvent {
|
||||
type: EventType.CREATE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteIntegrationEvent {
|
||||
type: EventType.DELETE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface AddTrustedIPEvent {
|
||||
type: EventType.ADD_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateTrustedIPEvent {
|
||||
type: EventType.UPDATE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteTrustedIPEvent {
|
||||
type: EventType.DELETE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateServiceTokenEvent {
|
||||
type: EventType.CREATE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteServiceTokenEvent {
|
||||
type: EventType.DELETE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateEnvironmentEvent {
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
oldName: string;
|
||||
newName: string;
|
||||
oldSlug: string;
|
||||
newSlug: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteEnvironmentEvent {
|
||||
type: EventType.DELETE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface AddWorkspaceMemberEvent {
|
||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface RemoveWorkspaceMemberEvent {
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateFolderEvent {
|
||||
type: EventType.CREATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateFolderEvent {
|
||||
type: EventType.UPDATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
oldFolderName: string;
|
||||
newFolderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteFolderEvent {
|
||||
type: EventType.DELETE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateWebhookEvent {
|
||||
type: EventType.CREATE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateWebhookStatusEvent {
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteWebhookEvent {
|
||||
type: EventType.DELETE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
interface GetSecretImportsEvent {
|
||||
type: EventType.GET_SECRET_IMPORTS,
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
numberOfImports: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface CreateSecretImportEvent {
|
||||
type: EventType.CREATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateSecretImportEvent {
|
||||
type: EventType.UPDATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
orderBefore: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[],
|
||||
orderAfter: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteSecretImportEvent {
|
||||
type: EventType.DELETE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateUserRole {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
oldRole: string;
|
||||
newRole: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateUserDeniedPermissions {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
deniedPermissions: {
|
||||
environmentSlug: string;
|
||||
ability: string;
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
| UpdateSecretEvent
|
||||
| DeleteSecretEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
| CreateIntegrationEvent
|
||||
| DeleteIntegrationEvent
|
||||
| AddTrustedIPEvent
|
||||
| UpdateTrustedIPEvent
|
||||
| DeleteTrustedIPEvent
|
||||
| CreateServiceTokenEvent
|
||||
| DeleteServiceTokenEvent
|
||||
| CreateEnvironmentEvent
|
||||
| UpdateEnvironmentEvent
|
||||
| DeleteEnvironmentEvent
|
||||
| AddWorkspaceMemberEvent
|
||||
| RemoveWorkspaceMemberEvent
|
||||
| CreateFolderEvent
|
||||
| UpdateFolderEvent
|
||||
| DeleteFolderEvent
|
||||
| CreateWebhookEvent
|
||||
| UpdateWebhookStatusEvent
|
||||
| DeleteWebhookEvent
|
||||
| GetSecretImportsEvent
|
||||
| CreateSecretImportEvent
|
||||
| UpdateSecretImportEvent
|
||||
| DeleteSecretImportEvent
|
||||
| UpdateUserRole
|
||||
| UpdateUserDeniedPermissions;
|
||||
|
||||
export type AuditLog = {
|
||||
_id: string;
|
||||
actor: Actor;
|
||||
organization: string;
|
||||
workspace: string;
|
||||
ipAddress: string;
|
||||
event: Event;
|
||||
userAgent: string;
|
||||
userAgentType: UserAgentType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type AuditLogFilters = {
|
||||
eventType?: EventType;
|
||||
userAgentType?: UserAgentType;
|
||||
actor?: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
export {
|
||||
useGetAuthToken,
|
||||
useGetCommonPasswords,
|
||||
useResetPassword,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken
|
||||
} from "./queries"
|
||||
useSendPasswordResetEmail,
|
||||
useSendVerificationEmail,
|
||||
useVerifyEmailVerificationCode,
|
||||
useVerifyMfaToken,
|
||||
useVerifyPasswordResetCode} from "./queries"
|
||||
|
||||
@@ -4,16 +4,86 @@ import { apiRequest } from "@app/config/request";
|
||||
import { setAuthToken } from "@app/reactQuery";
|
||||
|
||||
import {
|
||||
ChangePasswordDTO,
|
||||
CompleteAccountDTO,
|
||||
CompleteAccountSignupDTO,
|
||||
GetAuthTokenAPI,
|
||||
GetBackupEncryptedPrivateKeyDTO,
|
||||
IssueBackupPrivateKeyDTO,
|
||||
Login1DTO,
|
||||
Login1Res,
|
||||
Login2DTO,
|
||||
Login2Res,
|
||||
ResetPasswordDTO,
|
||||
SendMfaTokenDTO,
|
||||
SRP1DTO,
|
||||
SRPR1Res,
|
||||
VerifyMfaTokenDTO,
|
||||
VerifyMfaTokenRes} from "./types";
|
||||
VerifyMfaTokenRes,
|
||||
VerifySignupInviteDTO} from "./types";
|
||||
|
||||
const authKeys = {
|
||||
getAuthToken: ["token"] as const,
|
||||
commonPasswords: ["common-passwords"] as const
|
||||
};
|
||||
|
||||
export const login1 = async (loginDetails: Login1DTO) => {
|
||||
const { data } = await apiRequest.post<Login1Res>("/api/v3/auth/login1", loginDetails);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const login2 = async (loginDetails: Login2DTO) => {
|
||||
const { data } = await apiRequest.post<Login2Res>("/api/v3/auth/login2", loginDetails);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const useLogin1 = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (details: {
|
||||
email: string;
|
||||
clientPublicKey: string;
|
||||
providerAuthToken?: string;
|
||||
}) => {
|
||||
return login1(details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useLogin2 = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (details: {
|
||||
email: string;
|
||||
clientProof: string;
|
||||
providerAuthToken?: string;
|
||||
}) => {
|
||||
return login2(details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const srp1 = async (details: SRP1DTO) => {
|
||||
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const useCompleteAccountSignup = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (details: CompleteAccountSignupDTO) => {
|
||||
return completeAccountSignup(details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useSendMfaToken = () => {
|
||||
return useMutation<{}, {}, SendMfaTokenDTO>({
|
||||
mutationFn: async ({ email }) => {
|
||||
@@ -23,18 +93,161 @@ export const useSendMfaToken = () => {
|
||||
});
|
||||
}
|
||||
|
||||
export const verifyMfaToken = async ({
|
||||
email,
|
||||
mfaCode
|
||||
}: {
|
||||
email: string;
|
||||
mfaCode: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
||||
email,
|
||||
mfaToken: mfaCode
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export const useVerifyMfaToken = () => {
|
||||
return useMutation<VerifyMfaTokenRes, {}, VerifyMfaTokenDTO>({
|
||||
mutationFn: async ({ email, mfaCode }) => {
|
||||
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
||||
return verifyMfaToken({
|
||||
email,
|
||||
mfaToken: mfaCode
|
||||
mfaCode
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const verifySignupInvite = async (details: VerifySignupInviteDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v1/invite-org/verify", details);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const useSendVerificationEmail = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
email
|
||||
}: {
|
||||
email: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/signup/email/signup", {
|
||||
email
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useVerifyEmailVerificationCode = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
email,
|
||||
code
|
||||
}: {
|
||||
email: string;
|
||||
code: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/signup/email/verify", {
|
||||
email,
|
||||
code
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useSendPasswordResetEmail = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
email
|
||||
}: {
|
||||
email: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/email/password-reset", {
|
||||
email
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useVerifyPasswordResetCode = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
email,
|
||||
code
|
||||
}: {
|
||||
email: string;
|
||||
code: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
|
||||
email,
|
||||
code
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const getBackupEncryptedPrivateKey = async ({
|
||||
verificationToken
|
||||
}: GetBackupEncryptedPrivateKeyDTO) => {
|
||||
const { data } = await apiRequest.get("/api/v1/password/backup-private-key", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${verificationToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return data.backupPrivateKey;
|
||||
}
|
||||
|
||||
export const useResetPassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: async (details: ResetPasswordDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/password-reset", {
|
||||
protectedKey: details.protectedKey,
|
||||
protectedKeyIV: details.protectedKeyIV,
|
||||
protectedKeyTag: details.protectedKeyTag,
|
||||
encryptedPrivateKey: details.encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
|
||||
salt: details.salt,
|
||||
verifier: details.verifier
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${details.verificationToken}`
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const changePassword = async (details: ChangePasswordDTO) => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
|
||||
return data;
|
||||
}
|
||||
|
||||
export const useChangePassword = () => {
|
||||
// note: use after srp1
|
||||
return useMutation({
|
||||
mutationFn: async (details: ChangePasswordDTO) => {
|
||||
return changePassword(details);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh token is set as cookie when logged in
|
||||
// Using that we fetch the auth bearer token needed for auth calls
|
||||
const fetchAuthToken = async () => {
|
||||
|
||||
@@ -21,4 +21,107 @@ export type VerifyMfaTokenRes = {
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export type Login1DTO = {
|
||||
email: string;
|
||||
clientPublicKey: string;
|
||||
providerAuthToken?: string;
|
||||
}
|
||||
|
||||
export type Login2DTO = {
|
||||
email: string;
|
||||
clientProof: string;
|
||||
providerAuthToken?: string;
|
||||
}
|
||||
|
||||
export type Login1Res = {
|
||||
serverPublicKey: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
export type Login2Res = {
|
||||
mfaEnabled: boolean;
|
||||
token: string;
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
export type SRP1DTO = {
|
||||
clientPublicKey: string;
|
||||
}
|
||||
|
||||
export type SRPR1Res = {
|
||||
serverPublicKey: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
export type CompleteAccountDTO = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
}
|
||||
|
||||
export type CompleteAccountSignupDTO = CompleteAccountDTO & {
|
||||
providerAuthToken?: string;
|
||||
attributionSource?: string;
|
||||
organizationName: string;
|
||||
}
|
||||
|
||||
export type VerifySignupInviteDTO = {
|
||||
email: string;
|
||||
code: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
export type ChangePasswordDTO = {
|
||||
clientProof: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
}
|
||||
|
||||
export type ResetPasswordDTO = {
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
verificationToken: string;
|
||||
}
|
||||
|
||||
export type IssueBackupPrivateKeyDTO = {
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
clientProof: string;
|
||||
}
|
||||
|
||||
export type GetBackupEncryptedPrivateKeyDTO = {
|
||||
verificationToken: string;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -8,18 +8,16 @@ const incidentContactKeys = {
|
||||
getAllContact: (orgId: string) => ["org-incident-contacts", { orgId }] as const
|
||||
};
|
||||
|
||||
const fetchOrgIncidentContacts = async (orgId: string) => {
|
||||
const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>(
|
||||
`/api/v1/organization/${orgId}/incidentContactOrg`
|
||||
);
|
||||
|
||||
return data.incidentContactsOrg;
|
||||
};
|
||||
|
||||
export const useGetOrgIncidentContact = (orgId: string) =>
|
||||
useQuery({
|
||||
queryKey: incidentContactKeys.getAllContact(orgId),
|
||||
queryFn: () => fetchOrgIncidentContacts(orgId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>(
|
||||
`/api/v1/organization/${orgId}/incidentContactOrg`
|
||||
);
|
||||
|
||||
return data.incidentContactsOrg;
|
||||
},
|
||||
enabled: Boolean(orgId)
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./auditLogs";
|
||||
export * from "./auth";
|
||||
export * from "./bots";
|
||||
export * from "./incidentContacts";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export {
|
||||
useAuthorizeIntegration,
|
||||
useDeleteIntegrationAuth,
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthBitBucketWorkspaces,
|
||||
@@ -7,4 +8,6 @@ export {
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches} from "./queries";
|
||||
useGetIntegrationAuthVercelBranches,
|
||||
useSaveIntegrationAccessToken
|
||||
} from "./queries";
|
||||
|
||||
@@ -312,6 +312,69 @@ export const useGetIntegrationAuthNorthflankSecretGroups = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const useAuthorizeIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
workspaceId,
|
||||
code,
|
||||
integration
|
||||
}: {
|
||||
workspaceId: string;
|
||||
code: string;
|
||||
integration: string;
|
||||
}) => {
|
||||
const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/oauth-token", {
|
||||
workspaceId,
|
||||
code,
|
||||
integration
|
||||
});
|
||||
|
||||
return integrationAuth;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(res.workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useSaveIntegrationAccessToken = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
accessId,
|
||||
accessToken,
|
||||
url,
|
||||
namespace
|
||||
}: {
|
||||
workspaceId: string | null;
|
||||
integration: string | undefined;
|
||||
accessId: string | null;
|
||||
accessToken: string;
|
||||
url: string | null;
|
||||
namespace: string | null;
|
||||
}) => {
|
||||
const { data: { integrationAuth } } = await apiRequest.post("/api/v1/integration-auth/access-token", {
|
||||
workspaceId,
|
||||
integration,
|
||||
accessId,
|
||||
accessToken,
|
||||
url,
|
||||
namespace
|
||||
});
|
||||
|
||||
return integrationAuth;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(res.workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIntegrationAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
export { useDeleteIntegration,useGetCloudIntegrations } from "./queries";
|
||||
export {
|
||||
useCreateIntegration,
|
||||
useDeleteIntegration,
|
||||
useGetCloudIntegrations
|
||||
} from "./queries";
|
||||
|
||||
@@ -23,6 +23,63 @@ export const useGetCloudIntegrations = () =>
|
||||
queryFn: () => fetchIntegrations()
|
||||
});
|
||||
|
||||
export const useCreateIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
isActive: boolean;
|
||||
secretPath: string;
|
||||
app: string | null;
|
||||
appId: string | null;
|
||||
sourceEnvironment: string;
|
||||
targetEnvironment: string | null;
|
||||
targetEnvironmentId: string | null;
|
||||
targetService: string | null;
|
||||
targetServiceId: string | null;
|
||||
owner: string | null;
|
||||
path: string | null;
|
||||
region: string | null;
|
||||
}) => {
|
||||
const { data: { integration } } = await apiRequest.post("/api/v1/integration", {
|
||||
integrationAuthId,
|
||||
isActive,
|
||||
app,
|
||||
appId,
|
||||
sourceEnvironment,
|
||||
targetEnvironment,
|
||||
targetEnvironmentId,
|
||||
targetService,
|
||||
targetServiceId,
|
||||
owner,
|
||||
path,
|
||||
region,
|
||||
secretPath
|
||||
});
|
||||
|
||||
return integration;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(res.workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIntegration = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -32,4 +89,4 @@ export const useDeleteIntegration = () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -8,7 +8,7 @@ const encKeyKeys = {
|
||||
getUserWorkspaceKey: (workspaceID: string) => ["workspace-key-pair", { workspaceID }] as const
|
||||
};
|
||||
|
||||
const fetchUserWsKey = async (workspaceID: string) => {
|
||||
export const fetchUserWsKey = async (workspaceID: string) => {
|
||||
const { data } = await apiRequest.get<{ latestKey: UserWsKeyPair }>(
|
||||
`/api/v1/key/${workspaceID}/latest`
|
||||
);
|
||||
@@ -24,8 +24,23 @@ export const useGetUserWsKey = (workspaceID: string) =>
|
||||
});
|
||||
|
||||
// mutations
|
||||
export const uploadWsKey = async ({
|
||||
workspaceId,
|
||||
userId,
|
||||
encryptedKey,
|
||||
nonce
|
||||
}: UploadWsKeyDTO) => {
|
||||
return apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
|
||||
}
|
||||
|
||||
export const useUploadWsKey = () =>
|
||||
useMutation<{}, {}, UploadWsKeyDTO>({
|
||||
mutationFn: ({ encryptedKey, nonce, userId, workspaceId }) =>
|
||||
apiRequest.post(`/api/v1/key/${workspaceId}`, { key: { userId, encryptedKey, nonce } })
|
||||
mutationFn: async ({ encryptedKey, nonce, userId, workspaceId }) => {
|
||||
return uploadWsKey({
|
||||
workspaceId,
|
||||
userId,
|
||||
encryptedKey,
|
||||
nonce
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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,16 @@ const organizationKeys = {
|
||||
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const
|
||||
};
|
||||
|
||||
export const useGetOrganization = () => {
|
||||
return useQuery({
|
||||
queryKey: organizationKeys.getUserOrganization,
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization");
|
||||
export const fetchOrganizations = async () => {
|
||||
const { data: { organizations } } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization");
|
||||
return organizations;
|
||||
}
|
||||
|
||||
return data.organizations;
|
||||
export const useGetOrganizations = () => {
|
||||
return useQuery({
|
||||
queryKey: organizationKeys.getUserOrganizations,
|
||||
queryFn: async () => {
|
||||
return fetchOrganizations();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -41,10 +45,11 @@ export const useRenameOrg = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, RenameOrgDTO>({
|
||||
mutationFn: ({ newOrgName, orgId }) =>
|
||||
apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName }),
|
||||
mutationFn: ({ newOrgName, orgId }) => {
|
||||
return 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)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -79,7 +79,7 @@ export type UpdateSecretArg = {
|
||||
|
||||
export type CreateSecretArg = Omit<UpdateSecretArg, "_id">;
|
||||
|
||||
export type DeleteSecretArg = { _id: string };
|
||||
export type DeleteSecretArg = { _id: string, secretName: string; };
|
||||
|
||||
export type BatchSecretDTO = {
|
||||
workspaceId: string;
|
||||
@@ -147,3 +147,25 @@ 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;
|
||||
metadata?: {
|
||||
source?: 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;
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ export type SubscriptionPlan = {
|
||||
membersUsed: number;
|
||||
memberLimit: number;
|
||||
auditLogs: boolean;
|
||||
auditLogsRetentionDays: number;
|
||||
customAlerts: boolean;
|
||||
customRateLimits: boolean;
|
||||
pitRecovery: boolean;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
UserWsTags
|
||||
} from "./types";
|
||||
|
||||
|
||||
const workspaceTags = {
|
||||
getWsTags: (workspaceID: string) => ["workspace-tags", { workspaceID }] as const
|
||||
};
|
||||
@@ -23,12 +22,13 @@ const fetchWsTag = async (workspaceID: string) => {
|
||||
return data.workspaceTags;
|
||||
};
|
||||
|
||||
export const useGetWsTags = (workspaceID: string) =>
|
||||
useQuery({
|
||||
export const useGetWsTags = (workspaceID: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceTags.getWsTags(workspaceID),
|
||||
queryFn: () => fetchWsTag(workspaceID),
|
||||
enabled: Boolean(workspaceID)
|
||||
});
|
||||
}
|
||||
|
||||
export const useCreateWsTag = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -59,4 +59,4 @@ export const useDeleteWsTag = () => {
|
||||
queryClient.invalidateQueries(workspaceTags.getWsTags(tagData?.workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,7 @@ export {
|
||||
useDeleteOrgMembership,
|
||||
useGetMyAPIKeys,
|
||||
useGetMyIp,
|
||||
useGetMyOrganizationProjects,
|
||||
useGetMySessions,
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
@@ -14,6 +15,7 @@ export {
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
useRevokeMySessions,
|
||||
useUpdateMfaEnabled,
|
||||
useUpdateOrgUserRole,
|
||||
useUpdateUserAuthProvider
|
||||
} from "./queries";
|
||||
useUpdateUserAuthMethods
|
||||
} from "./queries";
|
||||
@@ -13,13 +13,15 @@ import {
|
||||
AddUserToWsDTO,
|
||||
AddUserToWsRes,
|
||||
APIKeyData,
|
||||
AuthMethod,
|
||||
CreateAPIKeyRes,
|
||||
DeletOrgMembershipDTO,
|
||||
OrgUser,
|
||||
RenameUserDTO,
|
||||
TokenVersion,
|
||||
UpdateOrgUserRoleDTO,
|
||||
User} from "./types";
|
||||
User
|
||||
} from "./types";
|
||||
|
||||
const userKeys = {
|
||||
getUser: ["user"] as const,
|
||||
@@ -27,7 +29,8 @@ const userKeys = {
|
||||
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
||||
myIp: ["ip"] as const,
|
||||
myAPIKeys: ["api-keys"] as const,
|
||||
mySessions: ["sessions"] as const
|
||||
mySessions: ["sessions"] as const,
|
||||
myOrganizationProjects: (orgId: string) => [{ orgId }, "organization-projects"] as const
|
||||
};
|
||||
|
||||
export const fetchUserDetails = async () => {
|
||||
@@ -38,7 +41,7 @@ export const fetchUserDetails = async () => {
|
||||
|
||||
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
|
||||
|
||||
const fetchUserAction = async (action: string) => {
|
||||
export const fetchUserAction = async (action: string) => {
|
||||
const { data } = await apiRequest.get<{ userAction: string }>("/api/v1/user-action", {
|
||||
params: {
|
||||
action
|
||||
@@ -59,17 +62,18 @@ export const useRenameUser = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserAuthProvider = () => {
|
||||
|
||||
export const useUpdateUserAuthMethods = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
authProvider
|
||||
authMethods
|
||||
}: {
|
||||
authProvider: string;
|
||||
authMethods: AuthMethod[];
|
||||
}) => {
|
||||
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
|
||||
authProvider
|
||||
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-methods", {
|
||||
authMethods
|
||||
});
|
||||
|
||||
return user;
|
||||
@@ -146,7 +150,9 @@ export const useAddUserToOrg = () => {
|
||||
}
|
||||
|
||||
return useMutation<Response, {}, AddUserToOrgDTO>({
|
||||
mutationFn: (dto) => apiRequest.post("/api/v1/invite-org/signup", dto),
|
||||
mutationFn: (dto) => {
|
||||
return apiRequest.post("/api/v1/invite-org/signup", dto);
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||
}
|
||||
@@ -157,8 +163,9 @@ export const useDeleteOrgMembership = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, DeletOrgMembershipDTO>({
|
||||
mutationFn: ({ membershipId, orgId }) =>
|
||||
apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`),
|
||||
mutationFn: ({ membershipId, orgId }) => {
|
||||
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`)
|
||||
},
|
||||
onSuccess: (_, { orgId }) => {
|
||||
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
|
||||
}
|
||||
@@ -169,10 +176,11 @@ export const useUpdateOrgUserRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
|
||||
mutationFn: ({ organizationId, membershipId, role }) =>
|
||||
apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
|
||||
mutationFn: ({ organizationId, membershipId, role }) => {
|
||||
return apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
|
||||
role
|
||||
}),
|
||||
});
|
||||
},
|
||||
onSuccess: (_, { organizationId }) => {
|
||||
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
|
||||
},
|
||||
@@ -195,7 +203,9 @@ export const useRegisterUserAction = () => {
|
||||
|
||||
export const useLogoutUser = () =>
|
||||
useMutation({
|
||||
mutationFn: () => apiRequest.post("/api/v1/auth/logout"),
|
||||
mutationFn: async () => {
|
||||
await apiRequest.post("/api/v1/auth/logout");
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAuthToken("");
|
||||
// Delete the cookie by not setting a value; Alternatively clear the local storage
|
||||
@@ -303,4 +313,45 @@ export const useRevokeMySessions = () => {
|
||||
queryClient.invalidateQueries(userKeys.mySessions);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useUpdateMfaEnabled = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
isMfaEnabled
|
||||
}: {
|
||||
isMfaEnabled: boolean;
|
||||
}) => {
|
||||
const { data: { user } } = await apiRequest.patch(
|
||||
"/api/v2/users/me/mfa",
|
||||
{
|
||||
isMfaEnabled
|
||||
}
|
||||
);
|
||||
|
||||
return user;
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const fetchMyOrganizationProjects = async (orgId: string) => {
|
||||
const { data: { workspaces } } = await apiRequest.get(
|
||||
`/api/v1/organization/${orgId}/my-workspaces`
|
||||
);
|
||||
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
export const useGetMyOrganizationProjects = (orgId: string) => {
|
||||
return useQuery({
|
||||
queryKey: userKeys.myOrganizationProjects(orgId),
|
||||
queryFn: async () => {
|
||||
return fetchMyOrganizationProjects(orgId);
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
|
||||
export enum AuthProvider {
|
||||
export enum AuthMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
OKTA_SAML = "okta-saml"
|
||||
GITHUB = "github",
|
||||
OKTA_SAML = "okta-saml",
|
||||
AZURE_SAML = "azure-saml",
|
||||
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||
}
|
||||
|
||||
export type User = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
email?: string;
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
authProvider?: AuthProvider;
|
||||
authProvider?: AuthMethod;
|
||||
authMethods: AuthMethod[];
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export {
|
||||
useAddUserToWorkspace,
|
||||
useCreateWorkspace,
|
||||
useCreateWsEnvironment,
|
||||
useDeleteUserFromWorkspace,
|
||||
useDeleteWorkspace,
|
||||
useDeleteWsEnvironment,
|
||||
useGetUserWorkspaceMemberships,
|
||||
@@ -11,8 +13,10 @@ export {
|
||||
useGetWorkspaceIndexStatus,
|
||||
useGetWorkspaceIntegrations,
|
||||
useGetWorkspaceSecrets,
|
||||
useGetWorkspaceUsers,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
useReorderWsEnvironment,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateWsEnvironment
|
||||
} from "./queries";
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUpdateWsEnvironment} from "./queries";
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
GetWsEnvironmentDTO,
|
||||
NameWorkspaceSecretsDTO,
|
||||
RenameWorkspaceDTO,
|
||||
ReorderEnvironmentsDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
Workspace,
|
||||
@@ -28,7 +29,9 @@ export const workspaceKeys = {
|
||||
getWorkspaceAuthorization: (workspaceId: string) => [{ workspaceId }, "workspace-authorizations"],
|
||||
getWorkspaceIntegrations: (workspaceId: string) => [{ workspaceId }, "workspace-integrations"],
|
||||
getAllUserWorkspace: ["workspaces"] as const,
|
||||
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const
|
||||
getUserWsEnvironments: (workspaceId: string) => ["workspace-env", { workspaceId }] as const,
|
||||
getWorkspaceAuditLogs: (workspaceId: string) => [{ workspaceId }] as const,
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }] as const
|
||||
};
|
||||
|
||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||
@@ -165,13 +168,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);
|
||||
}
|
||||
@@ -182,8 +193,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);
|
||||
}
|
||||
@@ -208,7 +220,9 @@ export const useDeleteWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, DeleteWorkspaceDTO>({
|
||||
mutationFn: ({ workspaceID }) => apiRequest.delete(`/api/v1/workspace/${workspaceID}`),
|
||||
mutationFn: ({ workspaceID }) => {
|
||||
return apiRequest.delete(`/api/v1/workspace/${workspaceID}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
@@ -219,11 +233,27 @@ 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useReorderWsEnvironment = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, ReorderEnvironmentsDTO>({
|
||||
mutationFn: ({ workspaceID, environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName}) => {
|
||||
return apiRequest.patch(`/api/v2/workspace/${workspaceID}/environments`, {
|
||||
environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
@@ -234,12 +264,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);
|
||||
}
|
||||
@@ -250,12 +281,85 @@ 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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceUsers = (workspaceId: string) => {
|
||||
return useQuery({
|
||||
queryKey: workspaceKeys.getWorkspaceUsers(workspaceId),
|
||||
queryFn: async () => {
|
||||
const { data: { users } } = await apiRequest.get(
|
||||
`/api/v1/workspace/${workspaceId}/users`
|
||||
);
|
||||
return users;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useAddUserToWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
email,
|
||||
workspaceId
|
||||
}: {
|
||||
email: string;
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
const { data: { invitee, latestKey } } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
|
||||
|
||||
return ({
|
||||
invitee,
|
||||
latestKey
|
||||
});
|
||||
},
|
||||
onSuccess: (_, dto) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteUserFromWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (membershipId: string) => {
|
||||
const { data: { deletedMembership } } = await apiRequest.delete(`/api/v1/membership/${membershipId}`);
|
||||
return deletedMembership;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(res.workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserWorkspaceRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
membershipId,
|
||||
role
|
||||
}: {
|
||||
membershipId: string;
|
||||
role: string;
|
||||
}) => {
|
||||
const { data: { membership } } = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, {
|
||||
role
|
||||
});
|
||||
return membership;
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(res.workspace));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -46,6 +46,15 @@ export type CreateEnvironmentDTO = {
|
||||
environmentName: string;
|
||||
};
|
||||
|
||||
export type ReorderEnvironmentsDTO = {
|
||||
workspaceID: string;
|
||||
environmentSlug: string;
|
||||
environmentName: string;
|
||||
otherEnvironmentSlug: string;
|
||||
otherEnvironmentName: string;
|
||||
|
||||
};
|
||||
|
||||
export type UpdateEnvironmentDTO = {
|
||||
workspaceID: string;
|
||||
oldEnvironmentSlug: string;
|
||||
@@ -53,4 +62,4 @@ export type UpdateEnvironmentDTO = {
|
||||
environmentName: string;
|
||||
};
|
||||
|
||||
export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string };
|
||||
export type DeleteEnvironmentDTO = { workspaceID: string; environmentSlug: string };
|
||||
@@ -495,6 +495,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
{/* <Link href={`/project/${currentWorkspace?._id}/logs`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?._id}/logs`
|
||||
}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link> */}
|
||||
{/* <Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -28,6 +28,7 @@ import { queryClient } from "@app/reactQuery";
|
||||
|
||||
import "nprogress/nprogress.css";
|
||||
import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||
import "react-day-picker/dist/style.css";
|
||||
import "../styles/globals.css";
|
||||
|
||||
import "@app/i18n";
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
interface Props {
|
||||
clientProof: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the second step of the change password process (pake)
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const changePassword2 = async ({
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
}: Props) => {
|
||||
const { data } = await apiRequest.post("/api/v1/password/change-password", {
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default changePassword2;
|
||||
@@ -4,12 +4,13 @@ import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
* This function is used to check if the user is authenticated.
|
||||
* To do that, we get their tokens from cookies, and verify if they are good.
|
||||
*/
|
||||
const checkAuth = async () =>
|
||||
SecurityClient.fetchCall("/api/v1/auth/checkAuth", {
|
||||
const checkAuth = async () => {
|
||||
return SecurityClient.fetchCall("/api/v1/auth/checkAuth", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then((res) => res);
|
||||
}
|
||||
|
||||
export default checkAuth;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route check the verification code from the email that user just recieved
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email
|
||||
* @param {string} obj.code
|
||||
* @returns
|
||||
*/
|
||||
const checkEmailVerificationCode = ({ email, code }: Props) => fetch("/api/v1/signup/email/verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
code
|
||||
})
|
||||
});
|
||||
|
||||
export default checkEmailVerificationCode;
|
||||
@@ -1,79 +0,0 @@
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
interface Props {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
providerAuthToken?: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
organizationName: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
attributionSource?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of the user completing signup
|
||||
* @param {string} obj.firstName - first name of the user completing signup
|
||||
* @param {string} obj.lastName - last name of the user completing sign up
|
||||
* @param {string} obj.protectedKey - protected key in encryption version 2
|
||||
* @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2
|
||||
* @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2
|
||||
* @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
|
||||
* @param {string} obj.publicKey - public key of the user completing signup
|
||||
* @param {string} obj.ciphertext
|
||||
* @param {string} obj.iv
|
||||
* @param {string} obj.tag
|
||||
* @param {string} obj.salt
|
||||
* @param {string} obj.verifier
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignup = async ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
organizationName,
|
||||
providerAuthToken,
|
||||
attributionSource
|
||||
}: Props) => {
|
||||
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
organizationName,
|
||||
providerAuthToken,
|
||||
...(attributionSource ? { attributionSource } : {})
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default completeAccountInformationSignup;
|
||||
@@ -1,70 +0,0 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
interface Props {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
}
|
||||
|
||||
// missing token?
|
||||
// TODO: add to SecurityClient
|
||||
|
||||
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of the user completing signupinvite flow
|
||||
* @param {string} obj.firstName - first name of the user completing signupinvite flow
|
||||
* @param {string} obj.lastName - last name of the user completing signupinvite flow
|
||||
* @param {string} obj.publicKey - public key of the user completing signupinvite flow
|
||||
* @param {string} obj.ciphertext
|
||||
* @param {string} obj.iv
|
||||
* @param {string} obj.tag
|
||||
* @param {string} obj.salt
|
||||
* @param {string} obj.verifier
|
||||
* @param {string} obj.token - token that confirms a user's identity
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignupInvite = async ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
}: Props) => {
|
||||
const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default completeAccountInformationSignupInvite;
|
||||
@@ -1,34 +0,0 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the second part of the account recovery step (a user needs to verify their email).
|
||||
* A user need to click on a button in a magic link page
|
||||
* @param {object} obj
|
||||
* @param {object} obj.email - email of a user that is trying to recover access to their account
|
||||
* @param {object} obj.code - token that a use received via the magic link
|
||||
* @returns
|
||||
*/
|
||||
const EmailVerifyOnPasswordReset = async ({ email, code }: Props) => {
|
||||
const response = await fetch("/api/v1/password/email/password-reset-verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
code
|
||||
})
|
||||
});
|
||||
if (response?.status === 200) {
|
||||
return response;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Something went wrong during email verification on password reset."
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailVerifyOnPasswordReset;
|
||||
@@ -1,51 +0,0 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
interface Props {
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
clientProof: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the route that issues a backup private key that will afterwards be added into a pdf
|
||||
* @param {object} obj
|
||||
* @param {string} obj.encryptedPrivateKey
|
||||
* @param {string} obj.iv
|
||||
* @param {string} obj.tag
|
||||
* @param {string} obj.salt
|
||||
* @param {string} obj.verifier
|
||||
* @param {string} obj.clientProof
|
||||
* @returns
|
||||
*/
|
||||
const issueBackupPrivateKey = ({
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier,
|
||||
clientProof
|
||||
}: Props) =>
|
||||
SecurityClient.fetchCall("/api/v1/password/backup-private-key", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientProof,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res?.status !== 200) {
|
||||
console.log("Failed to issue the backup key");
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
export default issueBackupPrivateKey;
|
||||
@@ -1,33 +0,0 @@
|
||||
interface Login1 {
|
||||
serverPublicKey: string;
|
||||
salt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the first step of the login process (pake)
|
||||
* @param {*} email
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const login1 = async (loginDetails: {
|
||||
email: string;
|
||||
clientPublicKey: string;
|
||||
providerAuthToken?: string;
|
||||
}) => {
|
||||
const response = await fetch("/api/v3/auth/login1", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(loginDetails),
|
||||
});
|
||||
// need precise error handling about the status code
|
||||
if (response?.status === 200) {
|
||||
const data = (await response.json()) as unknown as Login1;
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error("Wrong password");
|
||||
};
|
||||
|
||||
export default login1;
|
||||
@@ -1,42 +0,0 @@
|
||||
interface Login2Response {
|
||||
mfaEnabled: boolean;
|
||||
token: string;
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the second step of the login process
|
||||
* @param {*} email
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const login2 = async (loginDetails: {
|
||||
email: string;
|
||||
clientProof: string;
|
||||
providerAuthToken?: string;
|
||||
}) => {
|
||||
const response = await fetch("/api/v3/auth/login2", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(loginDetails),
|
||||
credentials: "include"
|
||||
});
|
||||
// need precise error handling about the status code
|
||||
if (response.status === 200) {
|
||||
const data = (await response.json()) as unknown as Login2Response;
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error("Password verification failed");
|
||||
};
|
||||
|
||||
export default login2;
|
||||
@@ -1,41 +0,0 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* This route logs the user out. Note: the user should authorized to do this.
|
||||
* We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
const res = await SecurityClient.fetchCall("/api/v1/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
credentials: "include"
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
SecurityClient.setToken("");
|
||||
// Delete the cookie by not setting a value; Alternatively clear the local storage
|
||||
localStorage.removeItem("protectedKey");
|
||||
localStorage.removeItem("protectedKeyIV");
|
||||
localStorage.removeItem("protectedKeyTag");
|
||||
localStorage.removeItem("publicKey");
|
||||
localStorage.removeItem("encryptedPrivateKey");
|
||||
localStorage.removeItem("iv");
|
||||
localStorage.removeItem("tag");
|
||||
localStorage.removeItem("PRIVATE_KEY");
|
||||
localStorage.removeItem("orgData.id");
|
||||
localStorage.removeItem("projectData.id");
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log("Error logging out", error);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default logout;
|
||||
@@ -1,29 +0,0 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
interface Props {
|
||||
clientPublicKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the first step of the change password process (pake)
|
||||
* @param {string} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const SRP1 = ({ clientPublicKey }: Props) =>
|
||||
SecurityClient.fetchCall("/api/v1/password/srp1", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientPublicKey
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to do the first step of SRP");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default SRP1;
|
||||
@@ -1,33 +0,0 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the first of the account recovery step (a user needs to verify their email).
|
||||
* It will send an email containing a magic link to start the account recovery flow.
|
||||
* @param {object} obj
|
||||
* @param {object} obj.email - email of a user that is trying to recover access to their account
|
||||
* @returns
|
||||
*/
|
||||
const SendEmailOnPasswordReset = async ({ email }: Props) => {
|
||||
const response = await fetch("/api/v1/password/email/password-reset", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email
|
||||
})
|
||||
});
|
||||
// need precise error handling about the status code
|
||||
if (response?.status === 200) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Something went wrong while sending the email verification for password reset."
|
||||
);
|
||||
};
|
||||
|
||||
export default SendEmailOnPasswordReset;
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* This route send the verification email to the user's email (contains a 6-digit verification code)
|
||||
* @param {*} email
|
||||
*/
|
||||
const sendVerificationEmail = (email: string) => {
|
||||
fetch("/api/v1/signup/email/signup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
export default sendVerificationEmail;
|
||||
@@ -1,16 +0,0 @@
|
||||
const token = async () =>
|
||||
fetch("/api/v1/auth/token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
credentials: "include"
|
||||
}).then(async (res) => {
|
||||
if (res.status === 200) {
|
||||
return (await res.json()).token;
|
||||
}
|
||||
console.log("Getting a new token failed");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default token;
|
||||
@@ -1,27 +0,0 @@
|
||||
interface Props {
|
||||
email: string;
|
||||
code: string;
|
||||
organizationId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route verifies the signup invite link
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email that a user is trying to verify
|
||||
* @param {string} obj.organizationId - id of organization that a user is trying to verify for
|
||||
* @param {string} obj.code - code that a user received to the abovementioned email
|
||||
* @returns
|
||||
*/
|
||||
const verifySignupInvite = ({ email, organizationId, code }: Props) => fetch("/api/v1/invite-org/verify", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
organizationId,
|
||||
code
|
||||
})
|
||||
});
|
||||
|
||||
export default verifySignupInvite;
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* This is the route that get an encrypted private key (will be decrypted with a backup key)
|
||||
* @param {object} obj
|
||||
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
|
||||
* @returns
|
||||
*/
|
||||
const getBackupEncryptedPrivateKey = ({
|
||||
verificationToken
|
||||
}: {
|
||||
verificationToken: string;
|
||||
}) => fetch("/api/v1/password/backup-private-key", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${ verificationToken}`
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res?.status !== 200) {
|
||||
console.log("Failed to get the backup key");
|
||||
}
|
||||
return (await res?.json())?.backupPrivateKey;
|
||||
});
|
||||
|
||||
export default getBackupEncryptedPrivateKey;
|
||||
@@ -1,8 +0,0 @@
|
||||
const publicKeyInfisical = () => fetch("/api/v1/key/publicKey/infisical", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
export default publicKeyInfisical;
|
||||
@@ -1,57 +0,0 @@
|
||||
interface Props {
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
verificationToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the route that resets the account password if all the previus steps were passed
|
||||
* @param {object} obj
|
||||
* @param {object} obj.verificationToken - this is the token that confirms that a user is the right one
|
||||
* @param {object} obj.encryptedPrivateKey - the new encrypted private key (encrypted using the new password)
|
||||
* @param {object} obj.iv
|
||||
* @param {object} obj.tag
|
||||
* @param {object} obj.salt
|
||||
* @param {object} obj.verifier
|
||||
* @returns
|
||||
*/
|
||||
const resetPasswordOnAccountRecovery = ({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
verificationToken,
|
||||
}: Props) => fetch("/api/v1/password/password-reset", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${verificationToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res?.status !== 200) {
|
||||
console.log("Failed to get the backup key");
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
export default resetPasswordOnAccountRecovery;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
/**
|
||||
* Verify MFA token [mfaToken] for user with email [email]
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of user
|
||||
* @param {string} obj.mfaToken - MFA cod/token to verify
|
||||
* @returns
|
||||
*/
|
||||
const verifyMfaToken = async ({
|
||||
email,
|
||||
mfaToken
|
||||
}: {
|
||||
email: string;
|
||||
mfaToken: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
|
||||
email,
|
||||
mfaToken
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default verifyMfaToken;
|
||||
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user