mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 21:28:59 +00:00
Fix merge conflicts
This commit is contained in:
@@ -1,385 +0,0 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
faGear,
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
import getOrganizationUsers from "@app/pages/api/organization/GetOrgUsers";
|
||||
import getUser from "@app/pages/api/user/getUser";
|
||||
import addUserToWorkspace from "@app/pages/api/workspace/addUserToWorkspace";
|
||||
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
|
||||
import getWorkspaces from "@app/pages/api/workspace/getWorkspaces";
|
||||
import uploadKeys from "@app/pages/api/workspace/uploadKeys";
|
||||
|
||||
import NavBarDashboard from "../navigation/NavBarDashboard";
|
||||
import onboardingCheck from "../utilities/checks/OnboardingCheck";
|
||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
||||
import { decryptAssymmetric, encryptAssymmetric } from "../utilities/cryptography/crypto";
|
||||
import Button from "./buttons/Button";
|
||||
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
|
||||
import Listbox from "./Listbox";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
// TODO: what to do about the fact that 2ids can have the same name
|
||||
|
||||
/**
|
||||
* When a user creates a new workspace, redirect them to the page of the new workspace.
|
||||
* @param {*} workspaceName
|
||||
*/
|
||||
const submitModal = async (workspaceName: string, addAllUsers: boolean) => {
|
||||
setLoading(true);
|
||||
// timeout code.
|
||||
setTimeout(() => setLoading(false), 1500);
|
||||
|
||||
try {
|
||||
const workspaces = await getWorkspaces();
|
||||
const currentWorkspaces = workspaces.map((workspace) => workspace.name);
|
||||
if (!currentWorkspaces.includes(workspaceName)) {
|
||||
const newWorkspace = await createWorkspace({
|
||||
workspaceName,
|
||||
organizationId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadKeys(newWorkspaceId, myUser._id, ciphertext, nonce);
|
||||
|
||||
if (addAllUsers) {
|
||||
console.log("adding other users");
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
orgUsers.map(async (user: any) => {
|
||||
if (user.status === "accepted" && user.email !== myUser.email) {
|
||||
const result = await addUserToWorkspace(user.user.email, newWorkspaceId);
|
||||
if (result?.invitee && result?.latestKey) {
|
||||
const TEMP_PRIVATE_KEY = tempLocalStorage("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: TEMP_PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: result.invitee.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(newWorkspaceId, result.invitee._id, inviteeCipherText, inviteeNonce);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setWorkspaceMapping((prevState) => ({
|
||||
...prevState,
|
||||
[workspaceName]: newWorkspaceId
|
||||
}));
|
||||
setWorkspaceSelected(workspaceName);
|
||||
setIsOpen(false);
|
||||
setNewWorkspaceName("");
|
||||
} else {
|
||||
console.error("A project with this name already exists.");
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.secrets"),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />
|
||||
},
|
||||
{
|
||||
href: `/users/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.members"),
|
||||
emoji: <FontAwesomeIcon icon={faUser} />
|
||||
},
|
||||
{
|
||||
href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />
|
||||
},
|
||||
{
|
||||
href: `/activity/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: "Audit Logs",
|
||||
emoji: <FontAwesomeIcon icon={faFileLines} />
|
||||
},
|
||||
{
|
||||
href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.project-settings"),
|
||||
emoji: <FontAwesomeIcon icon={faGear} />
|
||||
}
|
||||
],
|
||||
[t, workspaceMapping, workspaceSelected]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage("orgData.id") === "") {
|
||||
const userOrgs = await getOrganizations();
|
||||
localStorage.setItem("orgData.id", userOrgs[0]._id);
|
||||
}
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
(userWorkspaces.length === 0 &&
|
||||
router.asPath !== "/noprojects" &&
|
||||
!router.asPath.includes("home") &&
|
||||
!router.asPath.includes("settings")) ||
|
||||
router.asPath === "/dashboard/undefined"
|
||||
) {
|
||||
router.push("/noprojects");
|
||||
} else if (router.asPath !== "/noprojects") {
|
||||
const intendedWorkspaceId = router.asPath
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0];
|
||||
|
||||
if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
|
||||
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
}
|
||||
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
!["callback", "create", "authorize"].includes(intendedWorkspaceId) &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||
} else {
|
||||
setWorkspaceMapping(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
|
||||
) as any
|
||||
);
|
||||
setWorkspaceSelected(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
|
||||
)[router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
putUserInWorkSpace();
|
||||
onboardingCheck({ setTotalOnboardingActionsDone });
|
||||
}, [router.query.id]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (
|
||||
workspaceMapping[workspaceSelected as any] &&
|
||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||
router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]
|
||||
) {
|
||||
localStorage.setItem("projectData.id", `${workspaceMapping[workspaceSelected as any]}`);
|
||||
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, [workspaceSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-screen w-full flex-col overflow-x-hidden hidden md:flex">
|
||||
<NavBarDashboard />
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row dark">
|
||||
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
{/* <div className="py-6"></div> */}
|
||||
<div>
|
||||
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
|
||||
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.menu.project")}
|
||||
</div>
|
||||
{Object.keys(workspaceMapping).length > 0 ? (
|
||||
<Listbox
|
||||
isSelected={workspaceSelected}
|
||||
onChange={setWorkspaceSelected}
|
||||
data={Object.keys(workspaceMapping)}
|
||||
buttonAction={openModal}
|
||||
text=""
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text="Add Project"
|
||||
onButtonPressed={openModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ul>
|
||||
{Object.keys(workspaceMapping).length > 0 &&
|
||||
menuItems.map(({ href, title, emoji }) => (
|
||||
<li className="mx-2 mt-0.5" key={title}>
|
||||
{router.asPath.split("/")[1] === href.split("/")[1] &&
|
||||
(["project", "billing", "org", "personal"].includes(
|
||||
router.asPath.split("/")[2]
|
||||
)
|
||||
? router.asPath.split("/")[2] === href.split("/")[2]
|
||||
: true) ? (
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
) : router.asPath === "/noprojects" ? (
|
||||
<div className="flex rounded p-2.5 text-sm text-white">
|
||||
<p className="flex w-10 items-center justify-center text-lg">{emoji}</p>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<div className="flex cursor-pointer rounded p-2.5 text-sm text-white hover:bg-primary-50/5">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-40 mb-4 w-full px-2">
|
||||
{router.asPath.split("/")[1] === "home" ? (
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
|
||||
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<AddWorkspaceDialog
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
submitModal={submitModal}
|
||||
workspaceName={newWorkspaceName}
|
||||
setWorkspaceName={setNewWorkspaceName}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 z-[200] md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -1,208 +0,0 @@
|
||||
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 addAPIKey from "@app/pages/api/apiKey/addAPIKey";
|
||||
|
||||
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;
|
||||
// TODO: These never and any will be filled by single folder that contains types and hooks about the API
|
||||
apiKeys: any[];
|
||||
setApiKeys: (arg: any[]) => void;
|
||||
};
|
||||
|
||||
// TODO: convert to TS
|
||||
const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyName, setApiKeyName] = useState("");
|
||||
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day");
|
||||
const [apiKeyCopied, setApiKeyCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const generateAPIKey = async () => {
|
||||
const newApiKey = await addAPIKey({
|
||||
name: apiKeyName,
|
||||
expiresIn: expiryMapping[apiKeyExpiresIn as keyof typeof expiryMapping]
|
||||
});
|
||||
|
||||
setApiKeys([...apiKeys, newApiKey.apiKeyData]);
|
||||
setApiKey(newApiKey.apiKey);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
const copyText = document.getElementById("apiKey") 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);
|
||||
|
||||
setApiKeyCopied(true);
|
||||
setTimeout(() => setApiKeyCopied(false), 2000);
|
||||
// Alert the copied text
|
||||
// alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
const closeAddApiKeyModal = () => {
|
||||
closeModal();
|
||||
setApiKeyName("");
|
||||
setApiKey("");
|
||||
};
|
||||
|
||||
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"
|
||||
>
|
||||
{apiKey === "" ? (
|
||||
<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.api-key.add-dialog.title")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("section.api-key.add-dialog.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 max-h-28">
|
||||
<InputField
|
||||
label={t("section.api-key.add-dialog.name")}
|
||||
onChangeHandler={setApiKeyName}
|
||||
type="varName"
|
||||
value={apiKeyName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
<ListBox
|
||||
isSelected={apiKeyExpiresIn}
|
||||
onChange={setApiKeyExpiresIn}
|
||||
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={() => generateAPIKey()}
|
||||
color="mineshaft"
|
||||
text={t("section.api-key.add-dialog.add") as string}
|
||||
textDisabled={t("section.api-key.add-dialog.add") as string}
|
||||
size="md"
|
||||
active={apiKeyName !== ""}
|
||||
/>
|
||||
</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.api-key.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.api-key.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={apiKey}
|
||||
disabled
|
||||
id="apiKey"
|
||||
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">
|
||||
{apiKey}
|
||||
</div>
|
||||
<div className="group relative inline-block h-full font-normal text-gray-400 underline duration-200 hover:text-primary">
|
||||
<button
|
||||
type="button"
|
||||
onClick={copyToClipboard}
|
||||
className="h-full border-l border-white/20 py-2 pl-3.5 pr-4 duration-200 hover:bg-white/[0.12]"
|
||||
>
|
||||
{apiKeyCopied ? (
|
||||
<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={() => closeAddApiKeyModal()}
|
||||
color="mineshaft"
|
||||
text="Close"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
)}
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddApiKeyDialog;
|
||||
@@ -83,7 +83,7 @@ const AddProjectMemberDialog = ({
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md py-1 text-sm text-gray-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
onClick={() => router.push(`/org/${router.query.id}/members`)}
|
||||
aria-label="add member"
|
||||
/>,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
@@ -91,7 +91,7 @@ const AddProjectMemberDialog = ({
|
||||
type="button"
|
||||
className="ml-1 inline-flex justify-center rounded-md py-1 text-sm text-gray-500 hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() =>
|
||||
router.push(`/settings/org/${router.query.id}?invite`)
|
||||
router.push(`/org/${router.query.id}/members?action=invite`)
|
||||
}
|
||||
aria-label="add member"
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
|
||||
import { useOrganization } from "@app/context";
|
||||
|
||||
// REFACTOR: Move all these modals into one reusable one
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -15,6 +17,7 @@ const UpgradePlanModal = ({
|
||||
text,
|
||||
}:Props) => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -61,7 +64,7 @@ const UpgradePlanModal = ({
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex justify-center rounded-md border border-transparent bg-primary opacity-80 hover:opacity-100 px-4 py-2 text-sm font-medium text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2'
|
||||
onClick={() => router.push(`/settings/billing/${localStorage.getItem("projectData.id")}`)}
|
||||
onClick={() => router.push(`/org/${currentOrg?._id}/billing`)}
|
||||
>
|
||||
Upgrade Now
|
||||
</button>
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { faX } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
|
||||
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
|
||||
import guidGenerator from "../../utilities/randomId";
|
||||
import Button from "../buttons/Button";
|
||||
|
||||
interface TokenProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface ServiceTokensProps {
|
||||
data: TokenProps[];
|
||||
setApiKeys: (value: TokenProps[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the component that we utilize for the api key table
|
||||
* @param {object} obj
|
||||
* @param {any[]} obj.data - current state of the api key table
|
||||
* @param {function} obj.setApiKeys - updating the state of the api key table
|
||||
* @returns
|
||||
*/
|
||||
const ApiKeyTable = ({ data, setApiKeys }: 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">API KEY NAME</th>
|
||||
<th className="text-left pl-6 pt-2.5 pb-2">VALID 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">
|
||||
{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={() => {
|
||||
deleteAPIKey({ apiKeyId: row._id });
|
||||
setApiKeys(data.filter((token) => token._id !== row._id));
|
||||
createNotification({
|
||||
text: `'${row.name}' API key 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 API keys yet
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApiKeyTable;
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { plans } from "public/data/frequentConstants";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { Select, SelectItem } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission";
|
||||
import getOrganizationSubscriptions from "@app/pages/api/organization/GetOrgSubscription";
|
||||
import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace";
|
||||
import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace";
|
||||
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
|
||||
@@ -40,13 +39,12 @@ type EnvironmentProps = {
|
||||
* @returns
|
||||
*/
|
||||
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
const [roleSelected, setRoleSelected] = useState(
|
||||
Array(userData?.length).fill(userData.map((user) => user.role))
|
||||
);
|
||||
const host = window.location.origin;
|
||||
const router = useRouter();
|
||||
const [myRole, setMyRole] = useState("member");
|
||||
const [currentPlan, setCurrentPlan] = useState("");
|
||||
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
|
||||
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
|
||||
const { createNotification } = useNotificationContext();
|
||||
@@ -128,7 +126,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
denials = [];
|
||||
}
|
||||
|
||||
if (currentPlan !== plans.professional && host === "https://app.infisical.com" && workspaceId !== "63ea8121b6e2b0543ba79616") {
|
||||
if (subscription?.rbac === false) {
|
||||
setIsUpgradeModalOpen(true);
|
||||
} else {
|
||||
const allDenials = userData[index].deniedPermissions
|
||||
@@ -167,14 +165,6 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
(async () => {
|
||||
const result = await getProjectInfo({ projectId: workspaceId });
|
||||
setWorkspaceEnvs(result.environments);
|
||||
|
||||
const orgId = localStorage.getItem("orgData.id") as string;
|
||||
const subscriptions = await getOrganizationSubscriptions({
|
||||
orgId
|
||||
});
|
||||
if (subscriptions) {
|
||||
setCurrentPlan(subscriptions.data[0].plan.product);
|
||||
}
|
||||
})();
|
||||
}, [userData, myUser]);
|
||||
|
||||
@@ -208,11 +198,13 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
|
||||
return (
|
||||
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
|
||||
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
|
||||
<UpgradePlanModal
|
||||
isOpen={isUpgradeModalOpen}
|
||||
onClose={closeUpgradeModal}
|
||||
text="You can change user permissions if you switch to Infisical's Professional plan."
|
||||
/>
|
||||
{subscription && (
|
||||
<UpgradePlanModal
|
||||
isOpen={isUpgradeModalOpen}
|
||||
onClose={closeUpgradeModal}
|
||||
text={subscription.slug === null ? "You can use RBAC under an Enterprise license" : "You can use RBAC if you switch to Infisical's Team Plan."}
|
||||
/>
|
||||
)}
|
||||
<table className="my-0.5 w-full">
|
||||
<thead className="text-xs font-light text-gray-400 bg-mineshaft-800">
|
||||
<tr>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import StripeRedirect from "@app/pages/api/organization/StripeRedirect";
|
||||
|
||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
||||
|
||||
interface Props {
|
||||
plan: {
|
||||
name: string;
|
||||
price: string;
|
||||
priceExplanation?: string;
|
||||
text: string;
|
||||
subtext?: string;
|
||||
buttonTextMain: string;
|
||||
buttonTextSecondary: string;
|
||||
current: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Plan({ plan }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={`relative flex flex-col justify-between border min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
|
||||
plan.name !== "Starter" && plan.current === true ? "border-2 border-primary" : "border-mineshaft-600"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row justify-between items-center relative z-10">
|
||||
<p className="px-6 py-4 text-3xl font-semibold text-gray-400">{plan.name}</p>
|
||||
</div>
|
||||
<div className="flex flwx-row items-end justify-start mb-4">
|
||||
<p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
|
||||
<p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
|
||||
</div>
|
||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
|
||||
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
{plan.current === false ? (
|
||||
<>
|
||||
{plan.buttonTextMain === "Schedule a Demo" ? (
|
||||
<a href="/scheduledemo" target='_blank rel="noopener"'>
|
||||
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
|
||||
{plan.buttonTextMain}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<div
|
||||
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 text-gray-400 font-semibold ${
|
||||
plan.buttonTextMain === "Downgrade"
|
||||
? "hover:bg-red hover:text-white hover:border-red"
|
||||
: "hover:bg-primary hover:text-black hover:border-primary"
|
||||
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
StripeRedirect({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
})
|
||||
}
|
||||
>
|
||||
{plan.buttonTextMain}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<a href="https://infisical.com/pricing" target='_blank rel="noopener"'>
|
||||
<div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
|
||||
{plan.buttonTextSecondary}
|
||||
</div>
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
|
||||
plan.name !== "Starter" && plan.current === true ? "bg-primary" : "bg-mineshaft-400"
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs text-black font-semibold">CURRENT PLAN</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,7 +50,7 @@ const AddTagsMenu = ({ allTags, currentTags, modifyTags, id }: { allTags: Tag[];
|
||||
<button
|
||||
type="button"
|
||||
className='w-full text-left bg-mineshaft-800 hover:bg-primary hover:text-black duration-200 px-2 py-0.5 text-bunker-200 rounded-sm'
|
||||
onClick={() => router.push(`/settings/project/${String(router.query.id)}`)}
|
||||
onClick={() => router.push(`/project/${String(router.query.id)}/settings`)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-2 text-xs" />Add more tags
|
||||
</button>
|
||||
|
||||
@@ -182,7 +182,7 @@ const DropZone = ({
|
||||
|
||||
return loading ? (
|
||||
<div className="mb-16 flex items-center justify-center pt-16">
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="google logo" />
|
||||
<Image src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||
</div>
|
||||
) : keysExist ? (
|
||||
<div
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import axios from "axios"
|
||||
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import Error from "../basic/Error";
|
||||
import attemptCliLogin from "../utilities/attemptCliLogin";
|
||||
@@ -31,7 +32,8 @@ export default function InitialLoginStep({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
@@ -84,9 +86,11 @@ export default function InitialLoginStep({
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +102,7 @@ export default function InitialLoginStep({
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
|
||||
return <form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
|
||||
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
|
||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||
<Button
|
||||
@@ -143,7 +147,7 @@ export default function InitialLoginStep({
|
||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
onClick={async () => handleLogin()}
|
||||
type="submit"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
@@ -180,5 +184,5 @@ export default function InitialLoginStep({
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
|
||||
import { Button, Input } from "../v2";
|
||||
|
||||
/**
|
||||
* 1st step of login - user enters their username and password
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.email - email of user
|
||||
* @param {Function} obj.setEmail - function to set the email of user
|
||||
* @param {String} obj.password - password of user
|
||||
* @param {String} obj.setPassword - function to set the password of user
|
||||
* @param {Function} obj.setStep - function to set the login flow step
|
||||
* @returns
|
||||
*/
|
||||
export default function LoginStep({
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
setStep
|
||||
}: {
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
setStep: (step: number) => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const isLoginSuccessful = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||
// case: login was successful
|
||||
|
||||
if (isLoginSuccessful.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="w-full mx-auto h-full px-6">
|
||||
<p className="text-xl w-max mx-auto flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 mb-6">
|
||||
{t("login.login")}
|
||||
</p>
|
||||
<div className="flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
type="email"
|
||||
placeholder="Enter your email..."
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="password"
|
||||
placeholder="Enter your password..."
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] px-2 mt-4 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
onClick={async () => handleLogin()}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
> {String(t("login.login"))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-bunker-400 text-sm flex flex-row w-max mx-auto">
|
||||
<Link href="/verify-email">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
{t("common.maintenance-alert")}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import axios from "axios"
|
||||
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"
|
||||
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
||||
import { useSendMfaToken } from "@app/hooks/api/auth";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import Error from "../basic/Error";
|
||||
import { Button } from "../v2";
|
||||
@@ -101,7 +102,7 @@ export default function MFAStep({
|
||||
// cli page
|
||||
router.push("/cli-redirect");
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
const isLoginSuccessful = await attemptLoginMfa({
|
||||
email,
|
||||
password,
|
||||
@@ -111,7 +112,11 @@ export default function MFAStep({
|
||||
|
||||
if (isLoginSuccessful) {
|
||||
setIsLoading(false);
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +185,7 @@ export default function MFAStep({
|
||||
className='h-14'
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
isLoading={isLoading}
|
||||
> {String(t("mfa.verify"))} </Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,7 +193,7 @@ export default function MFAStep({
|
||||
<div className="flex flex-row items-baseline gap-1 text-sm">
|
||||
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
|
||||
<div className="mt-2 text-bunker-400 text-md flex flex-row">
|
||||
<button disabled={isLoading} onClick={handleResendMfaCode} type="button">
|
||||
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
|
||||
{isLoadingResend
|
||||
? t("signup.step2-resend-progress")
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
import SecurityClient from "../utilities/SecurityClient";
|
||||
import { Button, Input } from "../v2";
|
||||
@@ -50,7 +51,9 @@ export default function PasswordInputStep({
|
||||
}
|
||||
|
||||
// case: login does not require MFA step
|
||||
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
const userOrg = userOrgs[0]._id;
|
||||
router.push(`/org/${userOrg?._id}/overview`);
|
||||
}
|
||||
} catch (err) {
|
||||
setLoginError(true);
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable react/jsx-key */
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faAngleDown,
|
||||
faBook,
|
||||
faCoins,
|
||||
faEnvelope,
|
||||
faGear,
|
||||
faPlus,
|
||||
faRightFromBracket
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
import logout from "@app/pages/api/auth/Logout";
|
||||
|
||||
import getOrganization from "../../pages/api/organization/GetOrg";
|
||||
import getOrganizations from "../../pages/api/organization/getOrgs";
|
||||
import getUser from "../../pages/api/user/getUser";
|
||||
import guidGenerator from "../utilities/randomId";
|
||||
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faSlack} />,
|
||||
t("nav.support.slack"),
|
||||
"https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faBook} />,
|
||||
t("nav.support.docs"),
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faGithub} />,
|
||||
t("nav.support.issue"),
|
||||
"https://github.com/Infisical/infisical-cli/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faEnvelope} />,
|
||||
t("nav.support.email"),
|
||||
"mailto:[email protected]"
|
||||
]
|
||||
];
|
||||
|
||||
export interface ICurrentOrg {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface IUser {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the navigation bar in the main app.
|
||||
* It has two main components: support options and user menu (inlcudes billing, logout, org/user settings)
|
||||
* @returns NavBar
|
||||
*/
|
||||
export default function Navbar() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<IUser | undefined>();
|
||||
const [orgs, setOrgs] = useState([]);
|
||||
const [currentOrg, setCurrentOrg] = useState<ICurrentOrg | undefined>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const supportOptionsList = useMemo(() => supportOptions(t), [t]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const userData = await getUser();
|
||||
setUser(userData);
|
||||
const orgsData = await getOrganizations();
|
||||
setOrgs(orgsData);
|
||||
const currentUserOrg = await getOrganization({
|
||||
orgId: String(localStorage.getItem("orgData.id"))
|
||||
});
|
||||
setCurrentOrg(currentUserOrg);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const closeApp = async () => {
|
||||
await logout();
|
||||
router.push("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-between w-full bg-bunker text-white border-b border-mineshaft-500 z-[71]">
|
||||
<div className="m-auto flex justify-start items-center mx-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
|
||||
</div>
|
||||
<a href="#" className="text-2xl text-white font-semibold mx-2">
|
||||
Infisical
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex justify-start items-center mx-2 z-40">
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-200 hover:bg-white/10 px-3 rounded-md duration-200 text-sm mr-4 py-2 flex items-center"
|
||||
>
|
||||
<FontAwesomeIcon icon={faBook} className="text-xl mr-2" />
|
||||
Docs
|
||||
</a>
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div className="mr-4">
|
||||
<Menu.Button className="inline-flex w-full justify-center px-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
<FontAwesomeIcon className="text-xl" icon={faCircleQuestion} />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-20 ring-opacity-5 focus:outline-none px-2 py-1.5">
|
||||
{supportOptionsList.map(([icon, text, url]) => (
|
||||
<a
|
||||
key={guidGenerator()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="font-normal text-gray-300 duration-200 rounded-md w-full flex items-center py-0.5"
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/10 duration-200 hover:text-gray-200 w-full">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
<Menu as="div" className="relative inline-block text-left mr-4">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex w-full justify-center pr-2 pl-2 py-2 text-sm font-medium text-gray-200 rounded-md hover:bg-white/10 duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75">
|
||||
{user?.firstName} {user?.lastName}
|
||||
<FontAwesomeIcon
|
||||
icon={faAngleDown}
|
||||
className="ml-2 mt-1 text-sm text-gray-300 hover:text-lime-100"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute right-0 mt-0.5 w-64 origin-top-right divide-y divide-gray-700 rounded-md bg-bunker border border-mineshaft-700 shadow-lg ring-1 ring-black z-[999] ring-opacity-5 focus:outline-none">
|
||||
<div className="px-1 py-1 z-[100]">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav.user.signed-in-as")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
|
||||
className="flex flex-row items-center px-1 mx-1 my-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-8 w-9 rounded-full flex items-center justify-center text-gray-300">
|
||||
{user?.firstName?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<p className="text-gray-300 px-2 pt-1 text-sm">
|
||||
{" "}
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="text-gray-400 px-2 pb-1 text-xs"> {user?.email}</p>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="text-lg text-gray-400 p-2 mr-1 rounded-md cursor-pointer hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 pt-2">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav.user.current-organization")}
|
||||
</div>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}`)}
|
||||
className="flex flex-row items-center px-2 mt-2 py-1 hover:bg-white/5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<p className="text-gray-300 px-2 text-sm">{currentOrg?.name}</p>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
className="text-lg text-gray-400 p-2 rounded-md cursor-pointer hover:bg-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
// onClick={buttonAction}
|
||||
type="button"
|
||||
className="cursor-pointer w-full"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/billing/${router.query.id}`)}
|
||||
className="mt-1 relative flex justify-start cursor-pointer select-none py-2 px-2 rounded-md text-gray-400 hover:bg-white/5 duration-200 hover:text-gray-200"
|
||||
>
|
||||
<FontAwesomeIcon className="text-lg pl-1.5 pr-3" icon={faCoins} />
|
||||
<div className="text-sm">{t("nav.user.usage-billing")}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
// onClick={buttonAction}
|
||||
className="cursor-pointer w-full mb-2"
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/org/${router.query.id}?invite`)}
|
||||
className="relative flex justify-start cursor-pointer select-none py-2 pl-10 pr-4 rounded-md text-gray-400 hover:bg-primary/100 duration-200 hover:text-black hover:font-semibold mt-1"
|
||||
>
|
||||
<span className="rounded-lg absolute inset-y-0 left-0 flex items-center pl-3 pr-4">
|
||||
<FontAwesomeIcon icon={faPlus} className="ml-1" />
|
||||
</span>
|
||||
<div className="text-sm ml-1">{t("nav.user.invite")}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{orgs?.length > 1 && (
|
||||
<div className="px-1 pt-1">
|
||||
<div className="text-gray-400 self-start ml-2 mt-2 text-xs font-semibold tracking-wide">
|
||||
{t("nav.user.other-organizations")}
|
||||
</div>
|
||||
<div className="flex flex-col items-start px-1 mt-3 mb-2">
|
||||
{orgs
|
||||
.filter(
|
||||
(org: { _id: string }) => org._id !== localStorage.getItem("orgData.id")
|
||||
)
|
||||
.map((org: { _id: string; name: string }) => (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
key={guidGenerator()}
|
||||
onClick={() => {
|
||||
localStorage.setItem("orgData.id", org._id);
|
||||
router.reload();
|
||||
}}
|
||||
className="flex flex-row justify-start items-center hover:bg-white/5 w-full p-1.5 cursor-pointer rounded-md"
|
||||
>
|
||||
<div className="bg-white/10 h-7 w-8 rounded-md flex items-center justify-center text-gray-300">
|
||||
{org.name.charAt(0)}
|
||||
</div>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<p className="text-gray-300 px-2 text-sm">{org.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="px-1 py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeApp}
|
||||
className={`${
|
||||
active ? "bg-red font-semibold text-white" : "text-gray-400"
|
||||
} group flex w-full items-center rounded-md px-2 py-2 text-sm`}
|
||||
>
|
||||
<div className="relative flex justify-start items-center cursor-pointer select-none">
|
||||
<FontAwesomeIcon
|
||||
className="text-lg ml-1.5 mr-3"
|
||||
icon={faRightFromBracket}
|
||||
/>
|
||||
{t("common.logout")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -57,11 +57,17 @@ export default function NavHeader({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ml-4 flex flex-row items-center pt-6">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
<div className="flex flex-row items-center pt-6">
|
||||
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-black text-sm">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-bunker-300">{currentOrg?.name}</div>
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={`/org/${currentOrg?._id}/overview`}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary pl-0.5">{currentOrg?.name}</a>
|
||||
</Link>
|
||||
{isProjectRelated && (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
|
||||
@@ -79,7 +85,7 @@ export default function NavHeader({
|
||||
<Link
|
||||
passHref
|
||||
legacyBehavior
|
||||
href={{ pathname: "/dashboard/[id]", query: { id: router.query.id } }}
|
||||
href={{ pathname: "/project/[id]/secrets", query: { id: router.query.id } }}
|
||||
>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
|
||||
</Link>
|
||||
@@ -120,7 +126,7 @@ export default function NavHeader({
|
||||
{index + 1 === folders?.length ? (
|
||||
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
||||
) : (
|
||||
<Link passHref legacyBehavior href={{ pathname: "/dashboard/[id]", query }}>
|
||||
<Link passHref legacyBehavior href={{ pathname: "/project/[id]/secrets", query }}>
|
||||
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
|
||||
{name === "root" ? selectedEnv?.name : name}
|
||||
</a>
|
||||
|
||||
@@ -114,6 +114,7 @@ export default function CodeInputStep({
|
||||
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={incrementStep}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
|
||||
@@ -70,6 +70,7 @@ export default function EnterEmailStep({
|
||||
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={emailCheck}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useRouter } from "next/router";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
import addUserToOrg from "@app/pages/api/organization/addUserToOrg";
|
||||
import getWorkspaces from "@app/pages/api/workspace/getWorkspaces";
|
||||
|
||||
import { Button, EmailServiceSetupModal } from "../v2";
|
||||
|
||||
@@ -21,9 +20,7 @@ export default function TeamInviteStep(): JSX.Element {
|
||||
|
||||
// Redirect user to the getting started page
|
||||
const redirectToHome = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
const userWorkspace = userWorkspaces[0]._id;
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
|
||||
};
|
||||
|
||||
const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
|
||||
|
||||
@@ -248,7 +248,6 @@ export default function UserInfoStep({
|
||||
placeholder=""
|
||||
onChange={(e) => setAttributionSource(e.target.value)}
|
||||
value={attributionSource}
|
||||
isRequired
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
@@ -301,6 +300,7 @@ export default function UserInfoStep({
|
||||
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
|
||||
<div className="text-l py-1 text-lg w-full">
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={signupErrorCheck}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/* 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 Aes256Gcm from "./cryptography/aes-256-gcm";
|
||||
import { deriveArgonKey } from "./cryptography/crypto";
|
||||
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
|
||||
|
||||
const clientOldPassword = new jsrp.client();
|
||||
const clientNewPassword = new jsrp.client();
|
||||
|
||||
type Params = {
|
||||
email: string;
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
|
||||
let serverPublicKey; let salt;
|
||||
|
||||
try {
|
||||
const clientPublicKey = clientOldPassword.getPublicKey();
|
||||
|
||||
const res = await SRP1({ clientPublicKey });
|
||||
|
||||
serverPublicKey = res.serverPublicKey;
|
||||
salt = res.salt;
|
||||
|
||||
clientOldPassword.setSalt(salt);
|
||||
clientOldPassword.setServerPublicKey(serverPublicKey);
|
||||
|
||||
const clientProof = clientOldPassword.getProof();
|
||||
|
||||
clientNewPassword.init({ username: email, password: newPassword }, async () => {
|
||||
clientNewPassword.createVerifier(async (err, result) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: localStorage.getItem("PRIVATE_KEY") as string,
|
||||
secret: key
|
||||
});
|
||||
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString("hex"),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
await changePassword2({
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier
|
||||
});
|
||||
|
||||
saveTokenToLocalStorage({
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
});
|
||||
|
||||
resolve();
|
||||
} catch (err2) {
|
||||
console.error(err2);
|
||||
reject(err2);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default attemptChangePassword;
|
||||
@@ -5,7 +5,6 @@ interface OnboardingCheckProps {
|
||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||
setHasUserClickedSlack?: (value: boolean) => void;
|
||||
setHasUserClickedIntro?: (value: boolean) => void;
|
||||
setHasUserStarred?: (value: boolean) => void;
|
||||
setHasUserPushedSecrets?: (value: boolean) => void;
|
||||
setUsersInOrg?: (value: boolean) => void;
|
||||
}
|
||||
@@ -17,7 +16,6 @@ const onboardingCheck = async ({
|
||||
setTotalOnboardingActionsDone,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserClickedIntro,
|
||||
setHasUserStarred,
|
||||
setHasUserPushedSecrets,
|
||||
setUsersInOrg
|
||||
}: OnboardingCheckProps) => {
|
||||
@@ -46,14 +44,6 @@ const onboardingCheck = async ({
|
||||
}
|
||||
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||
|
||||
const userActionStar = await checkUserAction({
|
||||
action: "star_cta_clicked"
|
||||
});
|
||||
if (userActionStar) {
|
||||
countActions += 1;
|
||||
}
|
||||
if (setHasUserStarred) setHasUserStarred(!!userActionStar);
|
||||
|
||||
const orgId = localStorage.getItem("orgData.id");
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: orgId || ""
|
||||
|
||||
@@ -54,7 +54,6 @@ export const load = () => {
|
||||
|
||||
// Initializes Intercom
|
||||
export const boot = (options = {}) => {
|
||||
console.log("boot", { app_id: APP_ID, ...options })
|
||||
window &&
|
||||
window.Intercom &&
|
||||
window.Intercom("boot", { app_id: APP_ID, ...options });
|
||||
|
||||
@@ -61,7 +61,8 @@ const buttonVariants = cva(
|
||||
{
|
||||
colorSchema: "primary",
|
||||
variant: "star",
|
||||
className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100"
|
||||
className:
|
||||
"bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100"
|
||||
},
|
||||
{
|
||||
colorSchema: "primary",
|
||||
@@ -76,12 +77,14 @@ const buttonVariants = cva(
|
||||
{
|
||||
colorSchema: "primary",
|
||||
variant: "outline_bg",
|
||||
className: "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200"
|
||||
className:
|
||||
"bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200"
|
||||
},
|
||||
{
|
||||
colorSchema: "secondary",
|
||||
variant: "star",
|
||||
className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white"
|
||||
className:
|
||||
"bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white"
|
||||
},
|
||||
{
|
||||
colorSchema: "danger",
|
||||
@@ -163,13 +166,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
type="button"
|
||||
className={twMerge(
|
||||
buttonVariants({
|
||||
className,
|
||||
colorSchema,
|
||||
size,
|
||||
variant,
|
||||
isRounded,
|
||||
isDisabled,
|
||||
isFullWidth
|
||||
isFullWidth,
|
||||
className
|
||||
})
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
@@ -193,7 +196,15 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
<span className={twMerge("transition-all", isFullWidth ? "w-full" : "w-min", loadingToggleClass)}>{children}</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
"transition-all",
|
||||
isFullWidth ? "w-full" : "w-min",
|
||||
loadingToggleClass
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
|
||||
@@ -21,7 +21,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(
|
||||
"min-w-[220px] bg-bunker will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
"min-w-[220px] z-30 bg-mineshaft-900 border border-mineshaft-600 will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -62,11 +62,11 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
<DropdownMenuPrimitive.Item
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"text-sm block font-inter px-4 py-2 data-[highlighted]:bg-gray-700 outline-none cursor-pointer",
|
||||
"text-xs text-mineshaft-200 block font-inter px-4 py-2 data-[highlighted]:bg-mineshaft-700 rounded-sm outline-none cursor-pointer",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Item type="button" role="menuitem" class="flex w-full items-center" ref={inputRef}>
|
||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||
{icon && <span className="flex items-center mr-2">{icon}</span>}
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const EmptyState = ({
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} />
|
||||
<div className="flex flex-row items-center py-4">
|
||||
<div className="text-sm text-bunker-300">{title}</div>
|
||||
<div>{children}</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const inputVariants = cva(
|
||||
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-[0.05rem] hover:ring-bunker-400/60 duration-100",
|
||||
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
|
||||
@@ -48,7 +48,7 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
>
|
||||
<li
|
||||
className={twMerge(
|
||||
"group px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
|
||||
"group px-1 py-2 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
|
||||
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
||||
isDisabled && "hover:bg-transparent cursor-not-allowed",
|
||||
className
|
||||
@@ -56,16 +56,16 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
>
|
||||
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm">
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} -left-[0.28rem] absolute w-[0.07rem] rounded-md h-5 bg-primary`}/>
|
||||
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
style={{ width: 24, height: 24 }}
|
||||
style={{ width: 22, height: 22 }}
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
animationData={require(`../../../../public/lotties/${icon}.json`)}
|
||||
loop={false}
|
||||
autoplay={false}
|
||||
className="my-auto ml-3 mr-3"
|
||||
className="my-auto ml-[0.1rem] mr-3"
|
||||
/>
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
@@ -76,6 +76,53 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
)
|
||||
};
|
||||
|
||||
export const SubMenuItem = <T extends ElementType = "button">({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
isDisabled,
|
||||
isSelected,
|
||||
as: Item = "button",
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||
const iconRef = useRef()
|
||||
|
||||
return(
|
||||
<a
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
>
|
||||
<li
|
||||
className={twMerge(
|
||||
"group px-1 py-1 mt-0.5 font-inter flex flex-col text-sm text-mineshaft-300 hover:text-mineshaft-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
|
||||
isDisabled && "hover:bg-transparent cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm pl-6">
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
style={{ width: 16, height: 16 }}
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
animationData={require(`../../../../public/lotties/${icon}.json`)}
|
||||
loop={false}
|
||||
autoplay={false}
|
||||
className="my-auto ml-[0.1rem] mr-3"
|
||||
/>
|
||||
<span className="flex-grow text-left text-sm">{children}</span>
|
||||
</Item>
|
||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||
</motion.span>
|
||||
</li>
|
||||
</a>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
MenuItem.displayName = "MenuItem";
|
||||
|
||||
export type MenuGroupProps = {
|
||||
|
||||
@@ -72,12 +72,12 @@ export const TabsObject = () => {
|
||||
>
|
||||
Windows
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
{/* <Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab3"
|
||||
>
|
||||
Arch Linux
|
||||
</Tabs.Trigger>
|
||||
</Tabs.Trigger> */}
|
||||
<a
|
||||
target='_blank'
|
||||
rel="noopener noreferrer"
|
||||
@@ -88,7 +88,7 @@ export const TabsObject = () => {
|
||||
</a>
|
||||
</Tabs.List>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
value="tab1"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCode" />
|
||||
@@ -105,10 +105,10 @@ export const TabsObject = () => {
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none"
|
||||
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none"
|
||||
value="tab2"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeW" />
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="scoop bucket add org https://github.com/Infisical/scoop-infisical.git" id="downloadCodeW" />
|
||||
<div className='font-mono text-sm px-3 py-2 mt-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
|
||||
<input disabled value="scoop install infisical" id="downloadCodeW2" className='w-full bg-transparent text-bunker-200'/>
|
||||
<button
|
||||
@@ -138,22 +138,5 @@ export const TabsObject = () => {
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
value="tab3"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeL" />
|
||||
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeL" />
|
||||
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeL" />
|
||||
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeL" />
|
||||
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
|
||||
className='text-primary underline underline-offset-2'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://infisical.com/docs/integrations/overview'
|
||||
>
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { ReactNode } from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type TooltipProps = {
|
||||
export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
|
||||
children: ReactNode;
|
||||
content?: ReactNode;
|
||||
isOpen?: boolean;
|
||||
@@ -10,7 +10,7 @@ export type TooltipProps = {
|
||||
asChild?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
defaultOpen?: boolean;
|
||||
} & Omit<TooltipPrimitive.TooltipContentProps, "open">;
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { useSubscription } from "@app/context";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useGetOrgTrialUrl
|
||||
} from "@app/hooks/api";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Modal, ModalClose, ModalContent } from "../Modal";
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -13,32 +14,61 @@ type Props = {
|
||||
|
||||
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
||||
const link = (subscription && subscription.slug !== null)
|
||||
? `/settings/billing/${localStorage.getItem("projectData.id") as string}`
|
||||
? `/org/${currentOrg?._id}/billing`
|
||||
: "https://infisical.com/scheduledemo";
|
||||
|
||||
const handleUpgradeBtnClick = async () => {
|
||||
try {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
if (!subscription.has_used_trial) {
|
||||
// direct user to start pro trial
|
||||
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// direct user to upgrade their plan
|
||||
window.location.href = link;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Unleash Infisical's Full Power"
|
||||
footerContent={[
|
||||
<Link
|
||||
href={link}
|
||||
key="upgrade-plan"
|
||||
>
|
||||
<Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button>
|
||||
</Link>,
|
||||
<ModalClose asChild key="upgrade-plan-cancel">
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
]}
|
||||
>
|
||||
<p className="mb-2 text-bunker-300">{text}</p>
|
||||
<p className="text-bunker-300">
|
||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
||||
</p>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
colorSchema="primary"
|
||||
onClick={handleUpgradeBtnClick}
|
||||
className="mr-4"
|
||||
>
|
||||
{(subscription && !subscription.has_used_trial) ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => onOpenChange && onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ import { createContext, ReactNode, useContext, useMemo } from "react";
|
||||
import { useGetOrganization } from "@app/hooks/api";
|
||||
import { Organization } from "@app/hooks/api/types";
|
||||
|
||||
import { useWorkspace } from "../WorkspaceContext";
|
||||
|
||||
type TOrgContext = {
|
||||
orgs?: Organization[];
|
||||
@@ -18,10 +17,10 @@ type Props = {
|
||||
};
|
||||
|
||||
export const OrgProvider = ({ children }: Props): JSX.Element => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: userOrgs, isLoading } = useGetOrganization();
|
||||
|
||||
const currentWsOrgID = currentWorkspace?.organization;
|
||||
// const currentWsOrgID = currentWorkspace?.organization;
|
||||
const currentWsOrgID = localStorage.getItem("orgData.id");
|
||||
|
||||
// memorize the workspace details for the context
|
||||
const value = useMemo<TOrgContext>(
|
||||
|
||||
@@ -3,8 +3,7 @@ import { createContext, ReactNode, useContext, useMemo } from "react";
|
||||
import { useGetOrgSubscription } from "@app/hooks/api";
|
||||
import { SubscriptionPlan } from "@app/hooks/api/types";
|
||||
|
||||
import { useWorkspace } from "../WorkspaceContext";
|
||||
// import { Subscription } from '@app/hooks/api/workspace/types';
|
||||
import { useOrganization } from "../OrganizationContext";
|
||||
|
||||
type TSubscriptionContext = {
|
||||
subscription?: SubscriptionPlan;
|
||||
@@ -18,9 +17,10 @@ type Props = {
|
||||
};
|
||||
|
||||
export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { data, isLoading } = useGetOrgSubscription({
|
||||
orgID: currentWorkspace?.organization || ""
|
||||
orgID: currentOrg?._id || ""
|
||||
});
|
||||
|
||||
// memorize the workspace details for the context
|
||||
|
||||
@@ -129,7 +129,7 @@ const ActivitySideBar = ({ toggleSidebar, currentAction }: SideBarProps) => {
|
||||
<div
|
||||
className={`absolute border-l border-mineshaft-500 ${
|
||||
isLoading ? "bg-bunker-800" : "bg-bunker"
|
||||
} fixed top-14 right-0 z-40 flex h-[calc(100vh-56px)] w-96 flex-col justify-between shadow-xl`}
|
||||
} fixed right-0 z-40 flex h-[calc(100vh)] w-96 flex-col justify-between shadow-xl`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="mb-8 flex h-full items-center justify-center">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export {
|
||||
useGetAuthToken,
|
||||
useGetCommonPasswords,
|
||||
useRevokeAllSessions,
|
||||
useSendMfaToken,
|
||||
useSendMfaToken,
|
||||
useVerifyMfaToken
|
||||
} from "./queries";
|
||||
} from "./queries"
|
||||
|
||||
@@ -51,15 +51,6 @@ export const useGetAuthToken = () =>
|
||||
retry: 0
|
||||
});
|
||||
|
||||
export const useRevokeAllSessions = () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiRequest.delete("/api/v1/auth/sessions");
|
||||
return data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fetchCommonPasswords = async () => {
|
||||
const { data } = await apiRequest.get("/api/v1/auth/common-passwords");
|
||||
return data || [];
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./secretFolders";
|
||||
export * from "./secretImports";
|
||||
export * from "./secrets";
|
||||
export * from "./secretSnapshots";
|
||||
export * from "./serviceAccounts";
|
||||
@@ -13,4 +14,5 @@ export * from "./serviceTokens";
|
||||
export * from "./subscriptions";
|
||||
export * from "./tags";
|
||||
export * from "./users";
|
||||
export * from "./webhooks";
|
||||
export * from "./workspace";
|
||||
|
||||
@@ -12,5 +12,7 @@ export {
|
||||
useGetOrgPlanTable,
|
||||
useGetOrgPmtMethods,
|
||||
useGetOrgTaxIds,
|
||||
useGetOrgTrialUrl,
|
||||
useRenameOrg,
|
||||
useUpdateOrgBillingDetails} from "./queries";
|
||||
useUpdateOrgBillingDetails
|
||||
} from "./queries";
|
||||
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
PlanBillingInfo,
|
||||
PmtMethod,
|
||||
ProductsTable,
|
||||
RenameOrgDTO,
|
||||
TaxID} from "./types";
|
||||
RenameOrgDTO,
|
||||
TaxID
|
||||
} from "./types";
|
||||
|
||||
const organizationKeys = {
|
||||
getUserOrganization: ["organization"] as const,
|
||||
@@ -47,6 +48,24 @@ export const useRenameOrg = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetOrgTrialUrl = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
orgId,
|
||||
success_url
|
||||
}: {
|
||||
orgId: string;
|
||||
success_url: string;
|
||||
}) => {
|
||||
const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${orgId}/session/trial`, {
|
||||
success_url
|
||||
})
|
||||
|
||||
return url;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetOrgPlanBillingInfo = (organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
|
||||
export { useGetImportedSecrets, useGetSecretImports } from "./queries";
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretImportKeys } from "./queries";
|
||||
import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TUpdateSecretImportDTO } from "./types";
|
||||
|
||||
export const useCreateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateSecretImportDTO>({
|
||||
mutationFn: async ({ secretImport, environment, workspaceId, folderId }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-imports", {
|
||||
secretImport,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretImportDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, folderId, secretImports, id }) => {
|
||||
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
|
||||
secretImports,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretImportDTO>({
|
||||
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
|
||||
data: {
|
||||
secretImportPath,
|
||||
secretImportEnv
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGetImportedSecrets, TImportedSecrets, TSecretImports } from "./types";
|
||||
|
||||
export const secretImportKeys = {
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretImportSecrets: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-import-sec"
|
||||
]
|
||||
};
|
||||
|
||||
const fetchSecretImport = async (workspaceId: string, environment: string, folderId?: string) => {
|
||||
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
|
||||
"/api/v1/secret-imports",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secretImport;
|
||||
};
|
||||
|
||||
export const useGetSecretImports = (workspaceId: string, env: string, folderId?: string) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(env),
|
||||
queryKey: secretImportKeys.getProjectSecretImports(workspaceId, env, folderId),
|
||||
queryFn: () => fetchSecretImport(workspaceId, env, folderId)
|
||||
});
|
||||
|
||||
const fetchImportedSecrets = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
folderId?: string
|
||||
) => {
|
||||
const { data } = await apiRequest.get<{ secrets: TImportedSecrets }>(
|
||||
"/api/v1/secret-imports/secrets",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secrets;
|
||||
};
|
||||
|
||||
export const useGetImportedSecrets = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
decryptFileKey
|
||||
}: TGetImportedSecrets) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && Boolean(decryptFileKey),
|
||||
queryKey: secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId),
|
||||
queryFn: () => fetchImportedSecrets(workspaceId, environment, folderId),
|
||||
select: useCallback(
|
||||
(data: TImportedSecrets) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
return data.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
folderId: el.folderId,
|
||||
secrets: el.secrets.map((encSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
return {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
[decryptFileKey]
|
||||
)
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { UserWsKeyPair } from "../types";
|
||||
|
||||
export type TSecretImports = {
|
||||
_id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
imports: Array<{ environment: string; secretPath: string }>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TImportedSecrets = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: EncryptedSecret[];
|
||||
}[];
|
||||
|
||||
export type TGetImportedSecrets = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export type TCreateSecretImportDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImport: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImports: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TDeleteSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImportPath: string;
|
||||
secretImportEnv: string;
|
||||
};
|
||||
@@ -24,6 +24,10 @@ export const secretKeys = {
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets"
|
||||
],
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
|
||||
};
|
||||
|
||||
@@ -81,76 +85,79 @@ export const useGetProjectSecrets = ({
|
||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||
select: useCallback((data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const sharedSecrets: DecryptedSecret[] = [];
|
||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||
// this used for add-only mode in dashboard
|
||||
// type won't be there thus only one key is shown
|
||||
const duplicateSecretKey: Record<string, boolean> = {};
|
||||
data.forEach((encSecret: EncryptedSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
select: useCallback(
|
||||
(data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
const sharedSecrets: DecryptedSecret[] = [];
|
||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||
// this used for add-only mode in dashboard
|
||||
// type won't be there thus only one key is shown
|
||||
const duplicateSecretKey: Record<string, boolean> = {};
|
||||
data.forEach((encSecret: EncryptedSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const decryptedSecret = {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
if (encSecret.type === "personal") {
|
||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||
id: encSecret._id,
|
||||
value: secretValue
|
||||
const decryptedSecret = {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
} else {
|
||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||
sharedSecrets.push(decryptedSecret);
|
||||
|
||||
if (encSecret.type === "personal") {
|
||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||
id: encSecret._id,
|
||||
value: secretValue
|
||||
};
|
||||
} else {
|
||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||
sharedSecrets.push(decryptedSecret);
|
||||
}
|
||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||
}
|
||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||
}
|
||||
});
|
||||
sharedSecrets.forEach((val) => {
|
||||
const dupKey = `${val.key}-${val.env}`;
|
||||
if (personalSecrets?.[dupKey]) {
|
||||
val.idOverride = personalSecrets[dupKey].id;
|
||||
val.valueOverride = personalSecrets[dupKey].value;
|
||||
val.overrideAction = "modified";
|
||||
}
|
||||
});
|
||||
return { secrets: sharedSecrets };
|
||||
}, [decryptFileKey])
|
||||
});
|
||||
sharedSecrets.forEach((val) => {
|
||||
const dupKey = `${val.key}-${val.env}`;
|
||||
if (personalSecrets?.[dupKey]) {
|
||||
val.idOverride = personalSecrets[dupKey].id;
|
||||
val.valueOverride = personalSecrets[dupKey].value;
|
||||
val.overrideAction = "modified";
|
||||
}
|
||||
});
|
||||
return { secrets: sharedSecrets };
|
||||
},
|
||||
[decryptFileKey]
|
||||
)
|
||||
});
|
||||
|
||||
export const useGetProjectSecretsByKey = ({
|
||||
@@ -167,82 +174,85 @@ export const useGetProjectSecretsByKey = ({
|
||||
// right now secretpath is passed as folderid as only this is used in overview
|
||||
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
|
||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
|
||||
select: useCallback((data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||
// this used for add-only mode in dashboard
|
||||
// type won't be there thus only one key is shown
|
||||
const duplicateSecretKey: Record<string, boolean> = {};
|
||||
const uniqSecKeys: Record<string, boolean> = {};
|
||||
data.forEach((encSecret: EncryptedSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
select: useCallback(
|
||||
(data: EncryptedSecret[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
|
||||
const personalSecrets: Record<string, { id: string; value: string }> = {};
|
||||
// this used for add-only mode in dashboard
|
||||
// type won't be there thus only one key is shown
|
||||
const duplicateSecretKey: Record<string, boolean> = {};
|
||||
const uniqSecKeys: Record<string, boolean> = {};
|
||||
data.forEach((encSecret: EncryptedSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
|
||||
|
||||
const decryptedSecret = {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
if (encSecret.type === "personal") {
|
||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||
id: encSecret._id,
|
||||
value: secretValue
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
const decryptedSecret = {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
} else {
|
||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
||||
sharedSecrets[secretKey].push(decryptedSecret);
|
||||
}
|
||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||
}
|
||||
});
|
||||
Object.keys(sharedSecrets).forEach((secName) => {
|
||||
sharedSecrets[secName].forEach((val) => {
|
||||
const dupKey = `${val.key}-${val.env}`;
|
||||
if (personalSecrets?.[dupKey]) {
|
||||
val.idOverride = personalSecrets[dupKey].id;
|
||||
val.valueOverride = personalSecrets[dupKey].value;
|
||||
val.overrideAction = "modified";
|
||||
|
||||
if (encSecret.type === "personal") {
|
||||
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
|
||||
id: encSecret._id,
|
||||
value: secretValue
|
||||
};
|
||||
} else {
|
||||
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
|
||||
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
|
||||
sharedSecrets[secretKey].push(decryptedSecret);
|
||||
}
|
||||
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
Object.keys(sharedSecrets).forEach((secName) => {
|
||||
sharedSecrets[secName].forEach((val) => {
|
||||
const dupKey = `${val.key}-${val.env}`;
|
||||
if (personalSecrets?.[dupKey]) {
|
||||
val.idOverride = personalSecrets[dupKey].id;
|
||||
val.valueOverride = personalSecrets[dupKey].value;
|
||||
val.overrideAction = "modified";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
||||
}, [decryptFileKey])
|
||||
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
|
||||
},
|
||||
[decryptFileKey]
|
||||
)
|
||||
});
|
||||
|
||||
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
|
||||
@@ -263,29 +273,32 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
|
||||
enabled: Boolean(dto.secretId && dto.decryptFileKey),
|
||||
queryKey: secretKeys.getSecretVersion(dto.secretId),
|
||||
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
|
||||
select: useCallback((data: EncryptedSecretVersion[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = dto.decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
select: useCallback(
|
||||
(data: EncryptedSecretVersion[]) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = dto.decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
return data
|
||||
.map((el) => ({
|
||||
createdAt: el.createdAt,
|
||||
id: el._id,
|
||||
value: decryptSymmetric({
|
||||
ciphertext: el.secretValueCiphertext,
|
||||
iv: el.secretValueIV,
|
||||
tag: el.secretValueTag,
|
||||
key
|
||||
})
|
||||
}))
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}, [])
|
||||
return data
|
||||
.map((el) => ({
|
||||
createdAt: el.createdAt,
|
||||
id: el._id,
|
||||
value: decryptSymmetric({
|
||||
ciphertext: el.secretValueCiphertext,
|
||||
iv: el.secretValueIV,
|
||||
tag: el.secretValueTag,
|
||||
key
|
||||
})
|
||||
}))
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
},
|
||||
[dto.decryptFileKey]
|
||||
)
|
||||
});
|
||||
|
||||
export const useBatchSecretsOp = () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ export type DecryptedSecret = {
|
||||
valueOverride?: string;
|
||||
idOverride?: string;
|
||||
overrideAction?: string;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export type EncryptedSecretVersion = {
|
||||
@@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
|
||||
folderId?: string;
|
||||
secretPath?: string;
|
||||
isPaused?: boolean;
|
||||
include_imports?: boolean;
|
||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
export type ServiceTokenScope = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type ServiceToken = {
|
||||
_id: string;
|
||||
name: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
scopes: ServiceTokenScope[];
|
||||
user: string;
|
||||
expiresAt: string;
|
||||
createdAt: string;
|
||||
@@ -14,9 +18,8 @@ export type ServiceToken = {
|
||||
export type CreateServiceTokenDTO = {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
scopes: ServiceTokenScope[];
|
||||
expiresIn: number;
|
||||
secretPath: string;
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
|
||||
@@ -13,4 +13,7 @@ export type SubscriptionPlan = {
|
||||
workspaceLimit: number;
|
||||
workspacesUsed: number;
|
||||
environmentLimit: number;
|
||||
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
||||
trial_end: number | null;
|
||||
has_used_trial: boolean;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"
|
||||
export type { SubscriptionPlan } from "./subscriptions/types";
|
||||
export type { WsTag } from "./tags/types";
|
||||
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types";
|
||||
export type { TWebhook } from "./webhooks/types";
|
||||
export type {
|
||||
CreateEnvironmentDTO,
|
||||
CreateWorkspaceDTO,
|
||||
|
||||
@@ -2,11 +2,15 @@ export {
|
||||
fetchOrgUsers,
|
||||
useAddUserToOrg,
|
||||
useAddUserToWs,
|
||||
useCreateAPIKey,
|
||||
useDeleteAPIKey,
|
||||
useDeleteOrgMembership,
|
||||
useGetMyAPIKeys,
|
||||
useGetMySessions,
|
||||
useGetOrgUsers,
|
||||
useGetUser,
|
||||
useGetUserAction,
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
useUpdateOrgUserRole
|
||||
} from "./queries";
|
||||
useRevokeMySessions,
|
||||
useUpdateOrgUserRole} from "./queries";
|
||||
|
||||
@@ -12,16 +12,21 @@ import {
|
||||
AddUserToOrgDTO,
|
||||
AddUserToWsDTO,
|
||||
AddUserToWsRes,
|
||||
APIKeyData,
|
||||
CreateAPIKeyRes,
|
||||
DeletOrgMembershipDTO,
|
||||
OrgUser,
|
||||
RenameUserDTO,
|
||||
TokenVersion,
|
||||
UpdateOrgUserRoleDTO,
|
||||
User
|
||||
} from "./types";
|
||||
User} from "./types";
|
||||
|
||||
const userKeys = {
|
||||
getUser: ["user"] as const,
|
||||
userAction: ["user-action"] as const,
|
||||
getOrgUsers: (orgId: string) => [{ orgId }, "user"]
|
||||
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
|
||||
myAPIKeys: ["api-keys"] as const,
|
||||
mySessions: ["sessions"] as const
|
||||
};
|
||||
|
||||
export const fetchUserDetails = async () => {
|
||||
@@ -41,6 +46,18 @@ const fetchUserAction = async (action: string) => {
|
||||
return data.userAction;
|
||||
};
|
||||
|
||||
export const useRenameUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, RenameUserDTO>({
|
||||
mutationFn: ({ newName }) =>
|
||||
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserAction = (action: string) =>
|
||||
useQuery({
|
||||
queryKey: userKeys.userAction,
|
||||
@@ -167,3 +184,90 @@ export const useLogoutUser = () =>
|
||||
localStorage.setItem("PRIVATE_KEY", "");
|
||||
}
|
||||
});
|
||||
|
||||
export const useGetMyAPIKeys = () => {
|
||||
return useQuery({
|
||||
queryKey: userKeys.myAPIKeys,
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<APIKeyData[]>(
|
||||
"/api/v2/users/me/api-keys"
|
||||
);
|
||||
return data;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useCreateAPIKey = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
name,
|
||||
expiresIn
|
||||
}: {
|
||||
name: string;
|
||||
expiresIn: number;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post<CreateAPIKeyRes>(
|
||||
"/api/v2/users/me/api-keys",
|
||||
{
|
||||
name,
|
||||
expiresIn
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useDeleteAPIKey = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (apiKeyDataId: string) => {
|
||||
const { data } = await apiRequest.delete(
|
||||
`/api/v2/users/me/api-keys/${apiKeyDataId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(userKeys.myAPIKeys);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const useGetMySessions = () => {
|
||||
return useQuery({
|
||||
queryKey: userKeys.mySessions,
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<TokenVersion[]>(
|
||||
"/api/v2/users/me/sessions"
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useRevokeMySessions = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
console.log("useRevokeAllSessions 1");
|
||||
const { data } = await apiRequest.delete(
|
||||
"/api/v2/users/me/sessions"
|
||||
);
|
||||
|
||||
console.log("useRevokeAllSessions 2: ", data);
|
||||
return data;
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(userKeys.mySessions);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -61,3 +61,31 @@ export type AddUserToOrgDTO = {
|
||||
inviteeEmail: string;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export type CreateAPIKeyRes = {
|
||||
apiKey: string;
|
||||
apiKeyData: APIKeyData;
|
||||
}
|
||||
|
||||
export type RenameUserDTO = {
|
||||
newName: string;
|
||||
};
|
||||
|
||||
export type APIKeyData = {
|
||||
_id: string;
|
||||
name: string;
|
||||
user: string;
|
||||
lastUsed: string;
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export type TokenVersion = {
|
||||
_id: string;
|
||||
user: string;
|
||||
userAgent: string;
|
||||
ip: string;
|
||||
lastUsed: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateWebhook, useDeleteWebhook, useTestWebhook, useUpdateWebhook } from "./mutation";
|
||||
export { useGetWebhooks } from "./query";
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { queryKeys } from "./query";
|
||||
import { TCreateWebhookDto, TDeleteWebhookDto, TTestWebhookDTO, TUpdateWebhookDto } from "./types";
|
||||
|
||||
export const useCreateWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateWebhookDto>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.post("/api/v1/webhooks", dto);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TTestWebhookDTO>({
|
||||
mutationFn: async ({ webhookId }) => {
|
||||
const { data } = await apiRequest.post(`/api/v1/webhooks/${webhookId}/test`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
},
|
||||
onError: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateWebhookDto>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/webhooks/${dto.webhookId}`, {
|
||||
isDisabled: dto.isDisabled
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteWebhookDto>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/webhooks/${dto.webhookId}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TWebhook } from "./types";
|
||||
|
||||
export const queryKeys = {
|
||||
getWebhooks: (workspaceId: string) => ["webhooks", { workspaceId }]
|
||||
};
|
||||
|
||||
const fetchWebhooks = async (workspaceId: string) => {
|
||||
const { data } = await apiRequest.get<{ webhooks: TWebhook[] }>("/api/v1/webhooks", {
|
||||
params: {
|
||||
workspaceId
|
||||
}
|
||||
});
|
||||
|
||||
return data.webhooks;
|
||||
};
|
||||
|
||||
export const useGetWebhooks = (workspaceId: string) =>
|
||||
useQuery({
|
||||
queryKey: queryKeys.getWebhooks(workspaceId),
|
||||
queryFn: () => fetchWebhooks(workspaceId),
|
||||
enabled: Boolean(workspaceId)
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
export type TWebhook = {
|
||||
_id: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url: string;
|
||||
lastStatus: "success" | "failed";
|
||||
lastRunErrorMessage?: string;
|
||||
isDisabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateWebhookDto = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
webhookUrl: string;
|
||||
webhookSecretKey?: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type TUpdateWebhookDto = {
|
||||
webhookId: string;
|
||||
workspaceId: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type TDeleteWebhookDto = {
|
||||
webhookId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type TTestWebhookDTO = {
|
||||
webhookId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export { useLeaveConfirm } from "./useLeaveConfirm";
|
||||
export { usePersistentState } from "./usePersistentState";
|
||||
export { usePopUp } from "./usePopUp";
|
||||
export { useSyntaxHighlight } from "./useSyntaxHighlight";
|
||||
export { useToggle } from "./useToggle";
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
|
||||
export const useSyntaxHighlight = () => {
|
||||
const syntaxHighlight = useCallback((text: string, isHidden?: boolean) => {
|
||||
if (isHidden) {
|
||||
if (!text) {
|
||||
return "EMPTY";
|
||||
}
|
||||
|
||||
return text
|
||||
.split("")
|
||||
.slice(0, 200)
|
||||
.map((el, i) =>
|
||||
el === "\n" ? (
|
||||
el
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
key={`${text}_${el}_${i + 1}`}
|
||||
className="mr-0.5 text-xxs"
|
||||
icon={faCircle}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// append a space on last new line this to show new line in ui for code component
|
||||
const val = text.at(-1) === "\n" ? text.concat(" ") : text;
|
||||
if (val?.length === 0) return <span className="font-mono text-bunker-400/80">EMPTY</span>;
|
||||
return val?.split(REGEX).map((word, i) =>
|
||||
word.match(REGEX) !== null ? (
|
||||
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
|
||||
${
|
||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
||||
}
|
||||
</span>
|
||||
) : (
|
||||
<span key={`${word}_${i + 1}`} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
return syntaxHighlight;
|
||||
};
|
||||
@@ -7,15 +7,16 @@
|
||||
// @ts-nocheck
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faBookOpen, faMobile, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import queryString from "query-string";
|
||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
@@ -25,6 +26,9 @@ import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypt
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
FormControl,
|
||||
Input,
|
||||
Menu,
|
||||
@@ -37,16 +41,35 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
|
||||
import { Navbar } from "./components/NavBar";
|
||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const supportOptions = [
|
||||
[
|
||||
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
|
||||
"Support Forum",
|
||||
"https://infisical.com/slack"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
|
||||
"Read Docs",
|
||||
"https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
|
||||
"GitHub Issues",
|
||||
"https://github.com/Infisical/infisical/issues"
|
||||
],
|
||||
[
|
||||
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
|
||||
"Email Support",
|
||||
"mailto:[email protected]"
|
||||
]
|
||||
];
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("Project Name").trim(),
|
||||
addMembers: yup.bool().required().label("Add Members")
|
||||
@@ -57,12 +80,14 @@ type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||
export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
const { workspaces, currentWorkspace } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { orgs, currentOrg } = useOrganization();
|
||||
const { user } = useUser();
|
||||
const { subscription } = useSubscription();
|
||||
// const [ isLearningNoteOpen, setIsLearningNoteOpen ] = useState(true);
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
||||
|
||||
@@ -83,10 +108,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
|
||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -101,112 +122,77 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const logout = useLogoutUser();
|
||||
const logOutUser = async () => {
|
||||
try {
|
||||
console.log("Logging out...")
|
||||
await logout.mutateAsync();
|
||||
localStorage.removeItem("protectedKey");
|
||||
localStorage.removeItem("protectedKeyIV");
|
||||
localStorage.removeItem("protectedKeyTag");
|
||||
localStorage.removeItem("publicKey");
|
||||
localStorage.removeItem("encryptedPrivateKey");
|
||||
localStorage.removeItem("iv");
|
||||
localStorage.removeItem("tag");
|
||||
localStorage.removeItem("PRIVATE_KEY");
|
||||
localStorage.removeItem("orgData.id");
|
||||
localStorage.removeItem("projectData.id");
|
||||
router.push("/login");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const changeOrg = async (orgId) => {
|
||||
localStorage.setItem("orgData.id", orgId);
|
||||
router.push(`/org/${orgId}/overview`)
|
||||
}
|
||||
|
||||
// TODO(akhilmhdh): This entire logic will be rechecked and will try to avoid
|
||||
// Placing the localstorage as much as possible
|
||||
// Wait till tony integrates the azure and its launched
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
|
||||
const putUserInWorkSpace = async () => {
|
||||
// Put a user in an org if they're not in one yet
|
||||
const putUserInOrg = async () => {
|
||||
if (tempLocalStorage("orgData.id") === "") {
|
||||
const userOrgs = await getOrganizations();
|
||||
localStorage.setItem("orgData.id", userOrgs[0]?._id);
|
||||
localStorage.setItem("orgData.id", orgs[0]?._id);
|
||||
}
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
(userWorkspaces?.length === 0 &&
|
||||
router.asPath !== "/noprojects" &&
|
||||
!router.asPath.includes("home") &&
|
||||
!router.asPath.includes("settings")) ||
|
||||
router.asPath === "/dashboard/undefined"
|
||||
) {
|
||||
router.push("/noprojects");
|
||||
} else if (router.asPath !== "/noprojects") {
|
||||
// const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0);
|
||||
if (currentOrg && (
|
||||
(workspaces?.length === 0 && router.asPath.includes("project"))
|
||||
|| router.asPath.includes("/project/undefined")
|
||||
|| (!orgs?.map(org => org._id)?.includes(router.query.id) && !router.asPath.includes("project") && !router.asPath.includes("personal") && !router.asPath.includes("integration"))
|
||||
)) {
|
||||
router.push(`/org/${currentOrg?._id}/overview`);
|
||||
}
|
||||
// else if (!router.asPath.includes("org") && !router.asPath.includes("project") && !router.asPath.includes("integrations") && !router.asPath.includes("personal-settings")) {
|
||||
|
||||
// let intendedWorkspaceId;
|
||||
// if (pathSegments.length >= 2 && pathSegments[0] === 'dashboard') {
|
||||
// intendedWorkspaceId = pathSegments[1];
|
||||
// } else if (pathSegments.length >= 3 && pathSegments[0] === 'settings') {
|
||||
// intendedWorkspaceId = pathSegments[2];
|
||||
// } else {
|
||||
// intendedWorkspaceId = router.asPath
|
||||
// .split('/')
|
||||
// [router.asPath.split('/').length - 1].split('?')[0];
|
||||
// }
|
||||
// const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
||||
|
||||
const pathSegments = router.asPath.split("/").filter((segment) => segment.length > 0);
|
||||
// let intendedWorkspaceId;
|
||||
// if (pathSegments.length >= 2 && pathSegments[0] === "dashboard") {
|
||||
// [, intendedWorkspaceId] = pathSegments;
|
||||
// } else if (pathSegments.length >= 3 && pathSegments[0] === "settings") {
|
||||
// [, , intendedWorkspaceId] = pathSegments;
|
||||
// } else {
|
||||
// const lastPathSegments = router.asPath.split("/").pop();
|
||||
// if (lastPathSegments !== undefined) {
|
||||
// [intendedWorkspaceId] = lastPathSegments.split("?");
|
||||
// }
|
||||
// }
|
||||
|
||||
let intendedWorkspaceId;
|
||||
if (pathSegments.length >= 2 && pathSegments[0] === "dashboard") {
|
||||
[, intendedWorkspaceId] = pathSegments;
|
||||
} else if (pathSegments.length >= 3 && pathSegments[0] === "settings") {
|
||||
[, , intendedWorkspaceId] = pathSegments;
|
||||
} else {
|
||||
const lastPathSegments = router.asPath.split("/").pop();
|
||||
if (lastPathSegments !== undefined) {
|
||||
[intendedWorkspaceId] = lastPathSegments.split("?");
|
||||
}
|
||||
// if (!intendedWorkspaceId) return;
|
||||
|
||||
// const lastPathSegment = router.asPath.split('/').pop().split('?');
|
||||
// [intendedWorkspaceId] = lastPathSegment;
|
||||
}
|
||||
|
||||
if (!intendedWorkspaceId) return;
|
||||
|
||||
if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
|
||||
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
}
|
||||
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
!["callback", "create", "authorize"].includes(intendedWorkspaceId) &&
|
||||
userWorkspaces[0]?._id !== undefined &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
const { env } = queryString.parse(router.asPath.split("?")[1]);
|
||||
if (!env) {
|
||||
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||
}
|
||||
} else {
|
||||
setWorkspaceMapping(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
|
||||
) as any
|
||||
);
|
||||
setWorkspaceSelected(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
|
||||
)[router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]]
|
||||
);
|
||||
}
|
||||
}
|
||||
// if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
|
||||
// localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
// }
|
||||
// }
|
||||
};
|
||||
putUserInWorkSpace();
|
||||
onboardingCheck({ setTotalOnboardingActionsDone });
|
||||
putUserInOrg();
|
||||
onboardingCheck({});
|
||||
}, [router.query.id]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (
|
||||
workspaceMapping[workspaceSelected as any] &&
|
||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||
router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]
|
||||
) {
|
||||
localStorage.setItem("projectData.id", `${workspaceMapping[workspaceSelected as any]}`);
|
||||
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, [workspaceSelected]);
|
||||
|
||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg?._id) return;
|
||||
@@ -247,7 +233,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
}
|
||||
createNotification({ text: "Workspace created", type: "success" });
|
||||
handlePopUpClose("addNewWs");
|
||||
router.push(`/dashboard/${newWorkspaceId}`);
|
||||
router.push(`/project/${newWorkspaceId}/secrets`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||
@@ -257,13 +243,84 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
<Navbar />
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
<aside className="w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60 dark">
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div>
|
||||
{currentWorkspace && router.asPath !== "/noprojects" ? (
|
||||
<div className="mt-3 mb-4 w-full p-4">
|
||||
{!router.asPath.includes("personal") && <div className="h-12 px-3 flex items-center pt-6 cursor-default">
|
||||
{(router.asPath.includes("project") || router.asPath.includes("integrations")) && <Link href={`/org/${currentOrg?._id}/overview`}><div className="pl-1 pr-2 text-mineshaft-400 hover:text-mineshaft-100 duration-200">
|
||||
<FontAwesomeIcon icon={faArrowLeft} />
|
||||
</div></Link>}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||
<div className="mr-auto flex items-center hover:bg-mineshaft-600 py-1.5 pl-1.5 pr-2 rounded-md">
|
||||
<div className="w-5 h-5 rounded-md bg-primary flex justify-center items-center text-sm">{currentOrg?.name.charAt(0)}</div>
|
||||
<div className="pl-3 text-mineshaft-100 text-sm">{currentOrg?.name} <FontAwesomeIcon icon={faAngleDown} className="text-xs pl-1 pt-1 text-mineshaft-300" /></div>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
||||
{orgs?.map(org => <DropdownMenuItem key={org._id}>
|
||||
<Button
|
||||
onClick={() => changeOrg(org?._id)}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
className="w-full flex items-center justify-start p-0 font-normal"
|
||||
leftIcon={currentOrg._id === org._id && <FontAwesomeIcon icon={faCheck} className="mr-3 text-primary"/>}
|
||||
>
|
||||
<div className="w-full flex justify-between items-center">{org.name}</div>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={logOutUser}
|
||||
className="w-full"
|
||||
>
|
||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
</button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="hover:bg-primary-400 hover:text-black data-[state=open]:text-black data-[state=open]:bg-primary-400 p-1">
|
||||
<div className="child w-6 h-6 rounded-full bg-mineshaft hover:bg-mineshaft-500 pr-1 text-xs text-mineshaft-300 flex justify-center items-center">
|
||||
{user?.firstName?.charAt(0)}{user?.lastName && user?.lastName?.charAt(0)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="text-xs text-mineshaft-400 px-2 py-1">{user?.email}</div>
|
||||
<Link href="/personal-settings"><DropdownMenuItem>Personal Settings</DropdownMenuItem></Link>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/introduction"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100"
|
||||
>
|
||||
<DropdownMenuItem>Documentation<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem>
|
||||
</a>
|
||||
<a
|
||||
href="https://infisical.com/slack"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full mt-3 text-sm text-mineshaft-300 font-normal leading-[1.2rem] hover:text-mineshaft-100"
|
||||
>
|
||||
<DropdownMenuItem>Join Slack Community<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-1.5 text-xxs mb-[0.06rem]" /></DropdownMenuItem>
|
||||
</a>
|
||||
<div className="h-1 mt-1 border-t border-mineshaft-600"/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={logOutUser}
|
||||
className="w-full"
|
||||
>
|
||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||
</button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>}
|
||||
{!router.asPath.includes("org") && (!router.asPath.includes("personal") && currentWorkspace ? (
|
||||
<div className="mt-5 mb-4 w-full p-3">
|
||||
<p className="ml-1.5 mb-1 text-xs font-semibold uppercase text-gray-400">
|
||||
Project
|
||||
</p>
|
||||
@@ -272,7 +329,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
value={currentWorkspace?._id}
|
||||
className="w-full truncate bg-mineshaft-600 py-2.5 font-medium"
|
||||
onValueChange={(value) => {
|
||||
router.push(`/dashboard/${value}`);
|
||||
router.push(`/project/${value}/secrets`);
|
||||
}}
|
||||
position="popper"
|
||||
dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 z-50 max-h-96 border-gray-700"
|
||||
@@ -311,43 +368,26 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</div>
|
||||
</Select>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 mb-4 w-full p-4">
|
||||
<Button
|
||||
className="border-mineshaft-500"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
isFullWidth
|
||||
onClick={() => {
|
||||
if (isAddingProjectsAllowed) {
|
||||
handlePopUpOpen("addNewWs")
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add Project
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${currentWorkspace && router.asPath !== "/noprojects" ? "block" : "hidden"}`}>
|
||||
<Menu>
|
||||
<Link href={`/dashboard/${currentWorkspace?._id}`} passHref>
|
||||
) : <Link href={`/org/${currentOrg?._id}/overview`}><div className="pr-2 my-6 flex justify-center items-center text-mineshaft-300 hover:text-mineshaft-100 cursor-default text-sm">
|
||||
<FontAwesomeIcon icon={faArrowLeft} className="pr-3"/>
|
||||
Back to organization
|
||||
</div></Link>)}
|
||||
<div className={`px-1 ${!router.asPath.includes("personal") ? "block" : "hidden"}`}>
|
||||
{((router.asPath.includes("project") || router.asPath.includes("integrations")) && currentWorkspace) ? <Menu>
|
||||
<Link href={`/project/${currentWorkspace?._id}/secrets`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes(`/dashboard/${currentWorkspace?._id}`)}
|
||||
isSelected={router.asPath.includes(`/project/${currentWorkspace?._id}/secrets`)}
|
||||
icon="system-outline-90-lock-closed"
|
||||
>
|
||||
{t("nav.menu.secrets")}
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/users/${currentWorkspace?._id}`} passHref>
|
||||
<Link href={`/project/${currentWorkspace?._id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/users/${currentWorkspace?._id}`}
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/members`}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
{t("nav.menu.members")}
|
||||
@@ -364,20 +404,32 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/activity/${currentWorkspace?._id}`} passHref>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/activity/${currentWorkspace?._id}`}
|
||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/audit-logs`}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/settings/project/${currentWorkspace?._id}`} passHref>
|
||||
{/* <Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/project/${currentWorkspace?._id}/secret-scanning`}
|
||||
// icon={<FontAwesomeIcon icon={faFileLines} size="lg" />}
|
||||
icon="system-outline-82-extension"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link> */}
|
||||
<Link href={`/project/${currentWorkspace?._id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/settings/project/${currentWorkspace?._id}`
|
||||
router.asPath === `/project/${currentWorkspace?._id}/settings`
|
||||
}
|
||||
icon="system-outline-109-slider-toggle-settings"
|
||||
>
|
||||
@@ -386,52 +438,162 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</a>
|
||||
</Link>
|
||||
</Menu>
|
||||
: <Menu className="mt-4">
|
||||
<Link href={`/org/${currentOrg?._id}/overview`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath.includes("/overview")}
|
||||
icon="system-outline-165-view-carousel"
|
||||
>
|
||||
Overview
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
{/* {workspaces.map(project => <Link key={project._id} href={`/project/${project?._id}/secrets`} passHref>
|
||||
<a>
|
||||
<SubMenuItem
|
||||
isSelected={false}
|
||||
icon="system-outline-44-folder"
|
||||
>
|
||||
{project.name}
|
||||
</SubMenuItem>
|
||||
</a>
|
||||
<div className="pl-8 text-mineshaft-300 text-sm py-1 cursor-default hover:text-mineshaft-100">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-xxs pr-0.5"/> {project.name} <FontAwesomeIcon icon={faArrowRight} className="text-xs pl-0.5"/>
|
||||
</div>
|
||||
</Link>)} */}
|
||||
<Link href={`/org/${currentOrg?._id}/members`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/org/${currentOrg?._id}/members`}
|
||||
icon="system-outline-96-groups"
|
||||
>
|
||||
Members
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?._id}/secret-scanning`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/org/${currentOrg?._id}/secret-scanning`}
|
||||
icon="system-outline-69-document-scan"
|
||||
>
|
||||
Secret Scanning
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?._id}/billing`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/org/${currentOrg?._id}/billing`}
|
||||
icon="system-outline-103-coin-cash-monetization"
|
||||
>
|
||||
Usage & Billing
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?._id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/org/${currentOrg?._id}/settings`
|
||||
}
|
||||
icon="system-outline-109-slider-toggle-settings"
|
||||
>
|
||||
Organization Settings
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
</Menu>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-40 mb-4 w-full px-2">
|
||||
{router.asPath.split("/")[1] === "home" ? (
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
<div className={`relative mt-10 ${subscription && subscription.slug === "starter" && !subscription.has_used_trial ? "mb-2" : "mb-4"} w-full px-3 text-mineshaft-400 cursor-default text-sm flex flex-col items-center`}>
|
||||
{/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/>
|
||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/>
|
||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
|
||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/>
|
||||
<div className={`${isLearningNoteOpen ? "block" : "hidden"} relative z-10 h-60 w-52 bg-mineshaft-900 border border-mineshaft-600 mb-6 rounded-md flex flex-col items-center justify-start px-3`}>
|
||||
<div className="w-full mt-2 text-md text-mineshaft-100 font-semibold">Kubernetes Operator</div>
|
||||
<div className="w-full mt-1 text-sm text-mineshaft-300 font-normal leading-[1.2rem] mb-1">Integrate Infisical into your Kubernetes infrastructure</div>
|
||||
<div className="h-[6.8rem] w-full bg-mineshaft-200 rounded-md mt-2 rounded-md border border-mineshaft-700">
|
||||
<Image src="/images/kubernetes-asset.png" height={319} width={539} alt="kubernetes image" className="rounded-sm" />
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/home/${currentWorkspace?._id}`}>
|
||||
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
<div className="w-full flex justify-between items-center mt-3 px-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsLearningNoteOpen(false)}
|
||||
className="text-mineshaft-400 hover:text-mineshaft-100 duration-200"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<a
|
||||
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-mineshaft-400 font-normal leading-[1.2rem] hover:text-mineshaft-100 duration-200"
|
||||
>
|
||||
Learn More <FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-xs pl-0.5"/>
|
||||
</a>
|
||||
</div>
|
||||
</div> */}
|
||||
{router.asPath.includes("org") && <div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/org/${router.query.id}/members?action=invite`)}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="hover:text-mineshaft-200 duration-200 mb-3 pl-5 w-full">
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-3"/>
|
||||
Invite people
|
||||
</div>
|
||||
</div>}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="hover:text-mineshaft-200 duration-200 mb-2 pl-5 w-full">
|
||||
<FontAwesomeIcon icon={faQuestion} className="px-[0.1rem] mr-3"/>
|
||||
Help & Support
|
||||
</div>
|
||||
</Link>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{supportOptions.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url}>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={String(url)}
|
||||
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
|
||||
>
|
||||
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
|
||||
{icon}
|
||||
<div className="text-sm">{text}</div>
|
||||
</div>
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="w-full mt-1.5"
|
||||
>
|
||||
<div className="hover:text-primary-400 text-mineshaft-300 duration-200 flex justify-left items-center py-1 bg-mineshaft-600 rounded-md hover:bg-mineshaft-500 mb-1.5 mt-1.5 pl-4 w-full">
|
||||
<FontAwesomeIcon icon={faInfinity} className="mr-3 ml-0.5 py-2 text-primary"/>
|
||||
Start Free Pro Trial
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -21,7 +21,9 @@ import {TFunction} from "i18next";
|
||||
|
||||
import guidGenerator from "@app/components/utilities/randomId";
|
||||
import { useOrganization, useSubscription,useUser } from "@app/context";
|
||||
import { useLogoutUser } from "@app/hooks/api";
|
||||
import {
|
||||
useGetOrgTrialUrl,
|
||||
useLogoutUser} from "@app/hooks/api";
|
||||
|
||||
const supportOptions = (t: TFunction) => [
|
||||
[
|
||||
@@ -64,8 +66,9 @@ export interface IUser {
|
||||
export const Navbar = () => {
|
||||
const router = useRouter();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
|
||||
const { currentOrg, orgs } = useOrganization();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
const { user } = useUser();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
@@ -96,16 +99,15 @@ export const Navbar = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="z-[70] flex w-full flex-row justify-between border-b border-mineshaft-500 bg-mineshaft-900 text-white">
|
||||
<div className="m-auto mx-4 flex items-center justify-start">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
|
||||
</div>
|
||||
<a href="#" className="mx-2 text-2xl font-semibold text-white">
|
||||
Infisical
|
||||
</a>
|
||||
<div className="z-[70] border-b border-mineshaft-500 bg-mineshaft-900 text-white">
|
||||
<div className="flex w-full justify-between px-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="flex justify-center py-4">
|
||||
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
|
||||
</div>
|
||||
<a href="#" className="mx-2 text-2xl font-semibold text-white">
|
||||
Infisical
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative z-40 mx-2 flex items-center justify-start">
|
||||
<a
|
||||
@@ -178,7 +180,7 @@ export const Navbar = () => {
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
|
||||
onClick={() => router.push("/personal-settings")}
|
||||
className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5"
|
||||
>
|
||||
<div className="flex h-8 w-9 items-center justify-center rounded-full bg-white/10 text-gray-300">
|
||||
@@ -190,7 +192,7 @@ export const Navbar = () => {
|
||||
{" "}
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="px-2 pb-1 text-xs text-gray-400"> {user?.email}</p>
|
||||
<p className="px-2 pb-1 text-xs text-gray-400">{user?.email}</p>
|
||||
</div>
|
||||
<FontAwesomeIcon
|
||||
icon={faGear}
|
||||
@@ -314,6 +316,28 @@ export const Navbar = () => {
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
<div className="w-full mx-auto border-t border-mineshaft-500 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="text-center py-4 text-sm mx-auto"
|
||||
>
|
||||
You are currently on the <span className="font-semibold">Starter</span> plan. Unlock the full power of Infisical on the <span className="font-semibold">Pro Free Trial →</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { Navbar } from "./NavBar";
|
||||
@@ -21,11 +21,10 @@ export default function Custom404() {
|
||||
</a>{" "}
|
||||
and we`ll fix it!{" "}
|
||||
</p>
|
||||
<Link
|
||||
href='/dashboard'
|
||||
className='bg-mineshaft-500 mt-8 py-2 px-4 rounded-md hover:bg-primary diration-200 hover:text-black cursor-pointer font-semibold'
|
||||
>
|
||||
Go to Dashboard
|
||||
<Link href='/dashboard'>
|
||||
<div className="mt-8 bg-mineshaft-500 py-2 px-4 rounded-md hover:bg-primary diration-200 hover:text-black font-semibold cursor-default">
|
||||
Go to Dashboard
|
||||
</div>
|
||||
</Link>
|
||||
<Image
|
||||
src='/images/dragon-404.svg'
|
||||
|
||||
@@ -77,8 +77,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<AuthProvider>
|
||||
<WorkspaceProvider>
|
||||
<OrgProvider>
|
||||
<OrgProvider>
|
||||
<WorkspaceProvider>
|
||||
<SubscriptionProvider>
|
||||
<UserProvider>
|
||||
<NotificationProvider>
|
||||
@@ -90,8 +90,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
</NotificationProvider>
|
||||
</UserProvider>
|
||||
</SubscriptionProvider>
|
||||
</OrgProvider>
|
||||
</WorkspaceProvider>
|
||||
</WorkspaceProvider>
|
||||
</OrgProvider>
|
||||
</AuthProvider>
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route adds an API key for the user
|
||||
* @param {object} obj
|
||||
* @param {string} obj.name - name of the API key
|
||||
* @param {string} obj.expiresIn - how soon the API key expires in ms
|
||||
* @returns
|
||||
*/
|
||||
const addAPIKey = ({ name, expiresIn }: Props) =>
|
||||
SecurityClient.fetchCall("/api/v2/api-key/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
expiresIn
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to add API key");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default addAPIKey;
|
||||
@@ -1,27 +0,0 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
interface Props {
|
||||
apiKeyId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This route revokes the API key with id [apiKeyId]
|
||||
* @param {object} obj
|
||||
* @param {string} obj.apiKeyId - id of the API key to delete
|
||||
* @returns
|
||||
*/
|
||||
const deleteAPIKey = ({ apiKeyId }: Props) =>
|
||||
SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to delete API key");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default deleteAPIKey;
|
||||
@@ -1,22 +0,0 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* This route gets API keys for the user
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
const getAPIKeys = () =>
|
||||
SecurityClient.fetchCall("/api/v2/api-key", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).apiKeyData;
|
||||
}
|
||||
console.log("Failed to get API keys");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getAPIKeys;
|
||||
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const createNewIntegrationSession = (organizationId: string) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/create-installation-session/organization/${organizationId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to create integration session");
|
||||
console.log("response", res)
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default createNewIntegrationSession;
|
||||
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const getInstallationStatus = (organizationId: string) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/installation-status/organization/${organizationId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).appInstallationComplete;
|
||||
}
|
||||
console.log("Failed to check installation status");
|
||||
console.log("response", res)
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getInstallationStatus;
|
||||
@@ -0,0 +1,58 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export type GitRisks = {
|
||||
_id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
startColumn: string;
|
||||
endColumn: string;
|
||||
match: string;
|
||||
secret: string;
|
||||
file: string;
|
||||
symlinkFile: string;
|
||||
commit: string;
|
||||
entropy: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
message: string;
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
status: string;
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
riskOwner: string | null; // New field for setting a risk owner (nullable string)
|
||||
installationId: string,
|
||||
repositoryId: string,
|
||||
repositoryLink: string
|
||||
repositoryFullName: string
|
||||
pusher: {
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
createdAt: string,
|
||||
organization: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const getRisksByOrganization = (oranizationId: string): Promise<GitRisks[]> =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${oranizationId}/risks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).risks;
|
||||
}
|
||||
console.log("Failed to fetch risks");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getRisksByOrganization;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const linkGitAppInstallationWithOrganization = (installationId: string, sessionId: string) =>
|
||||
SecurityClient.fetchCall("/api/v1/secret-scanning/link-installation", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
installationId,
|
||||
sessionId
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return true
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default linkGitAppInstallationWithOrganization;
|
||||
@@ -0,0 +1,33 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export enum RiskStatus {
|
||||
RESOLVED_FALSE_POSITIVE = "RESOLVED_FALSE_POSITIVE",
|
||||
RESOLVED_REVOKED = "RESOLVED_REVOKED",
|
||||
RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED",
|
||||
UNRESOLVED = "UNRESOLVED",
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const updateRiskStatus = (organizationId: string, riskId: string, status: RiskStatus) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${organizationId}/risks/${riskId}/status`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default updateRiskStatus;
|
||||
@@ -1,4 +1,5 @@
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function CliRedirect() {
|
||||
return (
|
||||
@@ -8,9 +9,12 @@ export default function CliRedirect() {
|
||||
<link rel='icon' href='/infisical.ico' />
|
||||
</Head>
|
||||
<div className='flex flex-col items-center justify-center text-gray-200 h-screen w-screen'>
|
||||
<p className='text-4xl mt-32'>Head back to your terminal!</p>
|
||||
<p className='mt-2 mb-1 text-lg'>
|
||||
You've successfully logged into infisical-cli
|
||||
<div className="mb-8 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||
</div>
|
||||
<p className='text-3xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center'>Head back to your terminal</p>
|
||||
<p className='mb-1 text-lg text-light text-mineshaft-400'>
|
||||
You've successfully logged in to the Infisical CLI
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
import getOrganizations from "./api/organization/getOrgs";
|
||||
|
||||
export default function DashboardRedirect() {
|
||||
const router = useRouter();
|
||||
@@ -12,14 +12,14 @@ export default function DashboardRedirect() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let userWorkspace;
|
||||
let userOrg;
|
||||
try {
|
||||
if (localStorage.getItem("projectData.id")) {
|
||||
router.push(`/dashboard/${ localStorage.getItem("projectData.id")}`);
|
||||
if (localStorage.getItem("orgData.id")) {
|
||||
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
|
||||
} else {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspace = userWorkspaces[0]._id;
|
||||
router.push(`/dashboard/${ userWorkspace}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
userOrg = userOrgs[0]._id;
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faHandPeace,
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faStar,
|
||||
faUserPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import { TabsObject } from "@app/components/v2/Tabs";
|
||||
|
||||
import registerUserAction from "../api/userActions/registerUserAction";
|
||||
|
||||
type ItemProps = {
|
||||
text: string;
|
||||
subText: string;
|
||||
complete: boolean;
|
||||
icon: IconProp;
|
||||
time: string;
|
||||
userAction?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const learningItem = ({
|
||||
text,
|
||||
subText,
|
||||
complete,
|
||||
icon,
|
||||
time,
|
||||
userAction,
|
||||
link
|
||||
}: ItemProps): JSX.Element => {
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
target={`${link.includes("https") ? "_blank" : "_self"}`}
|
||||
rel="noopener noreferrer"
|
||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||
href={link}
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction && userAction !== "first_time_secrets_pushed") {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="group relative mb-3 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-bunker-500 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction) {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500"
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-11 top-10 h-7 w-7 rounded-full bg-bunker-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="absolute left-12 top-16 h-5 w-5 text-4xl text-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="mt-0.5 text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This tab is called Home because in the future it will include some company news,
|
||||
* updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction
|
||||
* for the new users
|
||||
*/
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
||||
const [hasUserStarred, setHasUserStarred] = useState(false);
|
||||
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
||||
const [usersInOrg, setUsersInOrg] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onboardingCheck({
|
||||
setHasUserClickedIntro,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserPushedSecrets,
|
||||
setHasUserStarred,
|
||||
setUsersInOrg
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-6 w-full pt-4 lg:mx-0">
|
||||
<Head>
|
||||
<title>Infisical Guide</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="relative mx-auto flex max-w-3xl flex-col items-center px-6 py-6 text-lg text-gray-300 lg:max-w-4xl xl:max-w-5xl">
|
||||
<div className="mt-12 w-full text-left text-5xl font-bold">Your quick start guide</div>
|
||||
<div className="mb-14 w-full pt-2 pb-4 text-left text-lg text-bunker-300">
|
||||
Click on the items below and follow the instructions.
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 hidden h-min lg:block">
|
||||
<Image
|
||||
src="/images/dragon-book.svg"
|
||||
height={250}
|
||||
width={400}
|
||||
alt="start guise dragon illustration"
|
||||
/>
|
||||
</div>
|
||||
{learningItem({
|
||||
text: "Get to know Infisical",
|
||||
subText: "",
|
||||
complete: hasUserClickedIntro,
|
||||
icon: faHandPeace,
|
||||
time: "3 min",
|
||||
userAction: "intro_cta_clicked",
|
||||
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Add your secrets",
|
||||
subText: "Click to see example secrets, and add your own.",
|
||||
complete: hasUserPushedSecrets,
|
||||
icon: faPlus,
|
||||
time: "2 min",
|
||||
userAction: "first_time_secrets_pushed",
|
||||
link: `/dashboard/${router.query.id}`
|
||||
})}
|
||||
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-bunker-500 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-green" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>
|
||||
{learningItem({
|
||||
text: "Integrate Infisical with your infrastructure",
|
||||
subText: "Connect Infisical to various 3rd party services and platforms.",
|
||||
complete: false,
|
||||
icon: faPlug,
|
||||
time: "15 min",
|
||||
link: "https://infisical.com/docs/integrations/overview"
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Invite your teammates",
|
||||
subText: "",
|
||||
complete: usersInOrg,
|
||||
icon: faUserPlus,
|
||||
time: "2 min",
|
||||
link: `/settings/org/${router.query.id}?invite`
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Join Infisical Slack",
|
||||
subText: "Have any questions? Ask us!",
|
||||
complete: hasUserClickedSlack,
|
||||
icon: faSlack,
|
||||
time: "1 min",
|
||||
userAction: "slack_cta_clicked",
|
||||
link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg"
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Star Infisical on GitHub",
|
||||
subText: "Like what we're doing? You know what to do! :)",
|
||||
complete: hasUserStarred,
|
||||
icon: faStar,
|
||||
time: "1 min",
|
||||
userAction: "star_cta_clicked",
|
||||
link: "https://github.com/Infisical/infisical"
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Home.requireAuth = true;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
|
||||
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
|
||||
|
||||
export default function LaravelForgeCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [serverId, setServerId] = useState("");
|
||||
const [serverIdErrorText, setServerIdErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
setServerIdErrorText("");
|
||||
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("Access Token cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
if (serverId.length === 0) {
|
||||
setServerIdErrorText("Server Id cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await saveIntegrationAccessToken({
|
||||
workspaceId: localStorage.getItem("projectData.id"),
|
||||
integration: "laravel-forge",
|
||||
accessId: serverId,
|
||||
accessToken: apiKey,
|
||||
url: null,
|
||||
namespace: null
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
router.push(`/integrations/laravel-forge/create?integrationAuthId=${integrationAuth._id}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Laravel Forge Integration</CardTitle>
|
||||
<FormControl
|
||||
label="Laravel Forge Access Token"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== "" ?? false}
|
||||
>
|
||||
<Input placeholder="Access Token" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<FormControl
|
||||
label="Laravel Forge Server ID"
|
||||
errorText={serverIdErrorText}
|
||||
isError={serverIdErrorText !== "" ?? false}
|
||||
>
|
||||
<Input placeholder="123456" value={serverId} onChange={(e) => setServerId(e.target.value)} />
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Laravel Forge
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LaravelForgeCreateIntegrationPage.requireAuth = true;
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import queryString from "query-string";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
FormControl,
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "../../../components/v2";
|
||||
import {
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthById
|
||||
} from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
import createIntegration from "../../api/integrations/createIntegration";
|
||||
|
||||
export default function LaravelForgeCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||
|
||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? ""
|
||||
});
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [targetApp, setTargetApp] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setTargetApp(integrationAuthApps[0].name);
|
||||
} else {
|
||||
setTargetApp("none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
if (!integrationAuth?._id) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
await createIntegration({
|
||||
integrationAuthId: integrationAuth?._id,
|
||||
isActive: true,
|
||||
app: targetApp,
|
||||
appId:
|
||||
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
|
||||
?.appId ?? null,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetEnvironment: null,
|
||||
targetEnvironmentId: null,
|
||||
targetService: null,
|
||||
targetServiceId: null,
|
||||
owner: null,
|
||||
path: null,
|
||||
region: null,
|
||||
secretPath
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return integrationAuth &&
|
||||
workspace &&
|
||||
selectedSourceEnvironment &&
|
||||
integrationAuthApps &&
|
||||
targetApp ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">Laravel Forge Integration</CardTitle>
|
||||
<FormControl label="Project Environment" className="mt-4">
|
||||
<Select
|
||||
value={selectedSourceEnvironment}
|
||||
onValueChange={(val) => setSelectedSourceEnvironment(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{workspace?.environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`source-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="Secrets Path">
|
||||
<Input
|
||||
value={secretPath}
|
||||
onChange={(evt) => setSecretPath(evt.target.value)}
|
||||
placeholder="Provide a path, default is /"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl label="Laravel Forge Sites" className="mt-4">
|
||||
<Select
|
||||
value={targetApp}
|
||||
onValueChange={(val) => setTargetApp(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={integrationAuthApp.name}
|
||||
key={`target-app-${integrationAuthApp.name}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No sites found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
}
|
||||
|
||||
LaravelForgeCreateIntegrationPage.requireAuth = true;
|
||||
@@ -10,12 +10,11 @@ import axios from "axios"
|
||||
import InitialLoginStep from "@app/components/login/InitialLoginStep";
|
||||
import MFAStep from "@app/components/login/MFAStep";
|
||||
import PasswordInputStep from "@app/components/login/PasswordInputStep";
|
||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||
import { useProviderAuth } from "@app/hooks/useProviderAuth";
|
||||
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
|
||||
|
||||
import { fetchUserDetails } from "~/hooks/api/users/queries";
|
||||
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
import getOrganizations from "./api/organization/getOrgs";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
@@ -44,10 +43,10 @@ export default function Login() {
|
||||
useEffect(() => {
|
||||
// TODO(akhilmhdh): workspace will be controlled by a workspace context
|
||||
const redirectToDashboard = async () => {
|
||||
let userWorkspace;
|
||||
try {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
|
||||
const userOrgs = await getOrganizations();
|
||||
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
|
||||
const userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||
|
||||
// user details
|
||||
const userDetails = await fetchUserDetails()
|
||||
@@ -62,7 +61,7 @@ export default function Login() {
|
||||
const instance = axios.create()
|
||||
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
|
||||
}
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
|
||||
import getOrganizationUserProjects from "./api/organization/GetOrgUserProjects";
|
||||
|
||||
export default function NoProjects() {
|
||||
const router = useRouter();
|
||||
|
||||
const redirectUser = async () => {
|
||||
const workspaces = await getOrganizationUserProjects({
|
||||
orgId: String(localStorage.getItem("orgData.id"))
|
||||
});
|
||||
if (workspaces.length > 0) {
|
||||
router.push(`/dashboard/${workspaces[0]._id}`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// on initial load - run auth check
|
||||
(async () => {
|
||||
await redirectUser();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mr-auto flex h-full w-11/12 flex-col items-center justify-center text-center text-lg text-gray-300">
|
||||
<Head>
|
||||
<title>No Projects | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<div className="mb-6 mt-16 mr-16">
|
||||
<Image src="/images/dragon-cant-find.svg" height={270} width={436} alt="google logo" />
|
||||
</div>
|
||||
<div className="mb-8 rounded-md bg-mineshaft-900 border border-mineshaft-700 px-4 py-6 text-bunker-300 shadow-xl">
|
||||
<div className="max-w-md">
|
||||
You are not part of any projects in this organization yet. When you are, they will appear
|
||||
here.
|
||||
</div>
|
||||
<div className="mt-4 max-w-md">
|
||||
Create a new project, or ask other organization members to give you necessary permissions.
|
||||
</div>
|
||||
<div className="mx-2 mt-6">
|
||||
<Button text="Check again" onButtonPressed={redirectUser} size="md" color="mineshaft" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoProjects.requireAuth = true;
|
||||
+1
@@ -11,6 +11,7 @@ export default function SettingsBilling() {
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<BillingSettingsPage />
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MembersPage } from "@app/views/Org/MembersPage";
|
||||
|
||||
export default function SettingsOrg() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MembersPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsOrg.requireAuth = true;
|
||||
@@ -0,0 +1,512 @@
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faArrowRight, faCheckCircle, faHandPeace, faMagnifyingGlass, faNetworkWired, faPlug, faPlus, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import { Button, Checkbox, FormControl, Input, Modal, ModalContent, UpgradePlanModal } from "@app/components/v2";
|
||||
import { TabsObject } from "@app/components/v2/Tabs";
|
||||
import { useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
|
||||
import registerUserAction from "../../../api/userActions/registerUserAction";
|
||||
|
||||
const features = [{
|
||||
"_id": 0,
|
||||
"name": "Kubernetes Operator",
|
||||
"description": "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes."
|
||||
}]
|
||||
|
||||
type ItemProps = {
|
||||
text: string;
|
||||
subText: string;
|
||||
complete: boolean;
|
||||
icon: IconProp;
|
||||
time: string;
|
||||
userAction?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const learningItem = ({
|
||||
text,
|
||||
subText,
|
||||
complete,
|
||||
icon,
|
||||
time,
|
||||
userAction,
|
||||
link
|
||||
}: ItemProps): JSX.Element => {
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
target={`${link.includes("https") ? "_blank" : "_self"}`}
|
||||
rel="noopener noreferrer"
|
||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||
href={link}
|
||||
>
|
||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} mb-3 rounded-md`}>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction && userAction !== "first_time_secrets_pushed") {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-32 pr-8 text-right text-sm font-semibold ${complete && "text-primary"}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{/* {complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />} */}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction) {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500"
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-11 top-10 h-7 w-7 rounded-full bg-bunker-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="absolute left-12 top-16 h-5 w-5 text-4xl text-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="mt-0.5 text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const learningItemSquare = ({
|
||||
text,
|
||||
subText,
|
||||
complete,
|
||||
icon,
|
||||
time,
|
||||
userAction,
|
||||
link
|
||||
}: ItemProps): JSX.Element => {
|
||||
return (
|
||||
<a
|
||||
target={`${link?.includes("https") ? "_blank" : "_self"}`}
|
||||
rel="noopener noreferrer"
|
||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||
href={link}
|
||||
>
|
||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} rounded-md w-full`}>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction && userAction !== "first_time_secrets_pushed") {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
||||
>
|
||||
<div className="flex flex-col items-center w-full px-6 py-4">
|
||||
<div className="flex flex-row items-start justify-between w-full">
|
||||
<FontAwesomeIcon icon={icon} className="w-16 text-5xl text-mineshaft-200 group-hover:text-mineshaft-100 duration-100 pt-2" />
|
||||
{complete && (
|
||||
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "text-primary font-semibold" : ""}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start w-full pt-4">
|
||||
<div className="mt-0.5 text-lg font-medium">{text}</div>
|
||||
<div className="text-sm font-normal text-mineshaft-300">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("Project Name").trim(),
|
||||
addMembers: yup.bool().required().label("Add Members")
|
||||
});
|
||||
|
||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
// #TODO: Update all the workspaceIds
|
||||
|
||||
export default function Organization() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaces } = useWorkspace();
|
||||
const orgWorkspaces = workspaces?.filter(workspace => workspace.organization === localStorage.getItem("orgData.id")) || []
|
||||
const currentOrg = String(router.query.id);
|
||||
const { createNotification } = useNotificationContext();
|
||||
const addWsUser = useAddUserToWs();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm<TAddProjectFormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
|
||||
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
||||
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
||||
const [usersInOrg, setUsersInOrg] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const createWs = useCreateWorkspace();
|
||||
const { user } = useUser();
|
||||
const uploadWsKey = useUploadWsKey();
|
||||
|
||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
workspace: { _id: newWorkspaceId }
|
||||
}
|
||||
} = await createWs.mutateAsync({
|
||||
organizationId: currentOrg,
|
||||
workspaceName: name
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadWsKey.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
nonce,
|
||||
userId: user?._id,
|
||||
workspaceId: newWorkspaceId
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
// not using hooks because need at this point only
|
||||
const orgUsers = await fetchOrgUsers(currentOrg);
|
||||
orgUsers.forEach(({ status, user: orgUser }) => {
|
||||
// skip if status of org user is not accepted
|
||||
// this orgUser is the person who created the ws
|
||||
if (status !== "accepted" || user.email === orgUser.email) return;
|
||||
addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId });
|
||||
});
|
||||
}
|
||||
createNotification({ text: "Workspace created", type: "success" });
|
||||
handlePopUpClose("addNewWs");
|
||||
router.push(`/project/${newWorkspaceId}/secrets`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
||||
|
||||
useEffect(() => {
|
||||
onboardingCheck({
|
||||
setHasUserClickedIntro,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserPushedSecrets,
|
||||
setUsersInOrg
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||
<div className="w-full flex flex-row mt-6">
|
||||
<Input
|
||||
className="h-[2.3rem] text-sm bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by project name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isAddingProjectsAllowed) {
|
||||
handlePopUpOpen("addNewWs")
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
className="ml-2"
|
||||
>
|
||||
Add New Project
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 w-full grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
||||
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div>
|
||||
<div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div>
|
||||
<Link href={`/project/${workspace._id}/secrets`}>
|
||||
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div>
|
||||
</Link>
|
||||
</div>)}
|
||||
</div>
|
||||
{orgWorkspaces.length === 0 && (
|
||||
<div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" />
|
||||
<div className="text-center font-light">
|
||||
You are not part of any projects in this organization yet. When you are, they will appear
|
||||
here.
|
||||
</div>
|
||||
<div className="mt-0.5 text-center font-light">
|
||||
Create a new project, or ask other organization members to give you necessary permissions.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
||||
<p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 w-full mb-3">
|
||||
{learningItemSquare({
|
||||
text: "Watch Infisical demo",
|
||||
subText: "Set up Infisical in 3 min.",
|
||||
complete: hasUserClickedIntro,
|
||||
icon: faHandPeace,
|
||||
time: "3 min",
|
||||
userAction: "intro_cta_clicked",
|
||||
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
||||
})}
|
||||
{orgWorkspaces.length !== 0 && learningItemSquare({
|
||||
text: "Add your secrets",
|
||||
subText: "Drop a .env file or type your secrets.",
|
||||
complete: hasUserPushedSecrets,
|
||||
icon: faPlus,
|
||||
time: "1 min",
|
||||
userAction: "first_time_secrets_pushed",
|
||||
link: `/project/${orgWorkspaces[0]?._id}/secrets`
|
||||
})}
|
||||
{learningItemSquare({
|
||||
text: "Invite your teammates",
|
||||
subText: "Infisical is better used as a team.",
|
||||
complete: usersInOrg,
|
||||
icon: faUserPlus,
|
||||
time: "2 min",
|
||||
link: `/org/${router.query.id}/members?action=invite`
|
||||
})}
|
||||
<div className="block xl:hidden 2xl:block">{learningItemSquare({
|
||||
text: "Join Infisical Slack",
|
||||
subText: "Have any questions? Ask us!",
|
||||
complete: hasUserClickedSlack,
|
||||
icon: faSlack,
|
||||
time: "1 min",
|
||||
userAction: "slack_cta_clicked",
|
||||
link: "https://infisical.com/slack"
|
||||
})}</div>
|
||||
</div>
|
||||
{orgWorkspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-green" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>}
|
||||
{orgWorkspaces.length !== 0 && learningItem({
|
||||
text: "Integrate Infisical with your infrastructure",
|
||||
subText: "Connect Infisical to various 3rd party services and platforms.",
|
||||
complete: false,
|
||||
icon: faPlug,
|
||||
time: "15 min",
|
||||
link: "https://infisical.com/docs/integrations/overview"
|
||||
})}
|
||||
</div>}
|
||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6">
|
||||
<p className="mr-4 font-semibold text-white">Explore More</p>
|
||||
<div className="mt-4 w-full grid grid-flow-dense gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}>
|
||||
{features.map(feature => <div key={feature._id} className="h-44 w-96 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
||||
<div className="text-lg text-mineshaft-100 mt-0">{feature.name}</div>
|
||||
<div className="text-[15px] font-light text-mineshaft-300 mb-4 mt-2">{feature.description}</div>
|
||||
<div className="w-full flex items-center">
|
||||
<div className="text-mineshaft-300 text-[15px] font-light">Setup time: 20 min</div>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80"
|
||||
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
|
||||
>
|
||||
Learn more <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isModalOpen) => {
|
||||
handlePopUpToggle("addNewWs", isModalOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create a new project"
|
||||
subTitle="This project will contain your secrets and configurations."
|
||||
>
|
||||
<form onSubmit={handleSubmit(onCreateProject)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Type your project name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-4 pl-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="addMembers"
|
||||
defaultValue
|
||||
render={({ field: { onBlur, value, onChange } }) => (
|
||||
<Checkbox
|
||||
id="add-project-layout"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
Add all members of my organization to this project
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Project
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => handlePopUpClose("addNewWs")}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You have exceeded the number of projects allowed on the free plan."
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Organization.requireAuth = true;
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||
|
||||
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
|
||||
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
|
||||
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter()
|
||||
const queryParams = router.query
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||
|
||||
useEffect(()=>{
|
||||
const linkInstallation = async () => {
|
||||
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
|
||||
try {
|
||||
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
|
||||
if (isLinked){
|
||||
router.reload()
|
||||
}
|
||||
|
||||
console.log("installation verification complete")
|
||||
}catch (e){
|
||||
console.log("app installation is stale, start new session", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInstallationStatus = async () => {
|
||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||
setIntegrationStatus(status)
|
||||
}
|
||||
|
||||
fetchInstallationStatus()
|
||||
linkInstallation()
|
||||
},[queryParams.state, queryParams.installation_id])
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
|
||||
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div>
|
||||
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
|
||||
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
|
||||
</div>
|
||||
{integrationEnabled ? (
|
||||
<div>
|
||||
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center h-[3.25rem]">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={generateNewIntegrationSession}
|
||||
className="py-2 h-min"
|
||||
>
|
||||
Integrate with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SecretScanningLogsTable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SecretScanning.requireAuth = true;
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FormEvent, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -24,6 +24,7 @@ const client = new jsrp.client();
|
||||
export default function PasswordReset() {
|
||||
const [verificationToken, setVerificationToken] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [backupKey, setBackupKey] = useState("");
|
||||
const [privateKey, setPrivateKey] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
@@ -38,7 +39,8 @@ export default function PasswordReset() {
|
||||
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
|
||||
|
||||
// Unencrypt the private key with a backup key
|
||||
const getEncryptedKeyHandler = async () => {
|
||||
const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
||||
|
||||
@@ -57,7 +59,8 @@ export default function PasswordReset() {
|
||||
};
|
||||
|
||||
// If everything is correct, reset the password
|
||||
const resetPasswordHandler = async () => {
|
||||
const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const errorCheck = passwordCheck({
|
||||
password: newPassword,
|
||||
setPasswordErrorLength,
|
||||
@@ -125,6 +128,7 @@ export default function PasswordReset() {
|
||||
if (response?.status === 200) {
|
||||
router.push("/login");
|
||||
}
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -162,7 +166,7 @@ export default function PasswordReset() {
|
||||
|
||||
// Input backup key
|
||||
const stepInputBackupKey = (
|
||||
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<form onSubmit={getEncryptedKeyHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Enter your backup key
|
||||
</p>
|
||||
@@ -186,18 +190,19 @@ export default function PasswordReset() {
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text="Submit Backup Key"
|
||||
onButtonPressed={() => getEncryptedKeyHandler()}
|
||||
onButtonPressed={() => {}}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
// Enter new password
|
||||
const stepEnterNewPassword = (
|
||||
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<form onSubmit={resetPasswordHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Enter new password
|
||||
</p>
|
||||
@@ -269,13 +274,15 @@ export default function PasswordReset() {
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text="Submit New Password"
|
||||
onButtonPressed={() => resetPasswordHandler()}
|
||||
onButtonPressed={() => setLoading(true)}
|
||||
size="lg"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { PersonalSettingsPage } from "@app/views/Settings/PersonalSettingsPage";
|
||||
|
||||
export default function PersonalSettings() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 text-white h-full">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<PersonalSettingsPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PersonalSettings.requireAuth = true;
|
||||
+5
-7
@@ -5,14 +5,13 @@ import { useRouter } from "next/router";
|
||||
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
import EventFilter from "@app/components/basic/EventFilter";
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { UpgradePlanModal } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import ActivitySideBar from "@app/ee/components/ActivitySideBar";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import getProjectLogs from "../../ee/api/secrets/GetProjectLogs";
|
||||
import ActivityTable from "../../ee/components/ActivityTable";
|
||||
import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs";
|
||||
import ActivityTable from "../../../../ee/components/ActivityTable";
|
||||
|
||||
interface LogData {
|
||||
_id: string;
|
||||
@@ -152,18 +151,17 @@ export default function Activity() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-6 lg:mx-0 w-full h-full">
|
||||
<div className="mx-auto w-full h-full max-w-7xl">
|
||||
<Head>
|
||||
<title>Audit Logs</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<NavHeader pageName="Audit Logs" isProjectRelated />
|
||||
{currentSidebarAction && (
|
||||
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
|
||||
)}
|
||||
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl">
|
||||
<div className="flex flex-col justify-between items-start mx-4 mb-4 text-xl px-2">
|
||||
<div className="flex flex-row justify-start items-center text-3xl mt-6">
|
||||
<p className="font-semibold mr-4 text-bunker-100">{t("activity.title")}</p>
|
||||
</div>
|
||||
<p className="mr-4 text-base text-gray-400">{t("activity.subtitle")}</p>
|
||||
+9
-11
@@ -9,20 +9,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMemberDialog";
|
||||
import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable";
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import guidGenerator from "@app/components/utilities/randomId";
|
||||
import { Input } from "@app/components/v2";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "../../components/utilities/cryptography/crypto";
|
||||
import getOrganizationUsers from "../api/organization/GetOrgUsers";
|
||||
import getUser from "../api/user/getUser";
|
||||
} from "../../../../components/utilities/cryptography/crypto";
|
||||
import getOrganizationUsers from "../../../api/organization/GetOrgUsers";
|
||||
import getUser from "../../../api/user/getUser";
|
||||
// import DeleteUserDialog from '@app/components/basic/dialog/DeleteUserDialog';
|
||||
import addUserToWorkspace from "../api/workspace/addUserToWorkspace";
|
||||
import getWorkspaceUsers from "../api/workspace/getWorkspaceUsers";
|
||||
import uploadKeys from "../api/workspace/uploadKeys";
|
||||
import addUserToWorkspace from "../../../api/workspace/addUserToWorkspace";
|
||||
import getWorkspaceUsers from "../../../api/workspace/getWorkspaceUsers";
|
||||
import uploadKeys from "../../../api/workspace/uploadKeys";
|
||||
|
||||
interface UserProps {
|
||||
firstName: string;
|
||||
@@ -149,12 +148,11 @@ export default function Users() {
|
||||
};
|
||||
|
||||
return userList ? (
|
||||
<div className="flex max-w-[calc(100vw-240px)] flex-col justify-start bg-bunker-800 md:h-screen">
|
||||
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<NavHeader pageName={t("settings.members.title")} isProjectRelated />
|
||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
||||
<p className="mr-4 font-semibold text-white">{t("settings.members.title")}</p>
|
||||
</div>
|
||||
@@ -172,7 +170,7 @@ export default function Users() {
|
||||
setEmail={setEmail}
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
<div className="absolute right-4 top-36 flex w-full flex-row items-start px-6 pb-1">
|
||||
<div className="flex w-full flex-row items-start px-6 pb-1">
|
||||
<div className="flex w-full max-w-sm flex flex-row ml-auto">
|
||||
<Input
|
||||
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
@@ -192,7 +190,7 @@ export default function Users() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="no-scrollbar::-webkit-scrollbar block overflow-x-scroll px-6 pb-6 no-scrollbar">
|
||||
<div className="block overflow-x-auto px-6 pb-6">
|
||||
<ProjectUsersTable
|
||||
userData={userList}
|
||||
changeData={setUserList}
|
||||
@@ -8,7 +8,7 @@ import { useRouter } from "next/router";
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { isLoggedIn } from "@app/reactQuery";
|
||||
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
import getOrganizations from "./api/organization/getOrgs";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
@@ -18,11 +18,11 @@ export default function Login() {
|
||||
useEffect(() => {
|
||||
// TODO(akhilmhdh): workspace will be controlled by a workspace context
|
||||
const redirectToDashboard = async () => {
|
||||
let userWorkspace;
|
||||
let userOrg;
|
||||
try {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
userOrg = userOrgs[0] && userOrgs[0]._id;
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(()=>{
|
||||
router.push(`${router.asPath.split("secret-scanning")[0]}/org/${localStorage.getItem("orgData.id")}/secret-scanning${router.asPath.split("secret-scanning")[1]}`)
|
||||
}, [])
|
||||
|
||||
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
SecretScanning.requireAuth = true;
|
||||
@@ -1,324 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { faBan,faCheck, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
import InputField from "@app/components/basic/InputField";
|
||||
import ListBox from "@app/components/basic/Listbox";
|
||||
import ApiKeyTable from "@app/components/basic/table/ApiKeyTable";
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import checkPassword from "@app/components/utilities/checks/checkPassword";
|
||||
import changePassword from "@app/components/utilities/cryptography/changePassword";
|
||||
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
|
||||
import {
|
||||
useGetCommonPasswords,
|
||||
useRevokeAllSessions} from "@app/hooks/api";
|
||||
import { SecuritySection } from "@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection";
|
||||
|
||||
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
|
||||
import getAPIKeys from "../../api/apiKey/getAPIKeys";
|
||||
import getUser from "../../api/user/getUser";
|
||||
|
||||
type Errors = {
|
||||
length?: string,
|
||||
upperCase?: string,
|
||||
lowerCase?: string,
|
||||
number?: string,
|
||||
specialChar?: string,
|
||||
repeatedChar?: string,
|
||||
};
|
||||
|
||||
export default function PersonalSettings() {
|
||||
const { data: commonPasswords } = useGetCommonPasswords();
|
||||
const [personalEmail, setPersonalEmail] = useState("");
|
||||
const [personalName, setPersonalName] = useState("");
|
||||
const [currentPasswordError, setCurrentPasswordError] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [backupPassword, setBackupPassword] = useState("");
|
||||
const [passwordChanged, setPasswordChanged] = useState(false);
|
||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
|
||||
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
||||
const [errors, setErrors] = useState<Errors>({});
|
||||
|
||||
const revokeAllSessions = useRevokeAllSessions();
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const router = useRouter();
|
||||
const lang = router.locale ?? "en";
|
||||
|
||||
const setLanguage = async (to: string) => {
|
||||
router.push(router.asPath, router.asPath, { locale: to });
|
||||
localStorage.setItem("lang", to);
|
||||
i18n.changeLanguage(to);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
try {
|
||||
const user = await getUser();
|
||||
setApiKeys(await getAPIKeys());
|
||||
setPersonalEmail(user.email);
|
||||
setPersonalName(`${user.firstName} ${user.lastName}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const closeAddApiKeyModal = () => {
|
||||
setIsAddApiKeyDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex max-h-screen flex-col justify-between bg-bunker-800 text-white">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<AddApiKeyDialog
|
||||
isOpen={isAddApiKeyDialogOpen}
|
||||
closeModal={closeAddApiKeyModal}
|
||||
apiKeys={apiKeys}
|
||||
setApiKeys={setApiKeys}
|
||||
/>
|
||||
<div className="flex flex-row">
|
||||
<div className="max-h-screen w-full pb-2">
|
||||
<NavHeader pageName={t("settings.personal.title")} isProjectRelated={false} />
|
||||
<div className="ml-6 mt-8 mb-6 flex max-w-5xl flex-row items-center justify-between text-xl">
|
||||
<div className="flex flex-col items-start justify-start text-3xl">
|
||||
<p className="mr-4 font-semibold text-gray-200">{t("settings.personal.title")}</p>
|
||||
<p className="mr-4 text-base font-normal text-gray-400">
|
||||
{t("settings.personal.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-6 mr-6 flex max-w-5xl flex-col text-mineshaft-50">
|
||||
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
|
||||
<p className="self-start text-xl font-semibold">
|
||||
{t("settings.personal.change-language")}
|
||||
</p>
|
||||
<div className="w-ful mt-4 max-h-28">
|
||||
<ListBox
|
||||
isSelected={lang}
|
||||
onChange={setLanguage}
|
||||
data={["en", "ko", "fr", "es"]}
|
||||
text={`${t("common.language")}: `}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SecuritySection />
|
||||
<div className="mt-2 mb-8 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-4">
|
||||
<div className="flex w-full flex-row justify-between">
|
||||
<div className="flex w-full flex-col">
|
||||
<p className="mb-3 text-xl font-semibold">
|
||||
{t("settings.personal.api-keys.title")}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t("settings.personal.api-keys.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2 w-40">
|
||||
<Button
|
||||
text={String(t("settings.personal.api-keys.add-new"))}
|
||||
onButtonPressed={() => {
|
||||
setIsAddApiKeyDialogOpen(true);
|
||||
}}
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ApiKeyTable data={apiKeys} setApiKeys={setApiKeys as any} />
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
|
||||
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
||||
<div className="flex w-full max-w-3xl flex-col justify-between">
|
||||
<p className="mb-3 min-w-max text-xl font-semibold">
|
||||
{t("section.password.change")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full max-w-xl">
|
||||
<InputField
|
||||
label={t("section.password.current") as string}
|
||||
onChangeHandler={(password) => {
|
||||
setCurrentPassword(password);
|
||||
}}
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
isRequired
|
||||
error={currentPasswordError}
|
||||
errorText={t("section.password.current-wrong") as string}
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="py-2" />
|
||||
<InputField
|
||||
label={t("section.password.new") as string}
|
||||
onChangeHandler={(password) => {
|
||||
setNewPassword(password);
|
||||
checkPassword({
|
||||
password,
|
||||
commonPasswords,
|
||||
setErrors
|
||||
});
|
||||
}}
|
||||
type="password"
|
||||
value={newPassword}
|
||||
isRequired
|
||||
error={Object.keys(errors).length > 0}
|
||||
autoComplete="new-password"
|
||||
id="new-password"
|
||||
/>
|
||||
</div>
|
||||
{Object.keys(errors).length > 0 && (
|
||||
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
|
||||
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
|
||||
{Object.keys(errors).map((key) => {
|
||||
if (errors[key as keyof Errors]) {
|
||||
return (
|
||||
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
|
||||
<div>
|
||||
<FontAwesomeIcon
|
||||
icon={faXmark}
|
||||
className="text-md text-red ml-0.5 mr-2.5"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm">
|
||||
{errors[key as keyof Errors]}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex w-52 flex-row items-center pr-3">
|
||||
<Button
|
||||
text={t("section.password.change") as string}
|
||||
onButtonPressed={() => {
|
||||
const errorCheck = checkPassword({
|
||||
password: newPassword,
|
||||
commonPasswords,
|
||||
setErrors
|
||||
});
|
||||
if (!errorCheck) {
|
||||
changePassword(
|
||||
personalEmail,
|
||||
currentPassword,
|
||||
newPassword,
|
||||
setCurrentPasswordError,
|
||||
setPasswordChanged,
|
||||
setCurrentPassword,
|
||||
setNewPassword
|
||||
);
|
||||
}
|
||||
}}
|
||||
active={Object.keys(errors).length === 0 && currentPassword !== ""}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
textDisabled={t("section.password.change") as string}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className={`ml-4 text-3xl text-primary ${
|
||||
passwordChanged ? "opacity-100" : "opacity-0"
|
||||
} duration-300`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
|
||||
<div className="my-4 flex w-full flex-row justify-between">
|
||||
<p className="text-xl font-semibold w-full">
|
||||
Sessions
|
||||
</p>
|
||||
<div className="w-40">
|
||||
<Button
|
||||
text="Revoke all"
|
||||
onButtonPressed={async () => {
|
||||
await revokeAllSessions.mutateAsync();
|
||||
router.push("/login");
|
||||
}}
|
||||
color="mineshaft"
|
||||
icon={faBan}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mb-5 text-sm text-mineshaft-300">
|
||||
Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your account out all active sessions across all browsers and CLIs.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
|
||||
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
|
||||
<div className="flex w-full max-w-3xl flex-col justify-between">
|
||||
<p className="mb-3 min-w-max text-xl font-semibold">
|
||||
{t("settings.personal.emergency.name")}
|
||||
</p>
|
||||
<p className="min-w-max text-sm text-mineshaft-300">
|
||||
{t("settings.personal.emergency.text1")}
|
||||
</p>
|
||||
<p className="mb-5 min-w-max text-sm text-mineshaft-300">
|
||||
{t("settings.personal.emergency.text2")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 w-full max-w-xl">
|
||||
<InputField
|
||||
label={t("section.password.current") as string}
|
||||
onChangeHandler={setBackupPassword}
|
||||
type="password"
|
||||
value={backupPassword}
|
||||
isRequired
|
||||
error={backupKeyError}
|
||||
errorText={t("section.password.current-wrong") as string}
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3 flex w-60 flex-row items-center">
|
||||
<Button
|
||||
text={t("settings.personal.emergency.download") as string}
|
||||
onButtonPressed={() => {
|
||||
issueBackupKey({
|
||||
email: personalEmail,
|
||||
password: backupPassword,
|
||||
personalName,
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={backupPassword !== ""}
|
||||
textDisabled={t("settings.personal.emergency.download") as string}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faCheck}
|
||||
className={`ml-4 text-3xl text-primary ${
|
||||
backupKeyIssued ? "opacity-100" : "opacity-0"
|
||||
} duration-300`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
PersonalSettings.requireAuth = true;
|
||||
@@ -16,7 +16,7 @@ import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { useProviderAuth } from "@app/hooks/useProviderAuth";
|
||||
|
||||
import checkEmailVerificationCode from "./api/auth/CheckEmailVerificationCode";
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
import getOrganizations from "./api/organization/getOrgs";
|
||||
|
||||
/**
|
||||
* @returns the signup page
|
||||
@@ -48,8 +48,8 @@ export default function SignUp() {
|
||||
useEffect(() => {
|
||||
const tryAuth = async () => {
|
||||
try {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
router.push(`/org/${userOrgs[0]._id}/overview`);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
@@ -90,8 +90,8 @@ export default function SignUp() {
|
||||
}
|
||||
|
||||
if (!serverDetails?.emailConfigured && step === 5) {
|
||||
getWorkspaces().then((userWorkspaces) => {
|
||||
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||
getOrganizations().then((userOrgs) => {
|
||||
router.push(`/org/${userOrgs[0]._id}/overview`);
|
||||
});
|
||||
}
|
||||
}, [step]);
|
||||
|
||||
@@ -46,6 +46,7 @@ type Errors = {
|
||||
|
||||
export default function SignupInvite() {
|
||||
const { data: commonPasswords } = useGetCommonPasswords();
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
@@ -212,7 +213,7 @@ export default function SignupInvite() {
|
||||
} else {
|
||||
// user will be redirected to dashboard
|
||||
// if not logged in gets kicked out to login
|
||||
router.push("/dashboard");
|
||||
router.push(`/org/${organizationId}/overview`);
|
||||
}
|
||||
} else {
|
||||
console.log("ERROR", response);
|
||||
@@ -338,22 +339,10 @@ export default function SignupInvite() {
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
router.push("/noprojects/");
|
||||
router.push(`/org/${organizationId}/overview`);
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
{/* <div
|
||||
className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer"
|
||||
onClick={() => {
|
||||
if (localStorage.getItem("projectData.id")) {
|
||||
router.push("/dashboard/" + localStorage.getItem("projectData.id"));
|
||||
} else {
|
||||
router.push("/noprojects")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Later
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { FormEvent, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -12,6 +12,7 @@ import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import SendEmailOnPasswordReset from "./api/auth/SendEmailOnPasswordReset";
|
||||
|
||||
export default function VerifyEmail() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
@@ -27,6 +28,18 @@ export default function VerifyEmail() {
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (serverDetails?.emailConfigured) {
|
||||
sendVerificationEmail();
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6">
|
||||
<Head>
|
||||
@@ -45,7 +58,7 @@ export default function VerifyEmail() {
|
||||
</div>
|
||||
</Link>
|
||||
{step === 1 && (
|
||||
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl">
|
||||
<form onSubmit={onSubmit} className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Forgot your password?
|
||||
</p>
|
||||
@@ -67,20 +80,10 @@ export default function VerifyEmail() {
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button
|
||||
text="Continue"
|
||||
onButtonPressed={() => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
sendVerificationEmail();
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
<Button type="submit" text="Continue" size="lg" onButtonPressed={() => {}} loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { Button, Input, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useGetProjectFoldersBatch,
|
||||
useGetProjectSecretsByKey,
|
||||
@@ -22,6 +22,7 @@ export const DashboardEnvOverview = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const workspaceId = currentWorkspace?._id as string;
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
@@ -29,7 +30,7 @@ export const DashboardEnvOverview = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !workspaceId && router.isReady) {
|
||||
router.push("/noprojects");
|
||||
router.push(`/org/${currentOrg?._id}/overview`);
|
||||
}
|
||||
}, [isLoading, workspaceId, router.isReady]);
|
||||
|
||||
@@ -143,7 +144,7 @@ export const DashboardEnvOverview = () => {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto max-w-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||
<div className="relative right-5">
|
||||
<div className="relative right-5 ml-4">
|
||||
<NavHeader pageName={t("dashboard.title")} isProjectRelated />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
|
||||
@@ -3,6 +3,19 @@ import { FormProvider, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
KeyboardSensor,
|
||||
MouseSensor,
|
||||
TouchSensor,
|
||||
useSensor,
|
||||
useSensors
|
||||
} from "@dnd-kit/core";
|
||||
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import {
|
||||
faAngleDown,
|
||||
faArrowLeft,
|
||||
faCheck,
|
||||
faClockRotateLeft,
|
||||
@@ -10,12 +23,14 @@ import {
|
||||
faDownload,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFileImport,
|
||||
faFolderPlus,
|
||||
faMagnifyingGlass,
|
||||
faPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
@@ -36,15 +51,19 @@ import {
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { leaveConfirmDefaultMessage } from "@app/const";
|
||||
import { useSubscription,useWorkspace } from "@app/context";
|
||||
import { useOrganization, useSubscription, useWorkspace } from "@app/context";
|
||||
import { useLeaveConfirm, usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useBatchSecretsOp,
|
||||
useCreateFolder,
|
||||
useCreateSecretImport,
|
||||
useCreateWsTag,
|
||||
useDeleteFolder,
|
||||
useDeleteSecretImport,
|
||||
useGetImportedSecrets,
|
||||
useGetProjectFolders,
|
||||
useGetProjectSecrets,
|
||||
useGetSecretImports,
|
||||
useGetSecretVersion,
|
||||
useGetSnapshotSecrets,
|
||||
useGetUserAction,
|
||||
@@ -55,7 +74,8 @@ import {
|
||||
useGetWsTags,
|
||||
usePerformSecretRollback,
|
||||
useRegisterUserAction,
|
||||
useUpdateFolder
|
||||
useUpdateFolder,
|
||||
useUpdateSecretImport
|
||||
} from "@app/hooks/api";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { WorkspaceEnv } from "@app/hooks/api/types";
|
||||
@@ -71,6 +91,8 @@ import {
|
||||
import { PitDrawer } from "./components/PitDrawer";
|
||||
import { SecretDetailDrawer } from "./components/SecretDetailDrawer";
|
||||
import { SecretDropzone } from "./components/SecretDropzone";
|
||||
import { SecretImportForm } from "./components/SecretImportForm";
|
||||
import { SecretImportSection } from "./components/SecretImportSection";
|
||||
import { SecretInputRow } from "./components/SecretInputRow";
|
||||
import { SecretTableHeader } from "./components/SecretTableHeader";
|
||||
import {
|
||||
@@ -84,7 +106,7 @@ import {
|
||||
} from "./DashboardPage.utils";
|
||||
|
||||
const USER_ACTION_PUSH = "first_time_secrets_pushed";
|
||||
|
||||
type TDeleteSecretImport = { environment: string; secretPath: string };
|
||||
/*
|
||||
* Some imp aspects to consider. Here there are multiple stats changing
|
||||
* Thus ideally we need to use a context. But instead we rely on react hook form
|
||||
@@ -113,7 +135,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
"compareSecrets",
|
||||
"folderForm",
|
||||
"deleteFolder",
|
||||
"upgradePlan"
|
||||
"upgradePlan",
|
||||
"addSecretImport",
|
||||
"deleteSecretImport"
|
||||
] as const);
|
||||
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
@@ -127,12 +151,15 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const isRollbackMode = Boolean(snapshotId);
|
||||
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const workspaceId = currentWorkspace?._id as string;
|
||||
const selectedEnvSlug = selectedEnv?.slug || "";
|
||||
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !workspaceId && router.isReady) {
|
||||
router.push("/noprojects");
|
||||
router.push(`/org/${currentOrg?._id}/overview`);
|
||||
}
|
||||
}, [isLoading, workspaceId, router.isReady]);
|
||||
|
||||
@@ -160,7 +187,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
|
||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||
workspaceId,
|
||||
env: selectedEnv?.slug || "",
|
||||
env: selectedEnvSlug,
|
||||
decryptFileKey: latestFileKey!,
|
||||
isPaused: Boolean(snapshotId),
|
||||
folderId
|
||||
@@ -168,7 +195,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
|
||||
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
|
||||
workspaceId: workspaceId || "",
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
parentFolderId: folderId,
|
||||
isPaused: isRollbackMode,
|
||||
sortDir
|
||||
@@ -181,7 +208,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
isFetchingNextPage
|
||||
} = useGetWorkspaceSecretSnapshots({
|
||||
workspaceId,
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
folder: folderId,
|
||||
limit: 10
|
||||
});
|
||||
@@ -192,17 +219,18 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
isFetching: isSnapshotChanging
|
||||
} = useGetSnapshotSecrets({
|
||||
snapshotId: snapshotId || "",
|
||||
env: selectedEnv?.slug || "",
|
||||
env: selectedEnvSlug,
|
||||
decryptFileKey: latestFileKey!
|
||||
});
|
||||
|
||||
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
|
||||
workspaceId,
|
||||
selectedEnv?.slug || "",
|
||||
selectedEnvSlug,
|
||||
folderId
|
||||
);
|
||||
|
||||
const { data: wsTags } = useGetWsTags(workspaceId);
|
||||
|
||||
// mutation calls
|
||||
const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
|
||||
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
|
||||
@@ -212,6 +240,50 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
|
||||
const { mutateAsync: deleteFolder } = useDeleteFolder(folderId);
|
||||
|
||||
const { data: secretImportCfg, isFetching: isSecretImportCfgFetching } = useGetSecretImports(
|
||||
workspaceId,
|
||||
selectedEnvSlug,
|
||||
folderId
|
||||
);
|
||||
|
||||
const { data: importedSecrets } = useGetImportedSecrets({
|
||||
workspaceId,
|
||||
decryptFileKey: latestFileKey!,
|
||||
environment: selectedEnvSlug,
|
||||
folderId
|
||||
});
|
||||
|
||||
// This is for dnd-kit. As react-query state mutation async
|
||||
// This will act as a placeholder to avoid a glitching animation on dropping items
|
||||
const [items, setItems] = useState<
|
||||
Array<{ environment: string; secretPath: string; id: string }>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isSecretImportCfgFetching ||
|
||||
// case in which u go to a folder and come back to fill in with cache data
|
||||
(items.length === 0 && secretImportCfg?.imports?.length !== 0 && isSecretImportCfgFetching)
|
||||
) {
|
||||
setItems(
|
||||
secretImportCfg?.imports?.map((el) => ({
|
||||
...el,
|
||||
id: `${el.environment}-${el.secretPath}`
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
}, [isSecretImportCfgFetching]);
|
||||
|
||||
const { mutateAsync: createSecretImport } = useCreateSecretImport();
|
||||
const { mutate: updateSecretImportSync } = useUpdateSecretImport();
|
||||
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {}),
|
||||
useSensor(TouchSensor, {}),
|
||||
useSensor(KeyboardSensor, {})
|
||||
);
|
||||
|
||||
const method = useForm<FormData>({
|
||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||
defaultValues: secrets as any,
|
||||
@@ -318,14 +390,12 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
await performSecretRollback({
|
||||
workspaceId,
|
||||
version: snapshotSecret.version,
|
||||
environment: selectedEnv?.slug || "",
|
||||
environment: selectedEnvSlug,
|
||||
folderId
|
||||
});
|
||||
setValue("isSnapshotMode", false);
|
||||
setSnaphotId(null);
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || "")
|
||||
);
|
||||
queryClient.invalidateQueries(secretKeys.getProjectSecret(workspaceId, selectedEnvSlug));
|
||||
createNotification({
|
||||
text: "Successfully rollback secrets",
|
||||
type: "success"
|
||||
@@ -339,7 +409,10 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onAppendSecret = () => append(DEFAULT_SECRET_VALUE);
|
||||
const onAppendSecret = () => {
|
||||
setSearchFilter("");
|
||||
append(DEFAULT_SECRET_VALUE);
|
||||
};
|
||||
|
||||
const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => {
|
||||
if (isSnapshotMode) {
|
||||
@@ -360,6 +433,10 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
);
|
||||
// type check
|
||||
if (!selectedEnv?.slug) return;
|
||||
if (batchedSecret.length === 0) {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await batchSecretOp({
|
||||
requests: batchedSecret,
|
||||
@@ -514,6 +591,79 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretImportCreate = async (env: string, secretPath: string) => {
|
||||
try {
|
||||
await createSecretImport({
|
||||
workspaceId,
|
||||
environment: selectedEnv?.slug || "",
|
||||
folderId,
|
||||
secretImport: {
|
||||
environment: env,
|
||||
secretPath
|
||||
}
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully create secret link"
|
||||
});
|
||||
handlePopUpClose("addSecretImport");
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to create secret link",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSecretImportDelete = async () => {
|
||||
const { environment: importEnv, secretPath: impSecPath } = popUp.deleteSecretImport
|
||||
?.data as TDeleteSecretImport;
|
||||
try {
|
||||
if (secretImportCfg?._id) {
|
||||
await deleteSecretImport({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug,
|
||||
folderId,
|
||||
id: secretImportCfg?._id,
|
||||
secretImportEnv: importEnv,
|
||||
secretImportPath: impSecPath
|
||||
});
|
||||
handlePopUpClose("deleteSecretImport");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully removed secret link"
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to remove secret link",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (evt: DragEndEvent) => {
|
||||
const { active, over } = evt;
|
||||
if (over?.id && active.id !== over.id) {
|
||||
const oldIndex = items.findIndex(({ id }) => id === active.id);
|
||||
const newIndex = items.findIndex(({ id }) => id === over.id);
|
||||
const newImportOrder = arrayMove(items, oldIndex, newIndex);
|
||||
setItems(newImportOrder);
|
||||
updateSecretImportSync({
|
||||
workspaceId,
|
||||
environment: selectedEnvSlug,
|
||||
folderId,
|
||||
id: secretImportCfg?._id || "",
|
||||
secretImports: newImportOrder.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath
|
||||
}))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// when secrets is not loading and secrets list is empty
|
||||
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
||||
|
||||
@@ -526,7 +676,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
const isSnapshotSecretEmtpy =
|
||||
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
|
||||
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
|
||||
const isEmptyPage = isFoldersEmpty && isSecretEmpty;
|
||||
const isSecretImportEmpty = !importedSecrets?.length;
|
||||
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
|
||||
|
||||
if (isSecretsLoading || isEnvListLoading) {
|
||||
return (
|
||||
@@ -545,7 +696,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
<FormProvider {...method}>
|
||||
<form autoComplete="off" className="h-full">
|
||||
{/* breadcrumb row */}
|
||||
<div className="relative right-6 -top-2 mb-2">
|
||||
<div className="relative right-6 -top-2 mb-2 ml-6">
|
||||
<NavHeader
|
||||
pageName={t("dashboard.title")}
|
||||
currentEnv={
|
||||
@@ -632,7 +783,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
handlePopUpOpen("secretSnapshots");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
|
||||
@@ -644,54 +795,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
</Button>
|
||||
</div>
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<>
|
||||
<div className="block lg:hidden">
|
||||
<Tooltip content="Add Folder">
|
||||
<IconButton
|
||||
ariaLabel="recovery"
|
||||
variant="outline_bg"
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFolderPlus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="block lg:hidden">
|
||||
<Tooltip content="Point-in-time Recovery">
|
||||
<IconButton
|
||||
ariaLabel="recovery"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (secretContainer.current) {
|
||||
secretContainer.current.scroll({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:block">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!(isReadOnly || isRollbackMode)) {
|
||||
if (secretContainer.current) {
|
||||
secretContainer.current.scroll({
|
||||
top: 0,
|
||||
@@ -699,15 +807,49 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
});
|
||||
}
|
||||
prepend(DEFAULT_SECRET_VALUE, { shouldFocus: false });
|
||||
}}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
>
|
||||
Add Secret
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
setSearchFilter("");
|
||||
}
|
||||
}}
|
||||
className="font-semibold bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-l-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 pr-4 duration-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="px-2"/>Add Secret
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="data-[state=open]:bg-mineshaft-600">
|
||||
<div className="bg-mineshaft-600 border border-mineshaft-500 p-2 rounded-r-md text-sm text-mineshaft-300 cursor-pointer hover:bg-primary/[0.1] hover:border-primary/40 duration-200">
|
||||
<FontAwesomeIcon icon={faAngleDown} className="pr-2 pl-1.5"/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="mt-1 z-[60] left-20 w-[10.8rem]">
|
||||
<div className="bg-mineshaft-800 p-1 border border-mineshaft-600 rounded-md">
|
||||
<div className="w-full pb-1">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||
onClick={() => handlePopUpOpen("folderForm")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Folder
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faFileImport} />}
|
||||
onClick={() => handlePopUpOpen("addSecretImport")}
|
||||
isDisabled={isReadOnly || isRollbackMode}
|
||||
variant="outline_bg"
|
||||
className="h-10"
|
||||
isFullWidth
|
||||
>
|
||||
Add Import
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
{isRollbackMode && (
|
||||
<Button
|
||||
@@ -742,49 +884,67 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
ref={secretContainer}
|
||||
>
|
||||
{!isEmptyPage && (
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
<FolderSection
|
||||
onFolderOpen={handleFolderOpen}
|
||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
folders={folderList}
|
||||
search={searchFilter}
|
||||
/>
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
isReadOnly={isReadOnly}
|
||||
isRollbackMode={isRollbackMode}
|
||||
isAddOnly={isAddOnly}
|
||||
index={index}
|
||||
searchTerm={searchFilter}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
wsTags={wsTags}
|
||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
||||
<DndContext
|
||||
onDragEnd={handleDragEnd}
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={[restrictToVerticalAxis]}
|
||||
>
|
||||
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||
<table className="secret-table relative">
|
||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||
<tbody className="max-h-96 overflow-y-auto">
|
||||
<SecretImportSection
|
||||
onSecretImportDelete={(impSecEnv, impSecPath) =>
|
||||
handlePopUpOpen("deleteSecretImport", {
|
||||
environment: impSecEnv,
|
||||
secretPath: impSecPath
|
||||
})
|
||||
}
|
||||
secrets={secrets?.secrets}
|
||||
importedSecrets={importedSecrets}
|
||||
items={items}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
<FolderSection
|
||||
onFolderOpen={handleFolderOpen}
|
||||
onFolderUpdate={(id, name) => handlePopUpOpen("folderForm", { id, name })}
|
||||
onFolderDelete={(id, name) => handlePopUpOpen("deleteFolder", { id, name })}
|
||||
folders={folderList}
|
||||
search={searchFilter}
|
||||
/>
|
||||
{fields.map(({ id, _id }, index) => (
|
||||
<SecretInputRow
|
||||
key={id}
|
||||
isReadOnly={isReadOnly}
|
||||
isRollbackMode={isRollbackMode}
|
||||
isAddOnly={isAddOnly}
|
||||
index={index}
|
||||
searchTerm={searchFilter}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onRowExpand={() => onDrawerOpen({ id: _id as string, index })}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
wsTags={wsTags}
|
||||
onCreateTagOpen={() => handlePopUpOpen("addTag")}
|
||||
/>
|
||||
))}
|
||||
{!isReadOnly && !isRollbackMode && (
|
||||
<tr>
|
||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||
onClick={onAppendSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
<span className="ml-2 w-20">Add Secret</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</DndContext>
|
||||
)}
|
||||
<PitDrawer
|
||||
isDrawerOpen={popUp?.secretSnapshots?.isOpen}
|
||||
@@ -872,6 +1032,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Modal
|
||||
isOpen={popUp?.addSecretImport?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
title="Add Secret Link"
|
||||
subTitle="To inherit secrets from another environment or folder"
|
||||
>
|
||||
<SecretImportForm
|
||||
environments={currentWorkspace?.environments}
|
||||
onCreate={handleSecretImportCreate}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteFolder.isOpen}
|
||||
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
||||
@@ -879,6 +1053,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||
onDeleteApproved={handleFolderDelete}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecretImport.isOpen}
|
||||
deleteKey="unlink"
|
||||
title="Do you want to remove this secret import?"
|
||||
subTitle={`This will unlink secrets from environment ${
|
||||
(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.environment
|
||||
} of path ${(popUp.deleteSecretImport?.data as TDeleteSecretImport)?.secretPath}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecretImport", isOpen)}
|
||||
onDeleteApproved={handleSecretImportDelete}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={popUp?.compareSecrets?.isOpen}
|
||||
onOpenChange={(open) => handlePopUpToggle("compareSecrets", open)}
|
||||
@@ -900,7 +1084,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={subscription.slug === null ? "You can perform point-in-time recovery under an Enterprise license" : "You can perform point-in-time recovery if you switch to Infisical's Team plan"}
|
||||
text={
|
||||
subscription.slug === null
|
||||
? "You can perform point-in-time recovery under an Enterprise license"
|
||||
: "You can perform point-in-time recovery if you switch to Infisical's Team plan"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { useCallback, useState } from "react";
|
||||
import { faCircle, faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useSyntaxHighlight } from "@app/hooks";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
|
||||
type Props = {
|
||||
secrets: any[] | undefined;
|
||||
@@ -12,7 +14,8 @@ type Props = {
|
||||
userAvailableEnvs?: any[];
|
||||
};
|
||||
|
||||
const REGEX = /([$]{.*?})/g;
|
||||
const SEC_VAL_LINE_HEIGHT = 21;
|
||||
const MAX_MULTI_LINE = 6;
|
||||
|
||||
const DashboardInput = ({
|
||||
isOverridden,
|
||||
@@ -25,84 +28,62 @@ const DashboardInput = ({
|
||||
isReadOnly?: boolean;
|
||||
secret?: any;
|
||||
}): JSX.Element => {
|
||||
const syntaxHighlight = useCallback((val: string) => {
|
||||
if (val === undefined)
|
||||
return (
|
||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
||||
</span>
|
||||
);
|
||||
if (val?.length === 0)
|
||||
return <span className="w-full font-sans text-bunker-400/80">EMPTY</span>;
|
||||
return val?.split(REGEX).map((word, index) =>
|
||||
word.match(REGEX) !== null ? (
|
||||
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}>
|
||||
{word.slice(0, 2)}
|
||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
||||
{word.slice(word.length - 1, word.length) === "}" ? (
|
||||
<span className="ph-no-capture text-yellow">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="ph-no-capture text-yellow-400">
|
||||
{word.slice(word.length - 1, word.length)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span key={word} className="ph-no-capture">
|
||||
{word}
|
||||
</span>
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
const ref = useRef<HTMLElement | null>(null);
|
||||
const [isFocused, setIsFocused] = useToggle();
|
||||
const syntaxHighlight = useSyntaxHighlight();
|
||||
|
||||
const value = isOverridden ? secret.valueOverride : secret?.value;
|
||||
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
||||
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
||||
|
||||
return (
|
||||
<td
|
||||
key={`row-${secret?.key || ""}--`}
|
||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
||||
className={`flex w-full cursor-default flex-row ${
|
||||
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
||||
}`}
|
||||
>
|
||||
<div className="group relative flex w-full cursor-default flex-col justify-center whitespace-pre">
|
||||
<input
|
||||
value={isOverridden ? secret.valueOverride : secret?.value || ""}
|
||||
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
||||
<textarea
|
||||
readOnly={isReadOnly}
|
||||
className={twMerge(
|
||||
"ph-no-capture no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full cursor-default bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-transparent outline-none no-scrollbar",
|
||||
isSecretValueHidden && "text-transparent focus:text-transparent active:text-transparent"
|
||||
)}
|
||||
value={value}
|
||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
||||
style={{ height: `${maxMultilineHeight}px` }}
|
||||
spellCheck="false"
|
||||
onBlur={() => setIsFocused.off()}
|
||||
onFocus={() => setIsFocused.on()}
|
||||
onInput={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
onScroll={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={twMerge(
|
||||
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full cursor-default flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible",
|
||||
isSecretValueHidden && secret?.value ? "invisible" : "visible",
|
||||
isSecretValueHidden &&
|
||||
secret?.value &&
|
||||
"duration-50 text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400",
|
||||
!secret?.value && "justify-center text-bunker-400"
|
||||
)}
|
||||
>
|
||||
{syntaxHighlight(secret?.value)}
|
||||
</div>
|
||||
{isSecretValueHidden && secret?.value && (
|
||||
<div className="duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden">
|
||||
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
|
||||
{(isOverridden ? secret.valueOverride : secret?.value || "")
|
||||
?.split("")
|
||||
.map((_a: string, index: number) => (
|
||||
<FontAwesomeIcon
|
||||
key={`${secret?.value}_${index + 1}`}
|
||||
className="mr-0.5 text-xxs"
|
||||
icon={faCircle}
|
||||
/>
|
||||
))}
|
||||
{(isOverridden ? secret.valueOverride : secret?.value || "")?.split("").length ===
|
||||
0 && <span className="text-sm text-bunker-400/80">EMPTY</span>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
<code
|
||||
ref={ref}
|
||||
className={`absolute top-1.5 left-3.5 z-10 overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
||||
isOverridden && "text-primary-300"
|
||||
} ${
|
||||
(value || "") === "" && "text-mineshaft-400"
|
||||
}`}
|
||||
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
||||
>
|
||||
{value === undefined ? (
|
||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
||||
</span>
|
||||
) : (
|
||||
syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
@@ -122,14 +103,14 @@ export const EnvComparisonRow = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
|
||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-bunker-400">
|
||||
<tr className="group flex min-w-full flex-row hover:bg-mineshaft-800">
|
||||
<td className="flex w-10 justify-center border-none px-4">
|
||||
<div className="flex h-8 w-10 items-center justify-center text-center text-xs text-bunker-400">
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="flex h-8 cursor-default flex-row items-center truncate">
|
||||
<td className="flex min-w-[200px] flex-row justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="flex h-8 cursor-default flex-row items-center justify-center truncate">
|
||||
{secrets![0].key || ""}
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -67,7 +67,7 @@ export const FolderSection = ({
|
||||
ariaLabel="delete"
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -193,8 +193,8 @@ export const SecretDetailDrawer = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||
<div className="rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||
<div className="font-mono">{value}</div>
|
||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||
<div className="break-all font-mono">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onCreate: (environment: string, secretPath: string) => Promise<void>;
|
||||
environments?: Array<{ slug: string; name: string }>;
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
environment: yup.string().required().label("Environment").trim(),
|
||||
secretPath: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Secret Path")
|
||||
.trim()
|
||||
.transform((val) =>
|
||||
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
|
||||
)
|
||||
});
|
||||
|
||||
type TFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const SecretImportForm = ({ onCreate, environments = [] }: Props): JSX.Element => {
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TFormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
|
||||
const onSubmit = async ({ environment, secretPath }: TFormData) => {
|
||||
await onCreate(environment, secretPath);
|
||||
reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
defaultValue={environments?.[0]?.slug}
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{environments.map(({ name, slug }) => (
|
||||
<SelectItem value={slug} key={slug}>
|
||||
{name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
defaultValue="/"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||
Create
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button variant="plain" colorSchema="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretImportForm } from "./SecretImportForm";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user