mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
414 lines
15 KiB
TypeScript
414 lines
15 KiB
TypeScript
/* eslint-disable no-unexpected-multiline */
|
|
/* eslint-disable react-hooks/exhaustive-deps */
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/router";
|
|
import { useTranslation } from "next-i18next";
|
|
import {
|
|
faBookOpen,
|
|
faFileLines,
|
|
faGear,
|
|
faKey,
|
|
faMobile,
|
|
faPlug,
|
|
faTimeline,
|
|
faUser,
|
|
} from "@fortawesome/free-solid-svg-icons";
|
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
|
|
import getOrganizations from "~/pages/api/organization/getOrgs";
|
|
import getOrganizationUserProjects from "~/pages/api/organization/GetOrgUserProjects";
|
|
import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers";
|
|
import checkUserAction from "~/pages/api/userActions/checkUserAction";
|
|
import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace";
|
|
import createWorkspace from "~/pages/api/workspace/createWorkspace";
|
|
import getWorkspaces from "~/pages/api/workspace/getWorkspaces";
|
|
import uploadKeys from "~/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;
|
|
}
|
|
|
|
export default function Layout({ children }: LayoutProps) {
|
|
const router = useRouter();
|
|
const [workspaceList, setWorkspaceList] = useState([]);
|
|
const [workspaceMapping, setWorkspaceMapping] = useState([{ "1": "2" }]);
|
|
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();
|
|
|
|
function closeModal() {
|
|
setIsOpen(false);
|
|
}
|
|
|
|
function 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
|
|
*/
|
|
async function submitModal(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;
|
|
|
|
if (addAllUsers) {
|
|
const orgUsers = await getOrganizationUsers({
|
|
orgId: tempLocalStorage("orgData.id"),
|
|
});
|
|
orgUsers.map(async (user: any) => {
|
|
if (user.status == "accepted") {
|
|
const result = await addUserToWorkspace(
|
|
user.user.email,
|
|
newWorkspaceId
|
|
);
|
|
if (result?.invitee && result?.latestKey) {
|
|
const 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: PRIVATE_KEY,
|
|
});
|
|
|
|
const { ciphertext, nonce } = encryptAssymmetric({
|
|
plaintext: key,
|
|
publicKey: result.invitee.publicKey,
|
|
privateKey: PRIVATE_KEY,
|
|
}) as { ciphertext: string; nonce: string };
|
|
|
|
uploadKeys(
|
|
newWorkspaceId,
|
|
result.invitee._id,
|
|
ciphertext,
|
|
nonce
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
router.push("/dashboard/" + newWorkspaceId);
|
|
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: 'Activity 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("settings")
|
|
) {
|
|
router.push("/noprojects");
|
|
} else if (router.asPath != "/noprojects") {
|
|
const intendedWorkspaceId = router.asPath
|
|
.split("/")
|
|
[router.asPath.split("/").length - 1].split("?")[0];
|
|
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
|
if (
|
|
intendedWorkspaceId != "heroku" &&
|
|
!userWorkspaces
|
|
.map((workspace: { _id: string }) => workspace._id)
|
|
.includes(intendedWorkspaceId)
|
|
) {
|
|
router.push("/dashboard/" + userWorkspaces[0]._id);
|
|
} else {
|
|
setWorkspaceList(
|
|
userWorkspaces.map((workspace: any) => workspace.name)
|
|
);
|
|
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 });
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
try {
|
|
if (
|
|
workspaceMapping[workspaceSelected as any] &&
|
|
`${workspaceMapping[workspaceSelected as any]}` !==
|
|
router.asPath
|
|
.split("/")
|
|
[router.asPath.split("/").length - 1].split("?")[0]
|
|
) {
|
|
router.push(
|
|
"/dashboard/" +
|
|
workspaceMapping[workspaceSelected as any]
|
|
);
|
|
localStorage.setItem(
|
|
"projectData.id",
|
|
`${workspaceMapping[workspaceSelected as any]}`
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
}, [workspaceSelected]);
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed w-full hidden md:block flex flex-col h-screen">
|
|
<script
|
|
src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/3.2.2/cdn.js"
|
|
defer
|
|
></script>
|
|
<NavBarDashboard />
|
|
<div className="flex flex-col md:flex-row flex-1">
|
|
<aside className="bg-bunker-600 border-r border-mineshaft-500 w-full md:w-60 h-screen">
|
|
<nav className="flex flex-col justify-between items-between h-full">
|
|
{/* <div className="py-6"></div> */}
|
|
<div>
|
|
<div className="flex justify-center w-full mt-[4.5rem] mb-6 bg-bunker-600 h-20 flex-col items-center px-4">
|
|
<div className="text-gray-400 self-start ml-1 mb-1 text-xs font-semibold tracking-wide">
|
|
{t("nav:menu.project")}
|
|
</div>
|
|
{workspaceList.length > 0 ? (
|
|
<Listbox
|
|
selected={workspaceSelected}
|
|
onChange={setWorkspaceSelected as any}
|
|
data={workspaceList}
|
|
buttonAction={openModal}
|
|
text=""
|
|
// workspaceMapping={workspaceMapping as any}
|
|
/>
|
|
) : (
|
|
<Button
|
|
text="Add Project"
|
|
onButtonPressed={openModal}
|
|
color="mineshaft"
|
|
size="md"
|
|
icon={faPlus}
|
|
/>
|
|
)}
|
|
</div>
|
|
<ul>
|
|
{workspaceList.length > 0 &&
|
|
menuItems.map(({ href, title, emoji }) => (
|
|
<li className="mt-0.5 mx-2" 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={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
|
>
|
|
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
|
|
<p className="w-6 ml-4 mr-2 flex items-center justify-center text-lg">
|
|
{emoji}
|
|
</p>
|
|
{title}
|
|
</div>
|
|
) : router.asPath == "/noprojects" ? (
|
|
<div
|
|
className={`flex p-2.5 text-white text-sm rounded`}
|
|
>
|
|
<p className="w-10 flex items-center justify-center text-lg">
|
|
{emoji}
|
|
</p>
|
|
{title}
|
|
</div>
|
|
) : (
|
|
<Link href={href}>
|
|
<div
|
|
className={`flex p-2.5 text-white text-sm rounded cursor-pointer hover:bg-primary-50/5`}
|
|
>
|
|
<p className="w-10 flex items-center justify-center text-lg">
|
|
{emoji}
|
|
</p>
|
|
{title}
|
|
</div>
|
|
</Link>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<div className="w-full mt-40 mb-4 px-2">
|
|
{router.asPath.split("/")[1] === "home" ? (
|
|
<div
|
|
className={`flex relative px-0.5 py-2.5 text-white text-sm rounded cursor-pointer bg-primary-50/10`}
|
|
>
|
|
<div className="absolute top-0 my-1 ml-1 inset-0 bg-primary w-1 rounded-xl mr-1"></div>
|
|
<p className="w-6 ml-4 mr-2 flex 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"
|
|
></img>
|
|
</div>
|
|
) : (
|
|
<Link
|
|
href={`/home/` + workspaceMapping[workspaceSelected as any]}
|
|
>
|
|
<div
|
|
className={`relative flex p-2.5 overflow-visible text-white h-10 text-sm rounded cursor-pointer bg-white/10 hover:bg-primary-50/[0.15] mt-max`}
|
|
>
|
|
<p className="w-10 flex 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"
|
|
></img>
|
|
</div>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</nav>
|
|
</aside>
|
|
<AddWorkspaceDialog
|
|
isOpen={isOpen}
|
|
closeModal={closeModal}
|
|
submitModal={submitModal}
|
|
workspaceName={newWorkspaceName}
|
|
setWorkspaceName={setNewWorkspaceName}
|
|
error={error}
|
|
loading={loading}
|
|
/>
|
|
<main className="flex-1 bg-bunker-800">{children}</main>
|
|
</div>
|
|
</div>
|
|
<div className="md:hidden bg-bunker-800 w-screen h-screen flex flex-col justify-center items-center">
|
|
<FontAwesomeIcon
|
|
icon={faMobile}
|
|
className="text-gray-300 text-7xl mb-8"
|
|
/>
|
|
<p className="text-gray-200 px-6 text-center text-lg max-w-sm">
|
|
{` ${t("common:no-mobile")} `}
|
|
</p>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|