mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 21:28:59 +00:00
chore: resolve merge conflicts
This commit is contained in:
@@ -1,385 +0,0 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable no-unexpected-multiline */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faBookOpen,
|
||||
faFileLines,
|
||||
faGear,
|
||||
faKey,
|
||||
faMobile,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
|
||||
import getOrganizationUsers from "@app/pages/api/organization/GetOrgUsers";
|
||||
import getUser from "@app/pages/api/user/getUser";
|
||||
import addUserToWorkspace from "@app/pages/api/workspace/addUserToWorkspace";
|
||||
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
|
||||
import getWorkspaces from "@app/pages/api/workspace/getWorkspaces";
|
||||
import uploadKeys from "@app/pages/api/workspace/uploadKeys";
|
||||
|
||||
import NavBarDashboard from "../navigation/NavBarDashboard";
|
||||
import onboardingCheck from "../utilities/checks/OnboardingCheck";
|
||||
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
|
||||
import { decryptAssymmetric, encryptAssymmetric } from "../utilities/cryptography/crypto";
|
||||
import Button from "./buttons/Button";
|
||||
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
|
||||
import Listbox from "./Listbox";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
|
||||
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState("");
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
// TODO: what to do about the fact that 2ids can have the same name
|
||||
|
||||
/**
|
||||
* When a user creates a new workspace, redirect them to the page of the new workspace.
|
||||
* @param {*} workspaceName
|
||||
*/
|
||||
const submitModal = async (workspaceName: string, addAllUsers: boolean) => {
|
||||
setLoading(true);
|
||||
// timeout code.
|
||||
setTimeout(() => setLoading(false), 1500);
|
||||
|
||||
try {
|
||||
const workspaces = await getWorkspaces();
|
||||
const currentWorkspaces = workspaces.map((workspace) => workspace.name);
|
||||
if (!currentWorkspaces.includes(workspaceName)) {
|
||||
const newWorkspace = await createWorkspace({
|
||||
workspaceName,
|
||||
organizationId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
const newWorkspaceId = newWorkspace._id;
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
|
||||
const myUser = await getUser();
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: myUser.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadKeys(newWorkspaceId, myUser._id, ciphertext, nonce);
|
||||
|
||||
if (addAllUsers) {
|
||||
console.log("adding other users");
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
orgUsers.map(async (user: any) => {
|
||||
if (user.status === "accepted" && user.email !== myUser.email) {
|
||||
const result = await addUserToWorkspace(user.user.email, newWorkspaceId);
|
||||
if (result?.invitee && result?.latestKey) {
|
||||
const TEMP_PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY");
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: result.latestKey.encryptedKey,
|
||||
nonce: result.latestKey.nonce,
|
||||
publicKey: result.latestKey.sender.publicKey,
|
||||
privateKey: TEMP_PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey: result.invitee.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
uploadKeys(newWorkspaceId, result.invitee._id, inviteeCipherText, inviteeNonce);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
setWorkspaceMapping((prevState) => ({
|
||||
...prevState,
|
||||
[workspaceName]: newWorkspaceId
|
||||
}));
|
||||
setWorkspaceSelected(workspaceName);
|
||||
setIsOpen(false);
|
||||
setNewWorkspaceName("");
|
||||
} else {
|
||||
console.error("A project with this name already exists.");
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const menuItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.secrets"),
|
||||
emoji: <FontAwesomeIcon icon={faKey} />
|
||||
},
|
||||
{
|
||||
href: `/users/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.members"),
|
||||
emoji: <FontAwesomeIcon icon={faUser} />
|
||||
},
|
||||
{
|
||||
href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.integrations"),
|
||||
emoji: <FontAwesomeIcon icon={faPlug} />
|
||||
},
|
||||
{
|
||||
href: `/activity/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: "Audit Logs",
|
||||
emoji: <FontAwesomeIcon icon={faFileLines} />
|
||||
},
|
||||
{
|
||||
href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
|
||||
title: t("nav.menu.project-settings"),
|
||||
emoji: <FontAwesomeIcon icon={faGear} />
|
||||
}
|
||||
],
|
||||
[t, workspaceMapping, workspaceSelected]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Put a user in a workspace if they're not in one yet
|
||||
|
||||
const putUserInWorkSpace = async () => {
|
||||
if (tempLocalStorage("orgData.id") === "") {
|
||||
const userOrgs = await getOrganizations();
|
||||
localStorage.setItem("orgData.id", userOrgs[0]._id);
|
||||
}
|
||||
|
||||
const orgUserProjects = await getOrganizationUserProjects({
|
||||
orgId: tempLocalStorage("orgData.id")
|
||||
});
|
||||
const userWorkspaces = orgUserProjects;
|
||||
if (
|
||||
(userWorkspaces.length === 0 &&
|
||||
router.asPath !== "/noprojects" &&
|
||||
!router.asPath.includes("home") &&
|
||||
!router.asPath.includes("settings")) ||
|
||||
router.asPath === "/dashboard/undefined"
|
||||
) {
|
||||
router.push("/noprojects");
|
||||
} else if (router.asPath !== "/noprojects") {
|
||||
const intendedWorkspaceId = router.asPath
|
||||
.split("/")
|
||||
[router.asPath.split("/").length - 1].split("?")[0];
|
||||
|
||||
if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
|
||||
localStorage.setItem("projectData.id", intendedWorkspaceId);
|
||||
}
|
||||
|
||||
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
|
||||
if (
|
||||
!["callback", "create", "authorize"].includes(intendedWorkspaceId) &&
|
||||
!userWorkspaces
|
||||
.map((workspace: { _id: string }) => workspace._id)
|
||||
.includes(intendedWorkspaceId)
|
||||
) {
|
||||
router.push(`/dashboard/${userWorkspaces[0]._id}`);
|
||||
} else {
|
||||
setWorkspaceMapping(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
|
||||
) as any
|
||||
);
|
||||
setWorkspaceSelected(
|
||||
Object.fromEntries(
|
||||
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
|
||||
)[router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]]
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
putUserInWorkSpace();
|
||||
onboardingCheck({ setTotalOnboardingActionsDone });
|
||||
}, [router.query.id]);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (
|
||||
workspaceMapping[workspaceSelected as any] &&
|
||||
`${workspaceMapping[workspaceSelected as any]}` !==
|
||||
router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]
|
||||
) {
|
||||
localStorage.setItem("projectData.id", `${workspaceMapping[workspaceSelected as any]}`);
|
||||
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}, [workspaceSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-screen w-full flex-col overflow-x-hidden hidden md:flex">
|
||||
<NavBarDashboard />
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row dark">
|
||||
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
|
||||
<nav className="items-between flex h-full flex-col justify-between">
|
||||
{/* <div className="py-6"></div> */}
|
||||
<div>
|
||||
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
|
||||
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
|
||||
{t("nav.menu.project")}
|
||||
</div>
|
||||
{Object.keys(workspaceMapping).length > 0 ? (
|
||||
<Listbox
|
||||
isSelected={workspaceSelected}
|
||||
onChange={setWorkspaceSelected}
|
||||
data={Object.keys(workspaceMapping)}
|
||||
buttonAction={openModal}
|
||||
text=""
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
text="Add Project"
|
||||
onButtonPressed={openModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ul>
|
||||
{Object.keys(workspaceMapping).length > 0 &&
|
||||
menuItems.map(({ href, title, emoji }) => (
|
||||
<li className="mx-2 mt-0.5" key={title}>
|
||||
{router.asPath.split("/")[1] === href.split("/")[1] &&
|
||||
(["project", "billing", "org", "personal"].includes(
|
||||
router.asPath.split("/")[2]
|
||||
)
|
||||
? router.asPath.split("/")[2] === href.split("/")[2]
|
||||
: true) ? (
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
) : router.asPath === "/noprojects" ? (
|
||||
<div className="flex rounded p-2.5 text-sm text-white">
|
||||
<p className="flex w-10 items-center justify-center text-lg">{emoji}</p>
|
||||
{title}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={href}>
|
||||
<div className="flex cursor-pointer rounded p-2.5 text-sm text-white hover:bg-primary-50/5">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
{emoji}
|
||||
</p>
|
||||
{title}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-40 mb-4 w-full px-2">
|
||||
{router.asPath.split("/")[1] === "home" ? (
|
||||
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
|
||||
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
|
||||
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
|
||||
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
|
||||
<p className="flex w-10 items-center justify-center text-lg">
|
||||
<FontAwesomeIcon icon={faBookOpen} />
|
||||
</p>
|
||||
Infisical Guide
|
||||
<img
|
||||
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
|
||||
totalOnboardingActionsDone === 1 ? "14" : ""
|
||||
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
|
||||
totalOnboardingActionsDone === 3 ? "43" : ""
|
||||
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
|
||||
totalOnboardingActionsDone === 5 ? "71" : ""
|
||||
}.svg`}
|
||||
height={58}
|
||||
width={58}
|
||||
alt="progress bar"
|
||||
className="absolute right-2 -top-2"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<AddWorkspaceDialog
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
submitModal={submitModal}
|
||||
workspaceName={newWorkspaceName}
|
||||
setWorkspaceName={setNewWorkspaceName}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 z-[200] md:hidden">
|
||||
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
|
||||
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -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>
|
||||
|
||||
@@ -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,184 +0,0 @@
|
||||
import { 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 Error from "../basic/Error";
|
||||
import attemptCliLogin from "../utilities/attemptCliLogin";
|
||||
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
|
||||
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Button, Input } from "../v2";
|
||||
|
||||
export default function InitialLoginStep({
|
||||
setStep,
|
||||
email,
|
||||
setEmail,
|
||||
password,
|
||||
setPassword,
|
||||
}: {
|
||||
setStep: (step: number) => void;
|
||||
email: string;
|
||||
setEmail: (email: string) => void;
|
||||
password: string;
|
||||
setPassword: (password: string) => void;
|
||||
}) {
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [loginError, setLoginError] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const queryParams = new URLSearchParams(window.location.search)
|
||||
if (queryParams && queryParams.get("callback_port")) {
|
||||
const callbackPort = queryParams.get("callback_port")
|
||||
|
||||
// attemptCliLogin
|
||||
const isCliLoginSuccessful = await attemptCliLogin({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
|
||||
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||
|
||||
if (isCliLoginSuccessful.mfaEnabled) {
|
||||
// case: login requires MFA step
|
||||
setStep(2);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
// case: login was successful
|
||||
const cliUrl = `http://localhost:${callbackPort}`
|
||||
|
||||
// send request to server endpoint
|
||||
const instance = axios.create()
|
||||
const cliResp = await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse })
|
||||
console.log(cliResp)
|
||||
|
||||
// cli page
|
||||
router.push("/cli-redirect");
|
||||
|
||||
// on success, router.push to cli Login Successful page
|
||||
|
||||
}
|
||||
} else {
|
||||
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 <div 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
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
window.open('/api/v1/auth/redirect/google')
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
{t('login.continue-with-google')}
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className="relative md:px-1.5 flex items-center justify-center lg:w-1/6 w-1/4 min-w-[21.3rem] md:min-w-[22rem] mx-auto rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full md:px-2 md:py-1 rounded-lg 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>
|
||||
<div className="relative pt-2 md:pt-0 md:px-1.5 flex items-center justify-center w-1/4 lg:w-1/6 min-w-[21.3rem] md:min-w-[22rem] mx-auto 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 select:-webkit-autofill:focus"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!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()}
|
||||
size="sm"
|
||||
isFullWidth
|
||||
className='h-12'
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
isLoading={isLoading}
|
||||
> Login </Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center mt-4 py-2'>
|
||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||
<span className='px-4 text-sm text-bunker-400'>or</span>
|
||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => router.push("/saml-sso")}
|
||||
isFullWidth
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
Continue with SAML SSO
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6 text-bunker-400 text-sm flex flex-row">
|
||||
<span className="mr-1">Don't have an acount yet?</span>
|
||||
<Link href="/signup">
|
||||
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.create-account")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="text-bunker-400 text-sm flex flex-row">
|
||||
<span className="mr-1">Forgot password?</span>
|
||||
<Link href="/verify-email">
|
||||
<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>
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Error from "@app/components/basic/Error";
|
||||
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||
|
||||
import SecurityClient from "../utilities/SecurityClient";
|
||||
import { Button, Input } from "../v2";
|
||||
|
||||
export default function PasswordInputStep({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
setPassword,
|
||||
setProviderAuthToken,
|
||||
setStep
|
||||
}: {
|
||||
email: string;
|
||||
password: string;
|
||||
providerAuthToken: string;
|
||||
setPassword: (password: string) => void;
|
||||
setProviderAuthToken: (value: 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 {
|
||||
setIsLoading(true);
|
||||
const loginAttempt = await attemptLogin({
|
||||
email,
|
||||
password,
|
||||
providerAuthToken,
|
||||
});
|
||||
|
||||
if (loginAttempt && loginAttempt.success) {
|
||||
// case: login was successful
|
||||
|
||||
if (loginAttempt.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="h-full mx-auto w-full max-w-md px-6 pt-8">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||
What’s your Infisical Password?
|
||||
</p>
|
||||
<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 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='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={async () => handleLogin()}
|
||||
isFullWidth
|
||||
isLoading={isLoading}
|
||||
className="h-14"
|
||||
>
|
||||
{t("login.login")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-bunker-400 text-xs flex flex-col items-center w-max mx-auto mt-4">
|
||||
<span className='duration-200 max-w-sm text-center px-4'>
|
||||
Infisical Master Password serves as a decryption mechanism so that even Google is not able to access your secrets.
|
||||
</span>
|
||||
<Link href="/verify-email">
|
||||
<span className='hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
setProviderAuthToken("");
|
||||
}}
|
||||
type="button"
|
||||
className="text-bunker-400 text-xs hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
||||
>
|
||||
{t("login.other-option")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -58,10 +58,16 @@ export default function NavHeader({
|
||||
|
||||
return (
|
||||
<div className="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="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
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// import { faGoogle } from '@fortawesome/free-brands-svg-icons';
|
||||
// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
// import { faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button } from "../v2";
|
||||
|
||||
export default function InitialSignupStep({
|
||||
@@ -16,19 +16,6 @@ export default function InitialSignupStep({
|
||||
|
||||
return <div 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' >{t("signup.initial-title")}</h1>
|
||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
window.open('/api/v1/auth/redirect/google')
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
{t('signup.continue-with-google')}
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
@@ -42,6 +29,25 @@ export default function InitialSignupStep({
|
||||
Sign Up with email
|
||||
</Button>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] flex flex-row items-center my-4 py-2'>
|
||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||
<span className='px-4 text-sm text-bunker-400'>or</span>
|
||||
<div className='w-1/2 border-t border-mineshaft-500' />
|
||||
</div>
|
||||
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="solid"
|
||||
onClick={() => {
|
||||
window.open("/api/v1/sso/redirect/google");
|
||||
window.close();
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-1" />}
|
||||
className="h-14 w-full mx-0"
|
||||
>
|
||||
{t("signup.continue-with-google")}
|
||||
</Button>
|
||||
</div> */}
|
||||
<div className='lg:w-1/6 w-1/4 min-w-[20rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,6 @@ interface OnboardingCheckProps {
|
||||
setTotalOnboardingActionsDone?: (value: number) => void;
|
||||
setHasUserClickedSlack?: (value: boolean) => void;
|
||||
setHasUserClickedIntro?: (value: boolean) => void;
|
||||
setHasUserStarred?: (value: boolean) => void;
|
||||
setHasUserPushedSecrets?: (value: boolean) => void;
|
||||
setUsersInOrg?: (value: boolean) => void;
|
||||
}
|
||||
@@ -17,7 +16,6 @@ const onboardingCheck = async ({
|
||||
setTotalOnboardingActionsDone,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserClickedIntro,
|
||||
setHasUserStarred,
|
||||
setHasUserPushedSecrets,
|
||||
setUsersInOrg
|
||||
}: OnboardingCheckProps) => {
|
||||
@@ -46,14 +44,6 @@ const onboardingCheck = async ({
|
||||
}
|
||||
if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
|
||||
|
||||
const userActionStar = await checkUserAction({
|
||||
action: "star_cta_clicked"
|
||||
});
|
||||
if (userActionStar) {
|
||||
countActions += 1;
|
||||
}
|
||||
if (setHasUserStarred) setHasUserStarred(!!userActionStar);
|
||||
|
||||
const orgId = localStorage.getItem("orgData.id");
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId: orgId || ""
|
||||
|
||||
@@ -54,7 +54,6 @@ export const load = () => {
|
||||
|
||||
// Initializes Intercom
|
||||
export const boot = (options = {}) => {
|
||||
console.log("boot", { app_id: APP_ID, ...options })
|
||||
window &&
|
||||
window.Intercom &&
|
||||
window.Intercom("boot", { app_id: APP_ID, ...options });
|
||||
|
||||
@@ -61,7 +61,8 @@ const buttonVariants = cva(
|
||||
{
|
||||
colorSchema: "primary",
|
||||
variant: "star",
|
||||
className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100"
|
||||
className:
|
||||
"bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100"
|
||||
},
|
||||
{
|
||||
colorSchema: "primary",
|
||||
@@ -76,12 +77,14 @@ const buttonVariants = cva(
|
||||
{
|
||||
colorSchema: "primary",
|
||||
variant: "outline_bg",
|
||||
className: "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200"
|
||||
className:
|
||||
"bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200"
|
||||
},
|
||||
{
|
||||
colorSchema: "secondary",
|
||||
variant: "star",
|
||||
className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white"
|
||||
className:
|
||||
"bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white"
|
||||
},
|
||||
{
|
||||
colorSchema: "danger",
|
||||
@@ -163,13 +166,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
type="button"
|
||||
className={twMerge(
|
||||
buttonVariants({
|
||||
className,
|
||||
colorSchema,
|
||||
size,
|
||||
variant,
|
||||
isRounded,
|
||||
isDisabled,
|
||||
isFullWidth
|
||||
isFullWidth,
|
||||
className
|
||||
})
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
@@ -193,7 +196,15 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
>
|
||||
{leftIcon}
|
||||
</div>
|
||||
<span className={twMerge("transition-all", isFullWidth ? "w-full" : "w-min", loadingToggleClass)}>{children}</span>
|
||||
<span
|
||||
className={twMerge(
|
||||
"transition-all",
|
||||
isFullWidth ? "w-full" : "w-min",
|
||||
loadingToggleClass
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
||||
|
||||
@@ -21,7 +21,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(
|
||||
"min-w-[220px] bg-bunker will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
"min-w-[220px] z-30 bg-mineshaft-900 border border-mineshaft-600 will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -62,11 +62,11 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
|
||||
<DropdownMenuPrimitive.Item
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"text-sm block font-inter px-4 py-2 data-[highlighted]:bg-gray-700 outline-none cursor-pointer",
|
||||
"text-xs text-mineshaft-200 block font-inter px-4 py-2 data-[highlighted]:bg-mineshaft-700 rounded-sm outline-none cursor-pointer",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Item type="button" role="menuitem" class="flex w-full items-center" ref={inputRef}>
|
||||
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
|
||||
{icon && <span className="flex items-center mr-2">{icon}</span>}
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const EmptyState = ({
|
||||
className
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
|
||||
<FontAwesomeIcon icon={icon} size={iconSize} />
|
||||
<div className="flex flex-row items-center py-4">
|
||||
<div className="text-sm text-bunker-300">{title}</div>
|
||||
<div>{children}</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const inputVariants = cva(
|
||||
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-[0.05rem] hover:ring-bunker-400/60 duration-100",
|
||||
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
|
||||
@@ -48,7 +48,7 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
>
|
||||
<li
|
||||
className={twMerge(
|
||||
"group px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
|
||||
"group px-1 py-2 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
|
||||
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
|
||||
isDisabled && "hover:bg-transparent cursor-not-allowed",
|
||||
className
|
||||
@@ -56,16 +56,16 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
>
|
||||
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm">
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
|
||||
<div className={`${isSelected ? "visisble" : "invisible"} -left-[0.28rem] absolute w-[0.07rem] rounded-md h-5 bg-primary`}/>
|
||||
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
style={{ width: 24, height: 24 }}
|
||||
style={{ width: 22, height: 22 }}
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
animationData={require(`../../../../public/lotties/${icon}.json`)}
|
||||
loop={false}
|
||||
autoplay={false}
|
||||
className="my-auto ml-3 mr-3"
|
||||
className="my-auto ml-[0.1rem] mr-3"
|
||||
/>
|
||||
<span className="flex-grow text-left">{children}</span>
|
||||
</Item>
|
||||
@@ -76,6 +76,53 @@ export const MenuItem = <T extends ElementType = "button">({
|
||||
)
|
||||
};
|
||||
|
||||
export const SubMenuItem = <T extends ElementType = "button">({
|
||||
children,
|
||||
icon,
|
||||
className,
|
||||
isDisabled,
|
||||
isSelected,
|
||||
as: Item = "button",
|
||||
description,
|
||||
// wrapping in forward ref with generic component causes the loss of ts definitions on props
|
||||
inputRef,
|
||||
...props
|
||||
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
|
||||
const iconRef = useRef()
|
||||
|
||||
return(
|
||||
<a
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
>
|
||||
<li
|
||||
className={twMerge(
|
||||
"group px-1 py-1 mt-0.5 font-inter flex flex-col text-sm text-mineshaft-300 hover:text-mineshaft-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
|
||||
isDisabled && "hover:bg-transparent cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm pl-6">
|
||||
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
|
||||
<Lottie
|
||||
lottieRef={iconRef}
|
||||
style={{ width: 16, height: 16 }}
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
animationData={require(`../../../../public/lotties/${icon}.json`)}
|
||||
loop={false}
|
||||
autoplay={false}
|
||||
className="my-auto ml-[0.1rem] mr-3"
|
||||
/>
|
||||
<span className="flex-grow text-left text-sm">{children}</span>
|
||||
</Item>
|
||||
{description && <span className="mt-2 text-xs">{description}</span>}
|
||||
</motion.span>
|
||||
</li>
|
||||
</a>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
MenuItem.displayName = "MenuItem";
|
||||
|
||||
export type MenuGroupProps = {
|
||||
|
||||
@@ -72,12 +72,12 @@ export const TabsObject = () => {
|
||||
>
|
||||
Windows
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
{/* <Tabs.Trigger
|
||||
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
|
||||
value="tab3"
|
||||
>
|
||||
Arch Linux
|
||||
</Tabs.Trigger>
|
||||
</Tabs.Trigger> */}
|
||||
<a
|
||||
target='_blank'
|
||||
rel="noopener noreferrer"
|
||||
@@ -88,7 +88,7 @@ export const TabsObject = () => {
|
||||
</a>
|
||||
</Tabs.List>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
value="tab1"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCode" />
|
||||
@@ -105,10 +105,10 @@ export const TabsObject = () => {
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none"
|
||||
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none"
|
||||
value="tab2"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeW" />
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="scoop bucket add org https://github.com/Infisical/scoop-infisical.git" id="downloadCodeW" />
|
||||
<div className='font-mono text-sm px-3 py-2 mt-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
|
||||
<input disabled value="scoop install infisical" id="downloadCodeW2" className='w-full bg-transparent text-bunker-200'/>
|
||||
<button
|
||||
@@ -138,22 +138,5 @@ export const TabsObject = () => {
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content
|
||||
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
|
||||
value="tab3"
|
||||
>
|
||||
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeL" />
|
||||
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeL" />
|
||||
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeL" />
|
||||
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeL" />
|
||||
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
|
||||
className='text-primary underline underline-offset-2'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href='https://infisical.com/docs/integrations/overview'
|
||||
>
|
||||
here
|
||||
</a>. </p>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { ReactNode } from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type TooltipProps = {
|
||||
export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
|
||||
children: ReactNode;
|
||||
content?: ReactNode;
|
||||
isOpen?: boolean;
|
||||
@@ -10,7 +10,7 @@ export type TooltipProps = {
|
||||
asChild?: boolean;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
defaultOpen?: boolean;
|
||||
} & Omit<TooltipPrimitive.TooltipContentProps, "open">;
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { useSubscription } from "@app/context";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useGetOrgTrialUrl
|
||||
} from "@app/hooks/api";
|
||||
|
||||
import { Button } from "../Button";
|
||||
import { Modal, ModalClose, ModalContent } from "../Modal";
|
||||
import { Modal, ModalContent } from "../Modal";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
@@ -13,32 +14,61 @@ type Props = {
|
||||
|
||||
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
||||
const link = (subscription && subscription.slug !== null)
|
||||
? `/settings/billing/${localStorage.getItem("projectData.id") as string}`
|
||||
? `/org/${currentOrg?._id}/billing`
|
||||
: "https://infisical.com/scheduledemo";
|
||||
|
||||
const handleUpgradeBtnClick = async () => {
|
||||
try {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
if (!subscription.has_used_trial) {
|
||||
// direct user to start pro trial
|
||||
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
} else {
|
||||
// direct user to upgrade their plan
|
||||
window.location.href = link;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Unleash Infisical's Full Power"
|
||||
footerContent={[
|
||||
<Link
|
||||
href={link}
|
||||
key="upgrade-plan"
|
||||
>
|
||||
<Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button>
|
||||
</Link>,
|
||||
<ModalClose asChild key="upgrade-plan-cancel">
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
]}
|
||||
>
|
||||
<p className="mb-2 text-bunker-300">{text}</p>
|
||||
<p className="text-bunker-300">
|
||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
||||
</p>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
colorSchema="primary"
|
||||
onClick={handleUpgradeBtnClick}
|
||||
className="mr-4"
|
||||
>
|
||||
{(subscription && !subscription.has_used_trial) ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => onOpenChange && onOpenChange(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ export const publicPaths = [
|
||||
"/signupinvite",
|
||||
"/pricing",
|
||||
"/signup",
|
||||
"/signup/sso",
|
||||
"/login",
|
||||
"/blog",
|
||||
"/docs",
|
||||
@@ -18,8 +19,9 @@ export const publicPaths = [
|
||||
"/verify-email",
|
||||
"/password-reset",
|
||||
"/saml-sso",
|
||||
"/login/provider/success",
|
||||
"/login/provider/error"
|
||||
"/login/provider/success", // TODO: change
|
||||
"/login/provider/error", // TODO: change
|
||||
"/login/sso"
|
||||
];
|
||||
|
||||
export const languageMap = {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -6,11 +6,14 @@ export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./secretFolders";
|
||||
export * from "./secretImports";
|
||||
export * from "./secrets";
|
||||
export * from "./secretSnapshots";
|
||||
export * from "./serviceAccounts";
|
||||
export * from "./serviceTokens";
|
||||
export * from "./ssoConfig";
|
||||
export * from "./subscriptions";
|
||||
export * from "./tags";
|
||||
export * from "./users";
|
||||
export * from "./webhooks";
|
||||
export * from "./workspace";
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export {
|
||||
useDeleteIntegrationAuth,
|
||||
useGetIntegrationAuthApps,
|
||||
useGetIntegrationAuthBitBucketWorkspaces,
|
||||
useGetIntegrationAuthById,
|
||||
useGetIntegrationAuthRailwayEnvironments,
|
||||
useGetIntegrationAuthRailwayServices,
|
||||
useGetIntegrationAuthTeams,
|
||||
useGetIntegrationAuthVercelBranches
|
||||
useGetIntegrationAuthVercelBranches,
|
||||
} from "./queries";
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace/queries";
|
||||
import { App, Environment, IntegrationAuth, Service, Team } from "./types";
|
||||
import { App, BitBucketWorkspace, Environment, IntegrationAuth, Service, Team } from "./types";
|
||||
|
||||
const integrationAuthKeys = {
|
||||
getIntegrationAuthById: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuth"] as const,
|
||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) =>
|
||||
[{ integrationAuthId, teamId }, "integrationAuthApps"] as const,
|
||||
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string, workspaceSlug?: string) =>
|
||||
[{ integrationAuthId, teamId, workspaceSlug }, "integrationAuthApps"] as const,
|
||||
getIntegrationAuthTeams: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuthTeams"] as const,
|
||||
getIntegrationAuthVercelBranches: ({
|
||||
@@ -19,6 +19,7 @@ const integrationAuthKeys = {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
|
||||
|
||||
getIntegrationAuthRailwayEnvironments: ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
@@ -32,7 +33,9 @@ const integrationAuthKeys = {
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
appId: string;
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const
|
||||
}) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
|
||||
getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
|
||||
[{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
@@ -44,12 +47,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
|
||||
|
||||
const fetchIntegrationAuthApps = async ({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
teamId,
|
||||
workspaceSlug
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
workspaceSlug?: string;
|
||||
}) => {
|
||||
const searchParams = new URLSearchParams(teamId ? { teamId } : undefined);
|
||||
const params: Record<string, string> = {}
|
||||
if (teamId) {
|
||||
params.teamId = teamId
|
||||
}
|
||||
if (workspaceSlug) {
|
||||
params.workspaceSlug = workspaceSlug
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(params);
|
||||
const { data } = await apiRequest.get<{ apps: App[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/apps`,
|
||||
{ params: searchParams }
|
||||
@@ -64,6 +77,7 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
|
||||
return data.teams;
|
||||
};
|
||||
|
||||
|
||||
const fetchIntegrationAuthVercelBranches = async ({
|
||||
integrationAuthId,
|
||||
appId
|
||||
@@ -127,6 +141,13 @@ const fetchIntegrationAuthRailwayServices = async ({
|
||||
return services;
|
||||
};
|
||||
|
||||
const fetchIntegrationAuthBitBucketWorkspaces = async (integrationAuthId: string) => {
|
||||
const { data: { workspaces } } = await apiRequest.get<{ workspaces: BitBucketWorkspace[] }>(
|
||||
`/api/v1/integration-auth/${integrationAuthId}/bitbucket/workspaces`
|
||||
);
|
||||
return workspaces;
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
|
||||
@@ -137,17 +158,20 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
|
||||
|
||||
export const useGetIntegrationAuthApps = ({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
teamId,
|
||||
workspaceSlug,
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
teamId?: string;
|
||||
workspaceSlug?: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId),
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthApps(integrationAuthId, teamId, workspaceSlug),
|
||||
queryFn: () =>
|
||||
fetchIntegrationAuthApps({
|
||||
integrationAuthId,
|
||||
teamId
|
||||
teamId,
|
||||
workspaceSlug
|
||||
}),
|
||||
enabled: true
|
||||
});
|
||||
@@ -224,6 +248,14 @@ export const useGetIntegrationAuthRailwayServices = ({
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIntegrationAuthBitBucketWorkspaces = (integrationAuthId: string) => {
|
||||
return useQuery({
|
||||
queryKey: integrationAuthKeys.getIntegrationAuthBitBucketWorkspaces(integrationAuthId),
|
||||
queryFn: () => fetchIntegrationAuthBitBucketWorkspaces(integrationAuthId),
|
||||
enabled: true
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIntegrationAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -235,3 +267,4 @@ export const useDeleteIntegrationAuth = () => {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -30,3 +30,9 @@ export type Service = {
|
||||
name: string;
|
||||
serviceId: string;
|
||||
};
|
||||
|
||||
export type BitBucketWorkspace = {
|
||||
uuid: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -21,7 +22,7 @@ const organizationKeys = {
|
||||
getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const,
|
||||
getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const,
|
||||
getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const,
|
||||
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const
|
||||
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const,
|
||||
};
|
||||
|
||||
export const useGetOrganization = () => {
|
||||
@@ -47,6 +48,24 @@ export const useRenameOrg = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetOrgTrialUrl = () => {
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
orgId,
|
||||
success_url
|
||||
}: {
|
||||
orgId: string;
|
||||
success_url: string;
|
||||
}) => {
|
||||
const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${orgId}/session/trial`, {
|
||||
success_url
|
||||
})
|
||||
|
||||
return url;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetOrgPlanBillingInfo = (organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
|
||||
export { useGetImportedSecrets, useGetSecretImports } from "./queries";
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretImportKeys } from "./queries";
|
||||
import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TUpdateSecretImportDTO } from "./types";
|
||||
|
||||
export const useCreateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateSecretImportDTO>({
|
||||
mutationFn: async ({ secretImport, environment, workspaceId, folderId }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-imports", {
|
||||
secretImport,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretImportDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, folderId, secretImports, id }) => {
|
||||
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
|
||||
secretImports,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretImport = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretImportDTO>({
|
||||
mutationFn: async ({ id, secretImportEnv, secretImportPath }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/secret-imports/${id}`, {
|
||||
data: {
|
||||
secretImportPath,
|
||||
secretImportEnv
|
||||
}
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId)
|
||||
);
|
||||
queryClient.invalidateQueries(
|
||||
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId)
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
decryptSymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGetImportedSecrets, TImportedSecrets, TSecretImports } from "./types";
|
||||
|
||||
export const secretImportKeys = {
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretImportSecrets: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-import-sec"
|
||||
]
|
||||
};
|
||||
|
||||
const fetchSecretImport = async (workspaceId: string, environment: string, folderId?: string) => {
|
||||
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
|
||||
"/api/v1/secret-imports",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secretImport;
|
||||
};
|
||||
|
||||
export const useGetSecretImports = (workspaceId: string, env: string, folderId?: string) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(env),
|
||||
queryKey: secretImportKeys.getProjectSecretImports(workspaceId, env, folderId),
|
||||
queryFn: () => fetchSecretImport(workspaceId, env, folderId)
|
||||
});
|
||||
|
||||
const fetchImportedSecrets = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
folderId?: string
|
||||
) => {
|
||||
const { data } = await apiRequest.get<{ secrets: TImportedSecrets }>(
|
||||
"/api/v1/secret-imports/secrets",
|
||||
{
|
||||
params: {
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
}
|
||||
}
|
||||
);
|
||||
return data.secrets;
|
||||
};
|
||||
|
||||
export const useGetImportedSecrets = ({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId,
|
||||
decryptFileKey
|
||||
}: TGetImportedSecrets) =>
|
||||
useQuery({
|
||||
enabled: Boolean(workspaceId) && Boolean(environment) && Boolean(decryptFileKey),
|
||||
queryKey: secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId),
|
||||
queryFn: () => fetchImportedSecrets(workspaceId, environment, folderId),
|
||||
select: useCallback(
|
||||
(data: TImportedSecrets) => {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
const latestKey = decryptFileKey;
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.encryptedKey,
|
||||
nonce: latestKey.nonce,
|
||||
publicKey: latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
return data.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
folderId: el.folderId,
|
||||
secrets: el.secrets.map((encSecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: encSecret.secretKeyCiphertext,
|
||||
iv: encSecret.secretKeyIV,
|
||||
tag: encSecret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: encSecret.secretValueCiphertext,
|
||||
iv: encSecret.secretValueIV,
|
||||
tag: encSecret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretComment = decryptSymmetric({
|
||||
ciphertext: encSecret.secretCommentCiphertext,
|
||||
iv: encSecret.secretCommentIV,
|
||||
tag: encSecret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
|
||||
return {
|
||||
_id: encSecret._id,
|
||||
env: encSecret.environment,
|
||||
key: secretKey,
|
||||
value: secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt
|
||||
};
|
||||
})
|
||||
}));
|
||||
},
|
||||
[decryptFileKey]
|
||||
)
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { EncryptedSecret } from "../secrets/types";
|
||||
import { UserWsKeyPair } from "../types";
|
||||
|
||||
export type TSecretImports = {
|
||||
_id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId: string;
|
||||
imports: Array<{ environment: string; secretPath: string }>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TImportedSecrets = {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
folderId: string;
|
||||
secrets: EncryptedSecret[];
|
||||
}[];
|
||||
|
||||
export type TGetImportedSecrets = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
};
|
||||
|
||||
export type TCreateSecretImportDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImport: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImports: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type TDeleteSecretImportDTO = {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
folderId?: string;
|
||||
secretImportPath: string;
|
||||
secretImportEnv: string;
|
||||
};
|
||||
@@ -24,6 +24,10 @@ export const secretKeys = {
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets"
|
||||
],
|
||||
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||
{ workspaceId, env, folderId },
|
||||
"secrets-imports"
|
||||
],
|
||||
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
useCreateSSOConfig,
|
||||
useGetSSOConfig,
|
||||
useUpdateSSOConfig
|
||||
} from "./queries";
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
const ssoConfigKeys = {
|
||||
getSSOConfig: (orgId: string) => [{ orgId }, "organization-saml-sso"] as const,
|
||||
}
|
||||
|
||||
export const useGetSSOConfig = (organizationId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ssoConfigKeys.getSSOConfig(organizationId),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get(
|
||||
`/api/v1/sso/config?organizationId=${organizationId}`
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
export const useCreateSSOConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
organizationId,
|
||||
authProvider,
|
||||
isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
}: {
|
||||
organizationId: string;
|
||||
authProvider: string;
|
||||
isActive: boolean;
|
||||
entryPoint: string;
|
||||
issuer: string;
|
||||
cert: string;
|
||||
audience: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post(
|
||||
"/api/v1/sso/config",
|
||||
{
|
||||
organizationId,
|
||||
authProvider,
|
||||
isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSSOConfig = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
organizationId,
|
||||
authProvider,
|
||||
isActive,
|
||||
entryPoint,
|
||||
issuer,
|
||||
cert,
|
||||
audience
|
||||
}: {
|
||||
organizationId: string;
|
||||
authProvider?: string;
|
||||
isActive?: boolean;
|
||||
entryPoint?: string;
|
||||
issuer?: string;
|
||||
cert?: string;
|
||||
audience?: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.patch(
|
||||
"/api/v1/sso/config",
|
||||
{
|
||||
organizationId,
|
||||
...(authProvider !== undefined ? { authProvider } : {}),
|
||||
...(isActive !== undefined ? { isActive } : {}),
|
||||
...(entryPoint !== undefined ? { entryPoint } : {}),
|
||||
...(issuer !== undefined ? { issuer } : {}),
|
||||
...(cert !== undefined ? { cert } : {}),
|
||||
...(audience !== undefined ? { audience } : {})
|
||||
}
|
||||
);
|
||||
|
||||
return data;
|
||||
},
|
||||
onSuccess(_, dto) {
|
||||
queryClient.invalidateQueries(ssoConfigKeys.getSSOConfig(dto.organizationId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -13,6 +13,8 @@ export type SubscriptionPlan = {
|
||||
workspaceLimit: number;
|
||||
workspacesUsed: number;
|
||||
environmentLimit: number;
|
||||
samlSSO: boolean;
|
||||
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
||||
trial_end: number | null;
|
||||
has_used_trial: boolean;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"
|
||||
export type { SubscriptionPlan } from "./subscriptions/types";
|
||||
export type { WsTag } from "./tags/types";
|
||||
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types";
|
||||
export type { TWebhook } from "./webhooks/types";
|
||||
export type {
|
||||
CreateEnvironmentDTO,
|
||||
CreateWorkspaceDTO,
|
||||
|
||||
@@ -13,4 +13,6 @@ export {
|
||||
useLogoutUser,
|
||||
useRegisterUserAction,
|
||||
useRevokeMySessions,
|
||||
useUpdateOrgUserRole} from "./queries";
|
||||
useUpdateOrgUserRole,
|
||||
useUpdateUserAuthProvider
|
||||
} from "./queries";
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CreateAPIKeyRes,
|
||||
DeletOrgMembershipDTO,
|
||||
OrgUser,
|
||||
RenameUserDTO,
|
||||
TokenVersion,
|
||||
UpdateOrgUserRoleDTO,
|
||||
User} from "./types";
|
||||
@@ -45,6 +46,39 @@ 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 useUpdateUserAuthProvider = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
authProvider
|
||||
}: {
|
||||
authProvider: string;
|
||||
}) => {
|
||||
const { data: { user } } = await apiRequest.patch("/api/v2/users/me/auth-provider", {
|
||||
authProvider
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetUserAction = (action: string) =>
|
||||
useQuery({
|
||||
queryKey: userKeys.userAction,
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { UserWsKeyPair } from "../keys/types";
|
||||
|
||||
export enum AuthProvider {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
OKTA_SAML = "okta-saml"
|
||||
}
|
||||
|
||||
export type User = {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
authProvider?: AuthProvider;
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
@@ -67,6 +74,10 @@ export type CreateAPIKeyRes = {
|
||||
apiKeyData: APIKeyData;
|
||||
}
|
||||
|
||||
export type RenameUserDTO = {
|
||||
newName: string;
|
||||
};
|
||||
|
||||
export type APIKeyData = {
|
||||
_id: string;
|
||||
name: string;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useCreateWebhook, useDeleteWebhook, useTestWebhook, useUpdateWebhook } from "./mutation";
|
||||
export { useGetWebhooks } from "./query";
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { queryKeys } from "./query";
|
||||
import { TCreateWebhookDto, TDeleteWebhookDto, TTestWebhookDTO, TUpdateWebhookDto } from "./types";
|
||||
|
||||
export const useCreateWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateWebhookDto>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.post("/api/v1/webhooks", dto);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useTestWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TTestWebhookDTO>({
|
||||
mutationFn: async ({ webhookId }) => {
|
||||
const { data } = await apiRequest.post(`/api/v1/webhooks/${webhookId}/test`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
},
|
||||
onError: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateWebhookDto>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/webhooks/${dto.webhookId}`, {
|
||||
isDisabled: dto.isDisabled
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteWebhook = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteWebhookDto>({
|
||||
mutationFn: async (dto) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/webhooks/${dto.webhookId}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(queryKeys.getWebhooks(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TWebhook } from "./types";
|
||||
|
||||
export const queryKeys = {
|
||||
getWebhooks: (workspaceId: string) => ["webhooks", { workspaceId }]
|
||||
};
|
||||
|
||||
const fetchWebhooks = async (workspaceId: string) => {
|
||||
const { data } = await apiRequest.get<{ webhooks: TWebhook[] }>("/api/v1/webhooks", {
|
||||
params: {
|
||||
workspaceId
|
||||
}
|
||||
});
|
||||
|
||||
return data.webhooks;
|
||||
};
|
||||
|
||||
export const useGetWebhooks = (workspaceId: string) =>
|
||||
useQuery({
|
||||
queryKey: queryKeys.getWebhooks(workspaceId),
|
||||
queryFn: () => fetchWebhooks(workspaceId),
|
||||
enabled: Boolean(workspaceId)
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
export type TWebhook = {
|
||||
_id: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url: string;
|
||||
lastStatus: "success" | "failed";
|
||||
lastRunErrorMessage?: string;
|
||||
isDisabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type TCreateWebhookDto = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
webhookUrl: string;
|
||||
webhookSecretKey?: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export type TUpdateWebhookDto = {
|
||||
webhookId: string;
|
||||
workspaceId: string;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export type TDeleteWebhookDto = {
|
||||
webhookId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type TTestWebhookDTO = {
|
||||
webhookId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
@@ -1,99 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import jwt_decode from "jwt-decode";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import SecurityClient, { PROVIDER_AUTH_TOKEN_KEY } from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export const useProviderAuth = () => {
|
||||
const router = useRouter();
|
||||
const { providerAuthToken: redirectedProviderAuthToken } = router.query;
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [userId, setUserId] = useState<string>("");
|
||||
const [providerAuthToken, setProviderAuthToken] = useState<string>(
|
||||
redirectedProviderAuthToken as string || ""
|
||||
);
|
||||
const [isProviderUserCompleted, setIsProviderUserCompleted] = useState<boolean>();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const AUTH_ERROR_KEY = "PROVIDER_AUTH_ERROR"
|
||||
|
||||
const handleRedirectWithToken = () => {
|
||||
if (providerAuthToken) {
|
||||
const {
|
||||
userId: resultUserId,
|
||||
email: resultEmail,
|
||||
isUserCompleted: resultIsUserCompleted,
|
||||
} = jwt_decode(providerAuthToken) as any;
|
||||
setEmail(resultEmail);
|
||||
setUserId(resultUserId);
|
||||
setIsProviderUserCompleted(resultIsUserCompleted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleRedirectWithToken();
|
||||
|
||||
// reset when there is no redirect auth token
|
||||
if (!providerAuthToken) {
|
||||
SecurityClient.setProviderAuthToken("");
|
||||
}
|
||||
|
||||
window.localStorage.removeItem(AUTH_ERROR_KEY);
|
||||
|
||||
const handleStorageChange = (event: StorageEvent) => {
|
||||
if (event.storageArea !== localStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === PROVIDER_AUTH_TOKEN_KEY) {
|
||||
if (event.newValue) {
|
||||
const token = event.newValue;
|
||||
const {
|
||||
userId: resultUserId,
|
||||
email: resultEmail,
|
||||
isUserCompleted: resultIsUserCompleted,
|
||||
} = jwt_decode(token) as any;
|
||||
setIsProviderUserCompleted(resultIsUserCompleted);
|
||||
setProviderAuthToken(token);
|
||||
setEmail(resultEmail);
|
||||
setUserId(resultUserId);
|
||||
} else {
|
||||
setProviderAuthToken("");
|
||||
setEmail("");
|
||||
setUserId("");
|
||||
setIsProviderUserCompleted(false);
|
||||
}
|
||||
setProviderAuthToken(event.newValue || "");
|
||||
}
|
||||
|
||||
if (event.key === AUTH_ERROR_KEY) {
|
||||
if (event.newValue) {
|
||||
createNotification({
|
||||
text: "An error has occured during login.",
|
||||
type: "error",
|
||||
timeoutMs: 6000,
|
||||
})
|
||||
|
||||
window.localStorage.removeItem(AUTH_ERROR_KEY);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("storage", handleStorageChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("storage", handleStorageChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
email,
|
||||
isProviderUserCompleted,
|
||||
providerAuthToken,
|
||||
userId,
|
||||
setEmail,
|
||||
setProviderAuthToken,
|
||||
setUserId,
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,10 @@ 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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
@@ -22,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) => [
|
||||
[
|
||||
@@ -67,6 +68,7 @@ export const Navbar = () => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { currentOrg, orgs } = useOrganization();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
const { user } = useUser();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
@@ -96,37 +98,6 @@ export const Navbar = () => {
|
||||
}
|
||||
};
|
||||
|
||||
function formatPlanSlug(slug: string) {
|
||||
return slug
|
||||
.replace(/(\b[a-z])/g, match => match.toUpperCase())
|
||||
.replace(/-/g, " ");
|
||||
}
|
||||
|
||||
const calculateRemainingDays = (date: number) => {
|
||||
const now = new Date();
|
||||
const endDate = new Date(date * 1000);
|
||||
|
||||
const differenceInTime = endDate.getTime() - now.getTime();
|
||||
const differenceInDays = Math.ceil(differenceInTime / (1000 * 3600 * 24));
|
||||
|
||||
return differenceInDays;
|
||||
}
|
||||
|
||||
const formatDate = (date: number) => {
|
||||
const endDate = new Date(date * 1000);
|
||||
const day: number = endDate.getDate();
|
||||
const month: number = endDate.getMonth() + 1;
|
||||
const year: number = endDate.getFullYear();
|
||||
|
||||
const formattedDate: string = `${day}/${month}/${year}`;
|
||||
const remainingDays: number = calculateRemainingDays(date);
|
||||
|
||||
return {
|
||||
formattedDate,
|
||||
remainingDays
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-[70] border-b border-mineshaft-500 bg-mineshaft-900 text-white">
|
||||
<div className="flex w-full justify-between px-4">
|
||||
@@ -209,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">
|
||||
@@ -346,12 +317,25 @@ export const Navbar = () => {
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
{subscription && subscription.status === "trialing" && subscription.trial_end && (
|
||||
<div className="w-full mx-auto border-t border-mineshaft-500">
|
||||
<p className="text-center py-4 text-sm">
|
||||
{`Currently trialing the ${formatPlanSlug(subscription.slug)} plan until ${formatDate(subscription.trial_end).formattedDate} - ${formatDate(subscription.trial_end).remainingDays} day(s) left. `}
|
||||
<Link href={`/settings/billing/${localStorage.getItem("projectData.id")}`}>Add a card to avoid being downgraded to the Starter plan afterward →</Link>
|
||||
</p>
|
||||
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||
<div className="w-full mx-auto border-t border-mineshaft-500 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (!subscription || !currentOrg) return;
|
||||
|
||||
// direct user to start pro trial
|
||||
const url = await mutateAsync({
|
||||
orgId: currentOrg._id,
|
||||
success_url: window.location.href
|
||||
});
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
className="text-center py-4 text-sm mx-auto"
|
||||
>
|
||||
You are currently on the <span className="font-semibold">Starter</span> plan. Unlock the full power of Infisical on the <span className="font-semibold">Pro Free Trial →</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { Navbar } from "./NavBar";
|
||||
@@ -21,11 +21,10 @@ export default function Custom404() {
|
||||
</a>{" "}
|
||||
and we`ll fix it!{" "}
|
||||
</p>
|
||||
<Link
|
||||
href='/dashboard'
|
||||
className='bg-mineshaft-500 mt-8 py-2 px-4 rounded-md hover:bg-primary diration-200 hover:text-black cursor-pointer font-semibold'
|
||||
>
|
||||
Go to Dashboard
|
||||
<Link href='/dashboard'>
|
||||
<div className="mt-8 bg-mineshaft-500 py-2 px-4 rounded-md hover:bg-primary diration-200 hover:text-black font-semibold cursor-default">
|
||||
Go to Dashboard
|
||||
</div>
|
||||
</Link>
|
||||
<Image
|
||||
src='/images/dragon-404.svg'
|
||||
|
||||
@@ -77,8 +77,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<AuthProvider>
|
||||
<WorkspaceProvider>
|
||||
<OrgProvider>
|
||||
<OrgProvider>
|
||||
<WorkspaceProvider>
|
||||
<SubscriptionProvider>
|
||||
<UserProvider>
|
||||
<NotificationProvider>
|
||||
@@ -90,8 +90,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
|
||||
</NotificationProvider>
|
||||
</UserProvider>
|
||||
</SubscriptionProvider>
|
||||
</OrgProvider>
|
||||
</WorkspaceProvider>
|
||||
</WorkspaceProvider>
|
||||
</OrgProvider>
|
||||
</AuthProvider>
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -70,7 +70,7 @@ const completeAccountInformationSignup = async ({
|
||||
verifier,
|
||||
organizationName,
|
||||
providerAuthToken,
|
||||
attributionSource,
|
||||
...(attributionSource ? { attributionSource } : {})
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const createNewIntegrationSession = (organizationId: string) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/create-installation-session/organization/${organizationId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to create integration session");
|
||||
console.log("response", res)
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default createNewIntegrationSession;
|
||||
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const getInstallationStatus = (organizationId: string) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/installation-status/organization/${organizationId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).appInstallationComplete;
|
||||
}
|
||||
console.log("Failed to check installation status");
|
||||
console.log("response", res)
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getInstallationStatus;
|
||||
@@ -0,0 +1,58 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export type GitRisks = {
|
||||
_id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
startColumn: string;
|
||||
endColumn: string;
|
||||
match: string;
|
||||
secret: string;
|
||||
file: string;
|
||||
symlinkFile: string;
|
||||
commit: string;
|
||||
entropy: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
message: string;
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
status: string;
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
riskOwner: string | null; // New field for setting a risk owner (nullable string)
|
||||
installationId: string,
|
||||
repositoryId: string,
|
||||
repositoryLink: string
|
||||
repositoryFullName: string
|
||||
pusher: {
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
createdAt: string,
|
||||
organization: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const getRisksByOrganization = (oranizationId: string): Promise<GitRisks[]> =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${oranizationId}/risks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).risks;
|
||||
}
|
||||
console.log("Failed to fetch risks");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getRisksByOrganization;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const linkGitAppInstallationWithOrganization = (installationId: string, sessionId: string) =>
|
||||
SecurityClient.fetchCall("/api/v1/secret-scanning/link-installation", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
installationId,
|
||||
sessionId
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return true
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default linkGitAppInstallationWithOrganization;
|
||||
@@ -0,0 +1,33 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export enum RiskStatus {
|
||||
RESOLVED_FALSE_POSITIVE = "RESOLVED_FALSE_POSITIVE",
|
||||
RESOLVED_REVOKED = "RESOLVED_REVOKED",
|
||||
RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED",
|
||||
UNRESOLVED = "UNRESOLVED",
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const updateRiskStatus = (organizationId: string, riskId: string, status: RiskStatus) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${organizationId}/risks/${riskId}/status`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default updateRiskStatus;
|
||||
@@ -1,4 +1,5 @@
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function CliRedirect() {
|
||||
return (
|
||||
@@ -8,9 +9,12 @@ export default function CliRedirect() {
|
||||
<link rel='icon' href='/infisical.ico' />
|
||||
</Head>
|
||||
<div className='flex flex-col items-center justify-center text-gray-200 h-screen w-screen'>
|
||||
<p className='text-4xl mt-32'>Head back to your terminal!</p>
|
||||
<p className='mt-2 mb-1 text-lg'>
|
||||
You've successfully logged into infisical-cli
|
||||
<div className="mb-8 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||
</div>
|
||||
<p className='text-3xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center'>Head back to your terminal</p>
|
||||
<p className='mb-1 text-lg text-light text-mineshaft-400'>
|
||||
You've successfully logged in to the Infisical CLI
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
import getOrganizations from "./api/organization/getOrgs";
|
||||
|
||||
export default function DashboardRedirect() {
|
||||
const router = useRouter();
|
||||
@@ -12,14 +12,14 @@ export default function DashboardRedirect() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let userWorkspace;
|
||||
let userOrg;
|
||||
try {
|
||||
if (localStorage.getItem("projectData.id")) {
|
||||
router.push(`/dashboard/${ localStorage.getItem("projectData.id")}`);
|
||||
if (localStorage.getItem("orgData.id")) {
|
||||
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
|
||||
} else {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
userWorkspace = userWorkspaces[0]._id;
|
||||
router.push(`/dashboard/${ userWorkspace}`);
|
||||
const userOrgs = await getOrganizations();
|
||||
userOrg = userOrgs[0]._id;
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faHandPeace,
|
||||
faNetworkWired,
|
||||
faPlug,
|
||||
faPlus,
|
||||
faStar,
|
||||
faUserPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import { TabsObject } from "@app/components/v2/Tabs";
|
||||
|
||||
import registerUserAction from "../api/userActions/registerUserAction";
|
||||
|
||||
type ItemProps = {
|
||||
text: string;
|
||||
subText: string;
|
||||
complete: boolean;
|
||||
icon: IconProp;
|
||||
time: string;
|
||||
userAction?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const learningItem = ({
|
||||
text,
|
||||
subText,
|
||||
complete,
|
||||
icon,
|
||||
time,
|
||||
userAction,
|
||||
link
|
||||
}: ItemProps): JSX.Element => {
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
target={`${link.includes("https") ? "_blank" : "_self"}`}
|
||||
rel="noopener noreferrer"
|
||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||
href={link}
|
||||
>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction && userAction !== "first_time_secrets_pushed") {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="group relative mb-3 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-bunker-500 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-mineshaft-700"
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction) {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500"
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-11 top-10 h-7 w-7 rounded-full bg-bunker-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="absolute left-12 top-16 h-5 w-5 text-4xl text-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="mt-0.5 text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This tab is called Home because in the future it will include some company news,
|
||||
* updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction
|
||||
* for the new users
|
||||
*/
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
||||
const [hasUserStarred, setHasUserStarred] = useState(false);
|
||||
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
||||
const [usersInOrg, setUsersInOrg] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onboardingCheck({
|
||||
setHasUserClickedIntro,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserPushedSecrets,
|
||||
setHasUserStarred,
|
||||
setUsersInOrg
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-6 w-full pt-4 lg:mx-0">
|
||||
<Head>
|
||||
<title>Infisical Guide</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="relative mx-auto flex max-w-3xl flex-col items-center px-6 py-6 text-lg text-gray-300 lg:max-w-4xl xl:max-w-5xl">
|
||||
<div className="mt-12 w-full text-left text-5xl font-bold">Your quick start guide</div>
|
||||
<div className="mb-14 w-full pt-2 pb-4 text-left text-lg text-bunker-300">
|
||||
Click on the items below and follow the instructions.
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 hidden h-min lg:block">
|
||||
<Image
|
||||
src="/images/dragon-book.svg"
|
||||
height={250}
|
||||
width={400}
|
||||
alt="start guise dragon illustration"
|
||||
/>
|
||||
</div>
|
||||
{learningItem({
|
||||
text: "Get to know Infisical",
|
||||
subText: "",
|
||||
complete: hasUserClickedIntro,
|
||||
icon: faHandPeace,
|
||||
time: "3 min",
|
||||
userAction: "intro_cta_clicked",
|
||||
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Add your secrets",
|
||||
subText: "Click to see example secrets, and add your own.",
|
||||
complete: hasUserPushedSecrets,
|
||||
icon: faPlus,
|
||||
time: "2 min",
|
||||
userAction: "first_time_secrets_pushed",
|
||||
link: `/dashboard/${router.query.id}`
|
||||
})}
|
||||
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-bunker-500 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-green" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>
|
||||
{learningItem({
|
||||
text: "Integrate Infisical with your infrastructure",
|
||||
subText: "Connect Infisical to various 3rd party services and platforms.",
|
||||
complete: false,
|
||||
icon: faPlug,
|
||||
time: "15 min",
|
||||
link: "https://infisical.com/docs/integrations/overview"
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Invite your teammates",
|
||||
subText: "",
|
||||
complete: usersInOrg,
|
||||
icon: faUserPlus,
|
||||
time: "2 min",
|
||||
link: `/settings/org/${router.query.id}?invite`
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Join Infisical Slack",
|
||||
subText: "Have any questions? Ask us!",
|
||||
complete: hasUserClickedSlack,
|
||||
icon: faSlack,
|
||||
time: "1 min",
|
||||
userAction: "slack_cta_clicked",
|
||||
link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg"
|
||||
})}
|
||||
{learningItem({
|
||||
text: "Star Infisical on GitHub",
|
||||
subText: "Like what we're doing? You know what to do! :)",
|
||||
complete: hasUserStarred,
|
||||
icon: faStar,
|
||||
time: "1 min",
|
||||
userAction: "star_cta_clicked",
|
||||
link: "https://github.com/Infisical/infisical"
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Home.requireAuth = true;
|
||||
@@ -0,0 +1,198 @@
|
||||
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,
|
||||
useGetIntegrationAuthBitBucketWorkspaces,
|
||||
useGetIntegrationAuthById,
|
||||
} from "../../../hooks/api/integrationAuth";
|
||||
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
|
||||
import createIntegration from "../../api/integrations/createIntegration";
|
||||
|
||||
export default function BitBucketCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [targetAppId, setTargetAppId] = useState("");
|
||||
const [targetEnvironmentId, setTargetEnvironmentId] = useState("");
|
||||
|
||||
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
|
||||
const [secretPath, setSecretPath] = useState("/");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
|
||||
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
|
||||
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
|
||||
const { data: targetEnvironments } = useGetIntegrationAuthBitBucketWorkspaces((integrationAuthId as string) ?? "");
|
||||
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
|
||||
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||
workspaceSlug: targetEnvironmentId
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (workspace) {
|
||||
setSelectedSourceEnvironment(workspace.environments[0].slug);
|
||||
}
|
||||
}, [workspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (integrationAuthApps) {
|
||||
if (integrationAuthApps.length > 0) {
|
||||
setTargetAppId(integrationAuthApps[0].appId as string);
|
||||
} else {
|
||||
setTargetAppId("none");
|
||||
}
|
||||
}
|
||||
}, [integrationAuthApps]);
|
||||
|
||||
useEffect(() => {
|
||||
if (targetEnvironments) {
|
||||
if (targetEnvironments.length > 0) {
|
||||
setTargetEnvironmentId(targetEnvironments[0].slug);
|
||||
} else {
|
||||
setTargetEnvironmentId("none");
|
||||
}
|
||||
}
|
||||
}, [targetEnvironments]);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
if (!integrationAuth?._id) return;
|
||||
|
||||
const targetApp = integrationAuthApps?.find(
|
||||
(integrationAuthApp) => integrationAuthApp.appId === targetAppId
|
||||
);
|
||||
const targetEnvironment = targetEnvironments?.find(
|
||||
(environment) => environment.slug === targetEnvironmentId
|
||||
);
|
||||
|
||||
if (!targetApp || !targetApp.appId || !targetEnvironment) return;
|
||||
|
||||
await createIntegration({
|
||||
integrationAuthId: integrationAuth?._id,
|
||||
isActive: true,
|
||||
app: targetApp.name,
|
||||
appId: targetApp.appId,
|
||||
sourceEnvironment: selectedSourceEnvironment,
|
||||
targetEnvironment: targetEnvironment.name,
|
||||
targetEnvironmentId: targetEnvironment.slug,
|
||||
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 &&
|
||||
targetEnvironments ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<Card className="max-w-md rounded-md p-8">
|
||||
<CardTitle className="text-center">BitBucket 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="BitBucket Workspace">
|
||||
<Select
|
||||
value={targetEnvironmentId}
|
||||
onValueChange={(val) => setTargetEnvironmentId(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
isDisabled={targetEnvironments.length === 0}
|
||||
>
|
||||
{targetEnvironments.length > 0 ? (
|
||||
targetEnvironments.map((targetEnvironment) => (
|
||||
<SelectItem
|
||||
value={targetEnvironment.slug as string}
|
||||
key={`target-environment-${targetEnvironment.slug as string}`}
|
||||
>
|
||||
{targetEnvironment.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-environment-none">
|
||||
No workspaces found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl label="BitBucket Repo">
|
||||
<Select
|
||||
value={targetAppId}
|
||||
onValueChange={(val) => setTargetAppId(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{integrationAuthApps.length > 0 ? (
|
||||
integrationAuthApps.map((integrationAuthApp) => (
|
||||
<SelectItem
|
||||
value={integrationAuthApp.appId as string}
|
||||
key={`target-app-${integrationAuthApp.appId as string}`}
|
||||
>
|
||||
{integrationAuthApp.name}
|
||||
</SelectItem>
|
||||
))
|
||||
) : (
|
||||
<SelectItem value="none" key="target-app-none">
|
||||
No repositories found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
}
|
||||
|
||||
BitBucketCreateIntegrationPage.requireAuth = true;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import queryString from "query-string";
|
||||
|
||||
import AuthorizeIntegration from "../../../api/integrations/authorizeIntegration";
|
||||
|
||||
export default function BitBucketOAuth2CallbackPage() {
|
||||
const router = useRouter();
|
||||
const { code, state } = queryString.parse(router.asPath.split("?")[1]);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
// validate state
|
||||
if (state !== localStorage.getItem("latestCSRFToken")) return;
|
||||
localStorage.removeItem("latestCSRFToken");
|
||||
|
||||
const integrationAuth = await AuthorizeIntegration({
|
||||
workspaceId: localStorage.getItem("projectData.id") as string,
|
||||
code: code as string,
|
||||
integration: "bitbucket"
|
||||
});
|
||||
|
||||
router.push(`/integrations/bitbucket/create?integrationAuthId=${integrationAuth._id}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div />;
|
||||
}
|
||||
|
||||
BitBucketOAuth2CallbackPage.requireAuth = true;
|
||||
@@ -0,0 +1,64 @@
|
||||
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 CodefreshCreateIntegrationPage() {
|
||||
const router = useRouter();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleButtonClick = async () => {
|
||||
try {
|
||||
setApiKeyErrorText("");
|
||||
if (apiKey.length === 0) {
|
||||
setApiKeyErrorText("API Key cannot be blank");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const integrationAuth = await saveIntegrationAccessToken({
|
||||
workspaceId: localStorage.getItem("projectData.id"),
|
||||
integration: "codefresh",
|
||||
accessId: null,
|
||||
accessToken: apiKey,
|
||||
url: null,
|
||||
namespace: null
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
|
||||
router.push(`/integrations/codefresh/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">Codefresh Integration</CardTitle>
|
||||
<FormControl
|
||||
label="Codefresh API Key"
|
||||
errorText={apiKeyErrorText}
|
||||
isError={apiKeyErrorText !== "" ?? false}
|
||||
>
|
||||
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Connect to Codefresh
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CodefreshCreateIntegrationPage.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 CodefreshCreateIntegrationPage() {
|
||||
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">Codefresh 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="Codefresh Service" 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 services found
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
color="mineshaft"
|
||||
className="mt-4"
|
||||
isLoading={isLoading}
|
||||
isDisabled={integrationAuthApps.length === 0}
|
||||
>
|
||||
Create Integration
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
}
|
||||
|
||||
CodefreshCreateIntegrationPage.requireAuth = true;
|
||||
@@ -1,140 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import axios from "axios"
|
||||
|
||||
// import ListBox from '@app/components/basic/Listbox';
|
||||
import InitialLoginStep from "@app/components/login/InitialLoginStep";
|
||||
import MFAStep from "@app/components/login/MFAStep";
|
||||
import PasswordInputStep from "@app/components/login/PasswordInputStep";
|
||||
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";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// const lang = router.locale ?? 'en';
|
||||
const {
|
||||
providerAuthToken,
|
||||
email: providerEmail,
|
||||
setProviderAuthToken,
|
||||
isProviderUserCompleted
|
||||
} = useProviderAuth();
|
||||
|
||||
if (providerAuthToken && isProviderUserCompleted === false) {
|
||||
router.push(`/signup?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
|
||||
}
|
||||
|
||||
// const setLanguage = async (to: string) => {
|
||||
// router.push('/login', '/login', { locale: to });
|
||||
// localStorage.setItem('lang', to);
|
||||
// };
|
||||
|
||||
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;
|
||||
|
||||
// user details
|
||||
const userDetails = await fetchUserDetails()
|
||||
// send details back to client
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search)
|
||||
if (queryParams && queryParams.get("callback_port")) {
|
||||
const callbackPort = queryParams.get("callback_port")
|
||||
|
||||
// send post request to cli with details
|
||||
const cliUrl = `http://localhost:${callbackPort}`
|
||||
const instance = axios.create()
|
||||
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
|
||||
}
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
};
|
||||
if (isLoggedIn()) {
|
||||
redirectToDashboard();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const renderView = (loginStep: number) => {
|
||||
if (providerAuthToken && step === 1) {
|
||||
return (
|
||||
<PasswordInputStep
|
||||
email={providerEmail}
|
||||
password={password}
|
||||
providerAuthToken={providerAuthToken}
|
||||
setPassword={setPassword}
|
||||
setProviderAuthToken={setProviderAuthToken}
|
||||
setStep={setStep}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (loginStep === 1) {
|
||||
return <InitialLoginStep
|
||||
setStep={setStep}
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
/>;
|
||||
}
|
||||
|
||||
if (step === 2) {
|
||||
return (
|
||||
<MFAStep
|
||||
email={email || providerEmail}
|
||||
password={password}
|
||||
providerAuthToken={providerAuthToken}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||
</Head>
|
||||
<Link href="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||
</div>
|
||||
</Link>
|
||||
{renderView(step)}
|
||||
{/* <div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
|
||||
<div className="mx-auto w-48">
|
||||
<ListBox
|
||||
isSelected={lang}
|
||||
onChange={setLanguage}
|
||||
data={['en', 'ko', 'fr', 'pt-BR']}
|
||||
isFull
|
||||
text={`${t('common.language')}: `}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Login } from "@app/views/Login";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||
</Head>
|
||||
<Link href="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||
</div>
|
||||
</Link>
|
||||
<Login />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { LoginSSO } from "@app/views/Login";
|
||||
|
||||
export default function LoginSSOPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const token = router.query.token as string;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||
</Head>
|
||||
<Link href="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||
</div>
|
||||
</Link>
|
||||
<LoginSSO providerAuthToken={token} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import Button from "@app/components/basic/buttons/Button";
|
||||
|
||||
import getOrganizationUserProjects from "./api/organization/GetOrgUserProjects";
|
||||
|
||||
export default function NoProjects() {
|
||||
const router = useRouter();
|
||||
|
||||
const redirectUser = async () => {
|
||||
const workspaces = await getOrganizationUserProjects({
|
||||
orgId: String(localStorage.getItem("orgData.id"))
|
||||
});
|
||||
if (workspaces.length > 0) {
|
||||
router.push(`/dashboard/${workspaces[0]._id}`);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// on initial load - run auth check
|
||||
(async () => {
|
||||
await redirectUser();
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mr-auto flex h-full w-11/12 flex-col items-center justify-center text-center text-lg text-gray-300">
|
||||
<Head>
|
||||
<title>No Projects | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<div className="mb-6 mt-16 mr-16">
|
||||
<Image src="/images/dragon-cant-find.svg" height={270} width={436} alt="google logo" />
|
||||
</div>
|
||||
<div className="mb-8 rounded-md bg-mineshaft-900 border border-mineshaft-700 px-4 py-6 text-bunker-300 shadow-xl">
|
||||
<div className="max-w-md">
|
||||
You are not part of any projects in this organization yet. When you are, they will appear
|
||||
here.
|
||||
</div>
|
||||
<div className="mt-4 max-w-md">
|
||||
Create a new project, or ask other organization members to give you necessary permissions.
|
||||
</div>
|
||||
<div className="mx-2 mt-6">
|
||||
<Button text="Check again" onButtonPressed={redirectUser} size="md" color="mineshaft" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoProjects.requireAuth = true;
|
||||
+1
@@ -11,6 +11,7 @@ export default function SettingsBilling() {
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<BillingSettingsPage />
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { MembersPage } from "@app/views/Org/MembersPage";
|
||||
|
||||
export default function SettingsOrg() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<MembersPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsOrg.requireAuth = true;
|
||||
@@ -0,0 +1,512 @@
|
||||
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faArrowRight, faCheckCircle, faHandPeace, faMagnifyingGlass, faNetworkWired, faPlug, faPlus, faUserPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import { Button, Checkbox, FormControl, Input, Modal, ModalContent, UpgradePlanModal } from "@app/components/v2";
|
||||
import { TabsObject } from "@app/components/v2/Tabs";
|
||||
import { useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
|
||||
import registerUserAction from "../../../api/userActions/registerUserAction";
|
||||
|
||||
const features = [{
|
||||
"_id": 0,
|
||||
"name": "Kubernetes Operator",
|
||||
"description": "Pull secrets into your Kubernetes containers and automatically redeploy upon secret changes."
|
||||
}]
|
||||
|
||||
type ItemProps = {
|
||||
text: string;
|
||||
subText: string;
|
||||
complete: boolean;
|
||||
icon: IconProp;
|
||||
time: string;
|
||||
userAction?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const learningItem = ({
|
||||
text,
|
||||
subText,
|
||||
complete,
|
||||
icon,
|
||||
time,
|
||||
userAction,
|
||||
link
|
||||
}: ItemProps): JSX.Element => {
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
target={`${link.includes("https") ? "_blank" : "_self"}`}
|
||||
rel="noopener noreferrer"
|
||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||
href={link}
|
||||
>
|
||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} mb-3 rounded-md`}>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction && userAction !== "first_time_secrets_pushed") {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={`group relative flex h-[5.5rem] w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-32 pr-8 text-right text-sm font-semibold ${complete && "text-primary"}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{/* {complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />} */}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction) {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500"
|
||||
>
|
||||
<div className="mr-4 flex flex-row items-center">
|
||||
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
|
||||
{complete && (
|
||||
<div className="absolute left-11 top-10 h-7 w-7 rounded-full bg-bunker-700">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="absolute left-12 top-16 h-5 w-5 text-4xl text-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="mt-0.5 text-xl font-semibold">{text}</div>
|
||||
<div className="mt-0.5 text-sm font-normal">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const learningItemSquare = ({
|
||||
text,
|
||||
subText,
|
||||
complete,
|
||||
icon,
|
||||
time,
|
||||
userAction,
|
||||
link
|
||||
}: ItemProps): JSX.Element => {
|
||||
return (
|
||||
<a
|
||||
target={`${link?.includes("https") ? "_blank" : "_self"}`}
|
||||
rel="noopener noreferrer"
|
||||
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
|
||||
href={link}
|
||||
>
|
||||
<div className={`${complete ? "bg-gradient-to-r from-primary-500/70 p-[0.07rem]" : ""} rounded-md w-full`}>
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={async () => {
|
||||
if (userAction && userAction !== "first_time_secrets_pushed") {
|
||||
await registerUserAction({
|
||||
action: userAction
|
||||
});
|
||||
}
|
||||
}}
|
||||
className={`group relative flex w-full items-center justify-between overflow-hidden rounded-md border ${complete? "bg-gradient-to-r from-[#0e1f01] to-mineshaft-700 border-mineshaft-900 cursor-default" : "bg-mineshaft-800 hover:bg-mineshaft-700 border-mineshaft-600 shadow-xl cursor-pointer"} duration-200 text-mineshaft-100`}
|
||||
>
|
||||
<div className="flex flex-col items-center w-full px-6 py-4">
|
||||
<div className="flex flex-row items-start justify-between w-full">
|
||||
<FontAwesomeIcon icon={icon} className="w-16 text-5xl text-mineshaft-200 group-hover:text-mineshaft-100 duration-100 pt-2" />
|
||||
{complete && (
|
||||
<div className="absolute left-14 top-12 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`text-right text-sm font-normal text-mineshaft-300 ${complete ? "text-primary font-semibold" : ""}`}
|
||||
>
|
||||
{complete ? "Complete!" : `About ${time}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start w-full pt-4">
|
||||
<div className="mt-0.5 text-lg font-medium">{text}</div>
|
||||
<div className="text-sm font-normal text-mineshaft-300">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("Project Name").trim(),
|
||||
addMembers: yup.bool().required().label("Add Members")
|
||||
});
|
||||
|
||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
// #TODO: Update all the workspaceIds
|
||||
|
||||
export default function Organization() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { workspaces } = useWorkspace();
|
||||
const orgWorkspaces = workspaces?.filter(workspace => workspace.organization === localStorage.getItem("orgData.id")) || []
|
||||
const currentOrg = String(router.query.id);
|
||||
const { createNotification } = useNotificationContext();
|
||||
const addWsUser = useAddUserToWs();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm<TAddProjectFormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
|
||||
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
||||
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
||||
const [usersInOrg, setUsersInOrg] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const createWs = useCreateWorkspace();
|
||||
const { user } = useUser();
|
||||
const uploadWsKey = useUploadWsKey();
|
||||
|
||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
workspace: { _id: newWorkspaceId }
|
||||
}
|
||||
} = await createWs.mutateAsync({
|
||||
organizationId: currentOrg,
|
||||
workspaceName: name
|
||||
});
|
||||
|
||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: randomBytes,
|
||||
publicKey: user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadWsKey.mutateAsync({
|
||||
encryptedKey: ciphertext,
|
||||
nonce,
|
||||
userId: user?._id,
|
||||
workspaceId: newWorkspaceId
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
// not using hooks because need at this point only
|
||||
const orgUsers = await fetchOrgUsers(currentOrg);
|
||||
orgUsers.forEach(({ status, user: orgUser }) => {
|
||||
// skip if status of org user is not accepted
|
||||
// this orgUser is the person who created the ws
|
||||
if (status !== "accepted" || user.email === orgUser.email) return;
|
||||
addWsUser.mutate({ email: orgUser.email, workspaceId: newWorkspaceId });
|
||||
});
|
||||
}
|
||||
createNotification({ text: "Workspace created", type: "success" });
|
||||
handlePopUpClose("addNewWs");
|
||||
router.push(`/project/${newWorkspaceId}/secrets`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create workspace", type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit ? (subscription.workspacesUsed < subscription.workspaceLimit) : true;
|
||||
|
||||
useEffect(() => {
|
||||
onboardingCheck({
|
||||
setHasUserClickedIntro,
|
||||
setHasUserClickedSlack,
|
||||
setHasUserPushedSecrets,
|
||||
setUsersInOrg
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||
<div className="w-full flex flex-row mt-6">
|
||||
<Input
|
||||
className="h-[2.3rem] text-sm bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||
placeholder="Search by project name..."
|
||||
value={searchFilter}
|
||||
onChange={(e) => setSearchFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
/>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (isAddingProjectsAllowed) {
|
||||
handlePopUpOpen("addNewWs")
|
||||
} else {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}}
|
||||
className="ml-2"
|
||||
>
|
||||
Add New Project
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 w-full grid gap-4 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
{orgWorkspaces.filter(ws => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase())).map(workspace => <div key={workspace._id} className="h-40 min-w-72 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
||||
<div className="text-lg text-mineshaft-100 mt-0">{workspace.name}</div>
|
||||
<div className="text-sm text-mineshaft-300 mt-0 pb-6">{(workspace.environments?.length || 0)} environments</div>
|
||||
<Link href={`/project/${workspace._id}/secrets`}>
|
||||
<div className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80">Explore <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200" /></div>
|
||||
</Link>
|
||||
</div>)}
|
||||
</div>
|
||||
{orgWorkspaces.length === 0 && (
|
||||
<div className="w-full rounded-md bg-mineshaft-800 border border-mineshaft-700 px-4 py-6 text-mineshaft-300 text-base">
|
||||
<FontAwesomeIcon icon={faFolderOpen} className="w-full text-center text-5xl mb-4 mt-2 text-mineshaft-400" />
|
||||
<div className="text-center font-light">
|
||||
You are not part of any projects in this organization yet. When you are, they will appear
|
||||
here.
|
||||
</div>
|
||||
<div className="mt-0.5 text-center font-light">
|
||||
Create a new project, or ask other organization members to give you necessary permissions.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{((new Date()).getTime() - (new Date(user?.createdAt)).getTime()) < 30 * 24 * 60 * 60 * 1000 && <div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
|
||||
<p className="mr-4 font-semibold text-white mb-4">Onboarding Guide</p>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 w-full mb-3">
|
||||
{learningItemSquare({
|
||||
text: "Watch Infisical demo",
|
||||
subText: "Set up Infisical in 3 min.",
|
||||
complete: hasUserClickedIntro,
|
||||
icon: faHandPeace,
|
||||
time: "3 min",
|
||||
userAction: "intro_cta_clicked",
|
||||
link: "https://www.youtube.com/watch?v=PK23097-25I"
|
||||
})}
|
||||
{orgWorkspaces.length !== 0 && learningItemSquare({
|
||||
text: "Add your secrets",
|
||||
subText: "Drop a .env file or type your secrets.",
|
||||
complete: hasUserPushedSecrets,
|
||||
icon: faPlus,
|
||||
time: "1 min",
|
||||
userAction: "first_time_secrets_pushed",
|
||||
link: `/project/${orgWorkspaces[0]?._id}/secrets`
|
||||
})}
|
||||
{learningItemSquare({
|
||||
text: "Invite your teammates",
|
||||
subText: "Infisical is better used as a team.",
|
||||
complete: usersInOrg,
|
||||
icon: faUserPlus,
|
||||
time: "2 min",
|
||||
link: `/org/${router.query.id}/members?action=invite`
|
||||
})}
|
||||
<div className="block xl:hidden 2xl:block">{learningItemSquare({
|
||||
text: "Join Infisical Slack",
|
||||
subText: "Have any questions? Ask us!",
|
||||
complete: hasUserClickedSlack,
|
||||
icon: faSlack,
|
||||
time: "1 min",
|
||||
userAction: "slack_cta_clicked",
|
||||
link: "https://infisical.com/slack"
|
||||
})}</div>
|
||||
</div>
|
||||
{orgWorkspaces.length !== 0 && <div className="group text-mineshaft-100 relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-800 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
|
||||
<div className="mb-4 flex w-full flex-row items-center pr-4">
|
||||
<div className="mr-4 flex w-full flex-row items-center">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
|
||||
{false && (
|
||||
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-green" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start pl-0.5">
|
||||
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">
|
||||
Replace .env files with a more secure and efficient alternative.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
<TabsObject />
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>}
|
||||
{orgWorkspaces.length !== 0 && learningItem({
|
||||
text: "Integrate Infisical with your infrastructure",
|
||||
subText: "Connect Infisical to various 3rd party services and platforms.",
|
||||
complete: false,
|
||||
icon: faPlug,
|
||||
time: "15 min",
|
||||
link: "https://infisical.com/docs/integrations/overview"
|
||||
})}
|
||||
</div>}
|
||||
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4 pb-6">
|
||||
<p className="mr-4 font-semibold text-white">Explore More</p>
|
||||
<div className="mt-4 w-full grid grid-flow-dense gap-4" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(256px, 4fr))" }}>
|
||||
{features.map(feature => <div key={feature._id} className="h-44 w-96 rounded-md bg-mineshaft-800 border border-mineshaft-600 p-4 flex flex-col justify-between">
|
||||
<div className="text-lg text-mineshaft-100 mt-0">{feature.name}</div>
|
||||
<div className="text-[15px] font-light text-mineshaft-300 mb-4 mt-2">{feature.description}</div>
|
||||
<div className="w-full flex items-center">
|
||||
<div className="text-mineshaft-300 text-[15px] font-light">Setup time: 20 min</div>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group cursor-default ml-auto hover:bg-primary-800/20 text-sm text-mineshaft-300 hover:text-mineshaft-200 bg-mineshaft-900 py-2 px-4 rounded-full w-max border border-mineshaft-600 hover:border-primary-500/80"
|
||||
href="https://infisical.com/docs/documentation/getting-started/kubernetes"
|
||||
>
|
||||
Learn more <FontAwesomeIcon icon={faArrowRight} className="pl-1.5 pr-0.5 group-hover:pl-2 group-hover:pr-0 duration-200"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isModalOpen) => {
|
||||
handlePopUpToggle("addNewWs", isModalOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create a new project"
|
||||
subTitle="This project will contain your secrets and configurations."
|
||||
>
|
||||
<form onSubmit={handleSubmit(onCreateProject)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Type your project name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-4 pl-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="addMembers"
|
||||
defaultValue
|
||||
render={({ field: { onBlur, value, onChange } }) => (
|
||||
<Checkbox
|
||||
id="add-project-layout"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
Add all members of my organization to this project
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-7 flex items-center">
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="mr-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Project
|
||||
</Button>
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => handlePopUpClose("addNewWs")}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You have exceeded the number of projects allowed on the free plan."
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Organization.requireAuth = true;
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { SecretScanningLogsTable } from "@app/views/SecretScanning/components";
|
||||
|
||||
import createNewIntegrationSession from "../../../api/secret-scanning/createSecretScanningSession";
|
||||
import getInstallationStatus from "../../../api/secret-scanning/getInstallationStatus";
|
||||
import linkGitAppInstallationWithOrganization from "../../../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter()
|
||||
const queryParams = router.query
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||
|
||||
useEffect(()=>{
|
||||
const linkInstallation = async () => {
|
||||
if (typeof queryParams.state === "string" && typeof queryParams.installation_id === "string"){
|
||||
try {
|
||||
const isLinked = await linkGitAppInstallationWithOrganization(queryParams.installation_id as string, queryParams.state as string)
|
||||
if (isLinked){
|
||||
router.reload()
|
||||
}
|
||||
|
||||
console.log("installation verification complete")
|
||||
}catch (e){
|
||||
console.log("app installation is stale, start new session", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInstallationStatus = async () => {
|
||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||
setIntegrationStatus(status)
|
||||
}
|
||||
|
||||
fetchInstallationStatus()
|
||||
linkInstallation()
|
||||
},[queryParams.state, queryParams.installation_id])
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
|
||||
<div className="max-w-7xl px-6 w-full">
|
||||
<div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
|
||||
<div className="mb-6 text-lg text-mineshaft-300">Automatically monitor your GitHub activity and prevent secret leaks</div>
|
||||
<div className="relative flex justify-between bg-mineshaft-800 border border-mineshaft-600 rounded-md p-6 mb-6">
|
||||
<div className="flex flex-col items-start">
|
||||
<div className="flex flex-row mb-1">Secret Scanning Status: {integrationEnabled ? <p className="text-green ml-1.5 font-semibold">Enabled</p> : <p className="text-red ml-1.5 font-semibold">Not enabled</p>}</div>
|
||||
<div>{integrationEnabled ? <p className="text-mineshaft-300">Your GitHub organization is connected to Infisical, and is being continuously monitored for secret leaks.</p> : <p className="text-mineshaft-300">Connect your GitHub organization to Infisical.</p>}</div>
|
||||
</div>
|
||||
{integrationEnabled ? (
|
||||
<div>
|
||||
<div className="absolute right-[2.5rem] top-[2.5rem] animate-ping rounded-full h-6 w-6 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.63rem] top-[2.63rem] animate-ping rounded-full h-5 w-5 bg-green flex items-center justify-center"/>
|
||||
<div className="absolute right-[2.82rem] top-[2.82rem] animate-ping rounded-full h-3.5 w-3.5 bg-green flex items-center justify-center"/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center h-[3.25rem]">
|
||||
<Button
|
||||
variant="solid"
|
||||
colorSchema="primary"
|
||||
onClick={generateNewIntegrationSession}
|
||||
className="py-2 h-min"
|
||||
>
|
||||
Integrate with GitHub
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<SecretScanningLogsTable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SecretScanning.requireAuth = true;
|
||||
@@ -1,6 +1,6 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FormEvent, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router";
|
||||
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
@@ -24,6 +24,7 @@ const client = new jsrp.client();
|
||||
export default function PasswordReset() {
|
||||
const [verificationToken, setVerificationToken] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [backupKey, setBackupKey] = useState("");
|
||||
const [privateKey, setPrivateKey] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
@@ -38,7 +39,8 @@ export default function PasswordReset() {
|
||||
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
|
||||
|
||||
// Unencrypt the private key with a backup key
|
||||
const getEncryptedKeyHandler = async () => {
|
||||
const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
||||
|
||||
@@ -57,7 +59,8 @@ export default function PasswordReset() {
|
||||
};
|
||||
|
||||
// If everything is correct, reset the password
|
||||
const resetPasswordHandler = async () => {
|
||||
const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const errorCheck = passwordCheck({
|
||||
password: newPassword,
|
||||
setPasswordErrorLength,
|
||||
@@ -125,6 +128,7 @@ export default function PasswordReset() {
|
||||
if (response?.status === 200) {
|
||||
router.push("/login");
|
||||
}
|
||||
setLoading(false)
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -162,7 +166,7 @@ export default function PasswordReset() {
|
||||
|
||||
// Input backup key
|
||||
const stepInputBackupKey = (
|
||||
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<form onSubmit={getEncryptedKeyHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Enter your backup key
|
||||
</p>
|
||||
@@ -186,18 +190,19 @@ export default function PasswordReset() {
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text="Submit Backup Key"
|
||||
onButtonPressed={() => getEncryptedKeyHandler()}
|
||||
onButtonPressed={() => {}}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
// Enter new password
|
||||
const stepEnterNewPassword = (
|
||||
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<form onSubmit={resetPasswordHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
|
||||
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Enter new password
|
||||
</p>
|
||||
@@ -269,13 +274,15 @@ export default function PasswordReset() {
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text="Submit New Password"
|
||||
onButtonPressed={() => resetPasswordHandler()}
|
||||
onButtonPressed={() => setLoading(true)}
|
||||
size="lg"
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
+5
-9
@@ -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,20 +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>
|
||||
<div className="ml-6">
|
||||
<NavHeader pageName="Audit Logs" isProjectRelated />
|
||||
</div>
|
||||
{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
-13
@@ -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,14 +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>
|
||||
<div className="ml-6">
|
||||
<NavHeader pageName={t("settings.members.title")} isProjectRelated />
|
||||
</div>
|
||||
<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>
|
||||
@@ -174,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"
|
||||
@@ -194,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}
|
||||
@@ -1,90 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
import { isLoggedIn } from "@app/reactQuery";
|
||||
|
||||
import getWorkspaces from "./api/workspace/getWorkspaces";
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [password, setPassword] = useState("");
|
||||
const { t } = useTranslation();
|
||||
|
||||
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;
|
||||
router.push(`/dashboard/${userWorkspace}`);
|
||||
} catch (error) {
|
||||
console.log("Error - Not logged in yet");
|
||||
}
|
||||
};
|
||||
if (isLoggedIn()) {
|
||||
redirectToDashboard();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("login.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={t("login.og-title") ?? ""} />
|
||||
<meta name="og:description" content={t("login.og-description") ?? ""} />
|
||||
</Head>
|
||||
<Link href="/">
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
|
||||
</div>
|
||||
</Link>
|
||||
<div className="mx-auto w-full max-w-md md:px-6">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||
What’s your email?
|
||||
</p>
|
||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md: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 rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type="email"
|
||||
placeholder="Enter your email..."
|
||||
isRequired
|
||||
autoComplete="email"
|
||||
id="email"
|
||||
className="h-12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {}}
|
||||
isFullWidth
|
||||
className="h-14"
|
||||
>
|
||||
{t("login.login")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-center mt-4">
|
||||
<button
|
||||
onClick={() => {router.push("/login")}}
|
||||
type="button"
|
||||
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
||||
>
|
||||
{t("login.other-option")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -13,10 +13,8 @@ import TeamInviteStep from "@app/components/signup/TeamInviteStep";
|
||||
import UserInfoStep from "@app/components/signup/UserInfoStep";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
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 checkEmailVerificationCode from "@app/pages/api/auth/CheckEmailVerificationCode";
|
||||
import getOrganizations from "@app/pages/api/organization/getOrgs";
|
||||
|
||||
/**
|
||||
* @returns the signup page
|
||||
@@ -35,21 +33,12 @@ export default function SignUp() {
|
||||
const [isSignupWithEmail, setIsSignupWithEmail] = useState(false);
|
||||
const [isCodeInputCheckLoading, setIsCodeInputCheckLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const { email: providerEmail, providerAuthToken, isProviderUserCompleted } = useProviderAuth();
|
||||
|
||||
if (providerAuthToken && isProviderUserCompleted) {
|
||||
router.push(`/login?providerAuthToken=${encodeURIComponent(providerAuthToken)}`);
|
||||
}
|
||||
|
||||
if (providerAuthToken && step < 3) {
|
||||
setStep(3);
|
||||
}
|
||||
|
||||
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 +79,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]);
|
||||
@@ -121,7 +110,7 @@ export default function SignUp() {
|
||||
return (
|
||||
<UserInfoStep
|
||||
incrementStep={incrementStep}
|
||||
email={email || providerEmail}
|
||||
email={email}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
name={name}
|
||||
@@ -130,7 +119,7 @@ export default function SignUp() {
|
||||
setOrganizationName={setOrganizationName}
|
||||
attributionSource={attributionSource}
|
||||
setAttributionSource={setAttributionSource}
|
||||
providerAuthToken={providerAuthToken}
|
||||
providerAuthToken={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -139,7 +128,7 @@ export default function SignUp() {
|
||||
return (
|
||||
<DownloadBackupPDF
|
||||
incrementStep={incrementStep}
|
||||
email={email || providerEmail}
|
||||
email={email}
|
||||
password={password}
|
||||
name={name}
|
||||
/>
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import { SignupSSO } from "@app/views/Signup";
|
||||
|
||||
export default function SignupSSOPage() {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const token = router.query.token as string;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6 pb-28 ">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("signup.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
<meta property="og:title" content={t("signup.og-title") as string} />
|
||||
<meta name="og:description" content={t("signup.og-description") as string} />
|
||||
</Head>
|
||||
<div className="mb-4 mt-20 flex justify-center">
|
||||
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
|
||||
</div>
|
||||
<SignupSSO providerAuthToken={token} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -46,6 +46,7 @@ type Errors = {
|
||||
|
||||
export default function SignupInvite() {
|
||||
const { data: commonPasswords } = useGetCommonPasswords();
|
||||
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
@@ -212,7 +213,7 @@ export default function SignupInvite() {
|
||||
} else {
|
||||
// user will be redirected to dashboard
|
||||
// if not logged in gets kicked out to login
|
||||
router.push("/dashboard");
|
||||
router.push(`/org/${organizationId}/overview`);
|
||||
}
|
||||
} else {
|
||||
console.log("ERROR", response);
|
||||
@@ -338,22 +339,10 @@ export default function SignupInvite() {
|
||||
setBackupKeyError,
|
||||
setBackupKeyIssued
|
||||
});
|
||||
router.push("/noprojects/");
|
||||
router.push(`/org/${organizationId}/overview`);
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
{/* <div
|
||||
className="text-l mt-4 text-lg text-gray-400 hover:text-gray-300 duration-200 bg-white/5 px-8 hover:bg-white/10 py-3 rounded-md cursor-pointer"
|
||||
onClick={() => {
|
||||
if (localStorage.getItem("projectData.id")) {
|
||||
router.push("/dashboard/" + localStorage.getItem("projectData.id"));
|
||||
} else {
|
||||
router.push("/noprojects")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Later
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { FormEvent, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -12,6 +12,7 @@ import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import SendEmailOnPasswordReset from "./api/auth/SendEmailOnPasswordReset";
|
||||
|
||||
export default function VerifyEmail() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [step, setStep] = useState(1);
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
@@ -27,6 +28,18 @@ export default function VerifyEmail() {
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
if (serverDetails?.emailConfigured) {
|
||||
sendVerificationEmail();
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6">
|
||||
<Head>
|
||||
@@ -45,7 +58,7 @@ export default function VerifyEmail() {
|
||||
</div>
|
||||
</Link>
|
||||
{step === 1 && (
|
||||
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl">
|
||||
<form onSubmit={onSubmit} className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
|
||||
Forgot your password?
|
||||
</p>
|
||||
@@ -67,20 +80,10 @@ export default function VerifyEmail() {
|
||||
</div>
|
||||
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
|
||||
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
|
||||
<Button
|
||||
text="Continue"
|
||||
onButtonPressed={() => {
|
||||
if (serverDetails?.emailConfigured) {
|
||||
sendVerificationEmail();
|
||||
} else {
|
||||
handlePopUpOpen("setUpEmail");
|
||||
}
|
||||
}}
|
||||
size="lg"
|
||||
/>
|
||||
<Button type="submit" text="Continue" size="lg" onButtonPressed={() => {}} loading={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl">
|
||||
|
||||
@@ -6,7 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import NavHeader from "@app/components/navigation/NavHeader";
|
||||
import { Button, Input, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useGetProjectFoldersBatch,
|
||||
useGetProjectSecretsByKey,
|
||||
@@ -22,6 +22,7 @@ export const DashboardEnvOverview = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const workspaceId = currentWorkspace?._id as string;
|
||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
@@ -29,7 +30,7 @@ export const DashboardEnvOverview = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !workspaceId && router.isReady) {
|
||||
router.push("/noprojects");
|
||||
router.push(`/org/${currentOrg?._id}/overview`);
|
||||
}
|
||||
}, [isLoading, workspaceId, router.isReady]);
|
||||
|
||||
|
||||
@@ -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 = !secretImportCfg?.imports?.length;
|
||||
const isEmptyPage = isFoldersEmpty && isSecretEmpty && isSecretImportEmpty;
|
||||
|
||||
if (isSecretsLoading || isEnvListLoading) {
|
||||
return (
|
||||
@@ -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>
|
||||
|
||||
@@ -4,8 +4,7 @@ import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-i
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useSyntaxHighlight } from "@app/hooks";
|
||||
|
||||
import { useToggle } from "~/hooks/useToggle";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
|
||||
type Props = {
|
||||
secrets: any[] | undefined;
|
||||
@@ -71,6 +70,8 @@ const DashboardInput = ({
|
||||
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)" }}
|
||||
>
|
||||
|
||||
@@ -67,7 +67,7 @@ export const FolderSection = ({
|
||||
ariaLabel="delete"
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</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";
|
||||
@@ -0,0 +1,157 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import {
|
||||
faFileImport,
|
||||
faFolder,
|
||||
faKey,
|
||||
faUpDown,
|
||||
faXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
|
||||
type Props = {
|
||||
onDelete: (environment: string, secretPath: string) => void;
|
||||
importedEnv: string;
|
||||
importedSecPath: string;
|
||||
importedSecrets: { key: string; value: string; overriden: { env: string; secretPath: string } }[];
|
||||
};
|
||||
|
||||
// to show the environment and folder icon
|
||||
export const EnvFolderIcon = ({ env, secretPath }: { env: string; secretPath: string }) => (
|
||||
<div className="inline-flex items-center space-x-2">
|
||||
<div style={{ minWidth: "96px" }}>{env || "-"}</div>
|
||||
{secretPath && (
|
||||
<div className="inline-flex items-center space-x-2 border-l border-mineshaft-600 pl-2">
|
||||
<FontAwesomeIcon icon={faFolder} size="lg" className="text-primary-700" />
|
||||
<span>{secretPath}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SecretImportItem = ({
|
||||
importedEnv,
|
||||
importedSecPath,
|
||||
onDelete,
|
||||
importedSecrets = []
|
||||
}: Props) => {
|
||||
const [isExpanded, setIsExpanded] = useToggle();
|
||||
const { attributes, listeners, transform, transition, setNodeRef, isDragging } = useSortable({
|
||||
id: `${importedEnv}-${importedSecPath}`
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const rowEnv = currentWorkspace?.environments?.find(({ slug }) => slug === importedEnv);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
setIsExpanded.off();
|
||||
}
|
||||
}, [isDragging]);
|
||||
|
||||
const style = {
|
||||
transform: transform ? `translateY(${transform.y ? Math.round(transform.y) : 0}px)` : "",
|
||||
transition
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||
onClick={() => setIsExpanded.toggle()}
|
||||
>
|
||||
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<FontAwesomeIcon icon={faFileImport} className="text-primary-700" />
|
||||
</td>
|
||||
<td
|
||||
colSpan={2}
|
||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
||||
style={{ paddingTop: "0", paddingBottom: "0" }}
|
||||
>
|
||||
<div className="flex-grow p-2">
|
||||
<EnvFolderIcon env={rowEnv?.name || ""} secretPath={importedSecPath} />
|
||||
</div>
|
||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Change Order" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="expand"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUpDown} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
onClick={(evt) => {
|
||||
evt.stopPropagation();
|
||||
onDelete(importedEnv, importedSecPath);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
{isExpanded && !isDragging && (
|
||||
<td colSpan={3}>
|
||||
<div className="rounded-md bg-bunker-700 p-4 pb-6">
|
||||
<div className="mb-2 text-lg font-medium">Secrets Imported</div>
|
||||
<TableContainer>
|
||||
<table className="secret-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Key</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Value</td>
|
||||
<td style={{ padding: "0.25rem 1rem" }}>Override</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{importedSecrets?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<EmptyState title="No secrets found" icon={faKey} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{importedSecrets.map(({ key, value, overriden }, index) => (
|
||||
<tr key={`${importedEnv}-${importedSecPath}-${key}-${index + 1}`}>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{value}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user