Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-07-19 23:25:52 +07:00
339 changed files with 19009 additions and 9758 deletions
-385
View File
@@ -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>
-86
View File
@@ -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>
}
-133
View File
@@ -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>
);
}
+9 -3
View File
@@ -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 });
+17 -6
View File
@@ -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>
+1 -1
View File
@@ -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: {
+51 -4
View File
@@ -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 = {
+5 -22
View File
@@ -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">
+2 -3
View File
@@ -1,7 +1,6 @@
export {
useGetAuthToken,
useGetCommonPasswords,
useRevokeAllSessions,
useSendMfaToken,
useSendMfaToken,
useVerifyMfaToken
} from "./queries";
} from "./queries"
-9
View File
@@ -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 || [];
+2
View File
@@ -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";
+3 -1
View File
@@ -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;
};
+166 -153
View File
@@ -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 = () => {
+2
View File
@@ -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;
};
+1
View File
@@ -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,
+6 -2
View File
@@ -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";
+107 -3
View File
@@ -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);
}
});
}
+28
View File
@@ -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));
}
});
};
+26
View File
@@ -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)
});
+36
View File
@@ -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
View File
@@ -1,4 +1,5 @@
export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useToggle } from "./useToggle";
+49
View File
@@ -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}`}>
$&#123;
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
&#125;
</span>
) : (
<span key={`${word}_${i + 1}`} className="ph-no-capture">
{word}
</span>
)
);
}, []);
return syntaxHighlight;
};
+352 -190
View File
@@ -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 &rarr;</span>
</button>
</div>
)}
</div>
);
};
};
@@ -1 +0,0 @@
export { Navbar } from "./NavBar";
+4 -5
View File
@@ -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'
+4 -4
View File
@@ -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;
+7 -3
View File
@@ -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&apos;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&apos;ve successfully logged in to the Infisical CLI
</p>
</div>
</div>
+7 -7
View File
@@ -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");
-244
View File
@@ -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;
+6 -7
View File
@@ -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");
}
-55
View File
@@ -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;
@@ -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;
+16 -9
View File
@@ -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 (
+20
View File
@@ -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,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,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}
+5 -5
View File
@@ -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");
}
+15
View File
@@ -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;
+5 -5
View File
@@ -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]);
+3 -14
View File
@@ -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>
);
+17 -14
View File
@@ -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">
+306 -118
View File
@@ -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>
@@ -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