Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-07-19 23:25:52 +07:00
339 changed files with 19009 additions and 9758 deletions

View File

@@ -7,7 +7,8 @@ module.exports = {
root: true,
env: {
browser: true,
es2021: true
es2021: true,
"es6": true
},
extends: [
"airbnb",
@@ -70,6 +71,7 @@ module.exports = {
],
"@typescript-eslint/no-non-null-assertion": "off",
"simple-import-sort/exports": "warn",
"@typescript-eslint/no-empty-function": "off",
"simple-import-sort/imports": [
"warn",
{

View File

@@ -5,6 +5,9 @@
"packages": {
"": {
"dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",
@@ -44,6 +47,7 @@
"classnames": "^2.3.1",
"cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0",
"dayjs": "^1.11.9",
"framer-motion": "^6.2.3",
"fs": "^0.0.2",
"gray-matter": "^4.0.3",
@@ -2467,6 +2471,68 @@
"node": ">=10.0.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.6",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.7",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -10571,6 +10637,11 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"node_modules/dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -24485,6 +24556,50 @@
"integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
"dev": true
},
"@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"requires": {
"tslib": "^2.0.0"
}
},
"@dnd-kit/core": {
"version": "6.0.8",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.8.tgz",
"integrity": "sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==",
"requires": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/modifiers": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz",
"integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==",
"requires": {
"@dnd-kit/utilities": "^3.2.1",
"tslib": "^2.0.0"
}
},
"@dnd-kit/sortable": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz",
"integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
}
},
"@dnd-kit/utilities": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.1.tgz",
"integrity": "sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==",
"requires": {
"tslib": "^2.0.0"
}
},
"@emotion/babel-plugin": {
"version": "11.11.0",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
@@ -30438,6 +30553,11 @@
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true
},
"dayjs": {
"version": "1.11.9",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
"integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View File

@@ -13,6 +13,9 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/css": "^11.10.0",
"@emotion/server": "^11.10.0",
"@fontsource/inter": "^4.5.15",
@@ -52,6 +55,7 @@
"classnames": "^2.3.1",
"cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0",
"dayjs": "^1.11.9",
"framer-motion": "^6.2.3",
"fs": "^0.0.2",
"gray-matter": "^4.0.3",

View File

@@ -12,6 +12,7 @@ const integrationSlugNameMapping: Mapping = {
'github': 'GitHub',
'gitlab': 'GitLab',
'render': 'Render',
'laravel-forge': "Laravel Forge",
'railway': 'Railway',
'flyio': 'Fly.io',
'circleci': 'CircleCI',
@@ -19,7 +20,8 @@ const integrationSlugNameMapping: Mapping = {
'supabase': 'Supabase',
'checkly': 'Checkly',
'hashicorp-vault': 'Vault',
'cloudflare-pages': 'Cloudflare Pages'
'cloudflare-pages': 'Cloudflare Pages',
'codefresh': 'Codefresh'
}
const envMapping: Mapping = {

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -251,6 +251,10 @@
}
},
"settings": {
"webhooks": {
"title": "Webhooks",
"description": "Manage webhooks to setup deployment hooks for your various integrations."
},
"members": {
"title": "Project Members",
"description": "This page shows the members of the selected project, and allows you to modify their permissions."

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,385 +0,0 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */
import crypto from "crypto";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import {
faBookOpen,
faFileLines,
faGear,
faKey,
faMobile,
faPlug,
faPlus,
faUser
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
import getOrganizationUsers from "@app/pages/api/organization/GetOrgUsers";
import getUser from "@app/pages/api/user/getUser";
import addUserToWorkspace from "@app/pages/api/workspace/addUserToWorkspace";
import createWorkspace from "@app/pages/api/workspace/createWorkspace";
import getWorkspaces from "@app/pages/api/workspace/getWorkspaces";
import uploadKeys from "@app/pages/api/workspace/uploadKeys";
import NavBarDashboard from "../navigation/NavBarDashboard";
import onboardingCheck from "../utilities/checks/OnboardingCheck";
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
import { decryptAssymmetric, encryptAssymmetric } from "../utilities/cryptography/crypto";
import Button from "./buttons/Button";
import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog";
import Listbox from "./Listbox";
interface LayoutProps {
children: React.ReactNode;
}
const Layout = ({ children }: LayoutProps) => {
const router = useRouter();
const [workspaceMapping, setWorkspaceMapping] = useState<Map<string, string>[]>([]);
const [workspaceSelected, setWorkspaceSelected] = useState("∞");
const [newWorkspaceName, setNewWorkspaceName] = useState("");
const [isOpen, setIsOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const [totalOnboardingActionsDone, setTotalOnboardingActionsDone] = useState(0);
const { t } = useTranslation();
const closeModal = () => {
setIsOpen(false);
};
const openModal = () => {
setIsOpen(true);
};
// TODO: what to do about the fact that 2ids can have the same name
/**
* When a user creates a new workspace, redirect them to the page of the new workspace.
* @param {*} workspaceName
*/
const submitModal = async (workspaceName: string, addAllUsers: boolean) => {
setLoading(true);
// timeout code.
setTimeout(() => setLoading(false), 1500);
try {
const workspaces = await getWorkspaces();
const currentWorkspaces = workspaces.map((workspace) => workspace.name);
if (!currentWorkspaces.includes(workspaceName)) {
const newWorkspace = await createWorkspace({
workspaceName,
organizationId: tempLocalStorage("orgData.id")
});
const newWorkspaceId = newWorkspace._id;
const randomBytes = crypto.randomBytes(16).toString("hex");
const PRIVATE_KEY = String(localStorage.getItem("PRIVATE_KEY"));
const myUser = await getUser();
const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes,
publicKey: myUser.publicKey,
privateKey: PRIVATE_KEY
});
await uploadKeys(newWorkspaceId, myUser._id, ciphertext, nonce);
if (addAllUsers) {
console.log("adding other users");
const orgUsers = await getOrganizationUsers({
orgId: tempLocalStorage("orgData.id")
});
orgUsers.map(async (user: any) => {
if (user.status === "accepted" && user.email !== myUser.email) {
const result = await addUserToWorkspace(user.user.email, newWorkspaceId);
if (result?.invitee && result?.latestKey) {
const TEMP_PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY");
// assymmetrically decrypt symmetric key with local private key
const key = decryptAssymmetric({
ciphertext: result.latestKey.encryptedKey,
nonce: result.latestKey.nonce,
publicKey: result.latestKey.sender.publicKey,
privateKey: TEMP_PRIVATE_KEY
});
const { ciphertext: inviteeCipherText, nonce: inviteeNonce } = encryptAssymmetric({
plaintext: key,
publicKey: result.invitee.publicKey,
privateKey: PRIVATE_KEY
});
uploadKeys(newWorkspaceId, result.invitee._id, inviteeCipherText, inviteeNonce);
}
}
});
}
setWorkspaceMapping((prevState) => ({
...prevState,
[workspaceName]: newWorkspaceId
}));
setWorkspaceSelected(workspaceName);
setIsOpen(false);
setNewWorkspaceName("");
} else {
console.error("A project with this name already exists.");
setError(true);
setLoading(false);
}
} catch (err) {
console.error(err);
setError(true);
setLoading(false);
}
};
const menuItems = useMemo(
() => [
{
href: `/dashboard/${workspaceMapping[workspaceSelected as any]}`,
title: t("nav.menu.secrets"),
emoji: <FontAwesomeIcon icon={faKey} />
},
{
href: `/users/${workspaceMapping[workspaceSelected as any]}`,
title: t("nav.menu.members"),
emoji: <FontAwesomeIcon icon={faUser} />
},
{
href: `/integrations/${workspaceMapping[workspaceSelected as any]}`,
title: t("nav.menu.integrations"),
emoji: <FontAwesomeIcon icon={faPlug} />
},
{
href: `/activity/${workspaceMapping[workspaceSelected as any]}`,
title: "Audit Logs",
emoji: <FontAwesomeIcon icon={faFileLines} />
},
{
href: `/settings/project/${workspaceMapping[workspaceSelected as any]}`,
title: t("nav.menu.project-settings"),
emoji: <FontAwesomeIcon icon={faGear} />
}
],
[t, workspaceMapping, workspaceSelected]
);
useEffect(() => {
// Put a user in a workspace if they're not in one yet
const putUserInWorkSpace = async () => {
if (tempLocalStorage("orgData.id") === "") {
const userOrgs = await getOrganizations();
localStorage.setItem("orgData.id", userOrgs[0]._id);
}
const orgUserProjects = await getOrganizationUserProjects({
orgId: tempLocalStorage("orgData.id")
});
const userWorkspaces = orgUserProjects;
if (
(userWorkspaces.length === 0 &&
router.asPath !== "/noprojects" &&
!router.asPath.includes("home") &&
!router.asPath.includes("settings")) ||
router.asPath === "/dashboard/undefined"
) {
router.push("/noprojects");
} else if (router.asPath !== "/noprojects") {
const intendedWorkspaceId = router.asPath
.split("/")
[router.asPath.split("/").length - 1].split("?")[0];
if (!["callback", "create", "authorize"].includes(intendedWorkspaceId)) {
localStorage.setItem("projectData.id", intendedWorkspaceId);
}
// If a user is not a member of a workspace they are trying to access, just push them to one of theirs
if (
!["callback", "create", "authorize"].includes(intendedWorkspaceId) &&
!userWorkspaces
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
) {
router.push(`/dashboard/${userWorkspaces[0]._id}`);
} else {
setWorkspaceMapping(
Object.fromEntries(
userWorkspaces.map((workspace: any) => [workspace.name, workspace._id])
) as any
);
setWorkspaceSelected(
Object.fromEntries(
userWorkspaces.map((workspace: any) => [workspace._id, workspace.name])
)[router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]]
);
}
}
};
putUserInWorkSpace();
onboardingCheck({ setTotalOnboardingActionsDone });
}, [router.query.id]);
useEffect(() => {
try {
if (
workspaceMapping[workspaceSelected as any] &&
`${workspaceMapping[workspaceSelected as any]}` !==
router.asPath.split("/")[router.asPath.split("/").length - 1].split("?")[0]
) {
localStorage.setItem("projectData.id", `${workspaceMapping[workspaceSelected as any]}`);
router.push(`/dashboard/${workspaceMapping[workspaceSelected as any]}`);
}
} catch (err) {
console.log(err);
}
}, [workspaceSelected]);
return (
<>
<div className="h-screen w-full flex-col overflow-x-hidden hidden md:flex">
<NavBarDashboard />
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row dark">
<aside className="w-full border-r border-mineshaft-500 bg-bunker-600 md:w-60">
<nav className="items-between flex h-full flex-col justify-between">
{/* <div className="py-6"></div> */}
<div>
<div className="mt-6 mb-6 flex h-20 w-full flex-col items-center justify-center bg-bunker-600 px-4">
<div className="ml-1 mb-1 self-start text-xs font-semibold tracking-wide text-gray-400">
{t("nav.menu.project")}
</div>
{Object.keys(workspaceMapping).length > 0 ? (
<Listbox
isSelected={workspaceSelected}
onChange={setWorkspaceSelected}
data={Object.keys(workspaceMapping)}
buttonAction={openModal}
text=""
/>
) : (
<Button
text="Add Project"
onButtonPressed={openModal}
color="mineshaft"
size="md"
icon={faPlus}
/>
)}
</div>
<ul>
{Object.keys(workspaceMapping).length > 0 &&
menuItems.map(({ href, title, emoji }) => (
<li className="mx-2 mt-0.5" key={title}>
{router.asPath.split("/")[1] === href.split("/")[1] &&
(["project", "billing", "org", "personal"].includes(
router.asPath.split("/")[2]
)
? router.asPath.split("/")[2] === href.split("/")[2]
: true) ? (
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
{emoji}
</p>
{title}
</div>
) : router.asPath === "/noprojects" ? (
<div className="flex rounded p-2.5 text-sm text-white">
<p className="flex w-10 items-center justify-center text-lg">{emoji}</p>
{title}
</div>
) : (
<Link href={href}>
<div className="flex cursor-pointer rounded p-2.5 text-sm text-white hover:bg-primary-50/5">
<p className="flex w-10 items-center justify-center text-lg">
{emoji}
</p>
{title}
</div>
</Link>
)}
</li>
))}
</ul>
</div>
<div className="mt-40 mb-4 w-full px-2">
{router.asPath.split("/")[1] === "home" ? (
<div className="relative flex cursor-pointer rounded bg-primary-50/10 px-0.5 py-2.5 text-sm text-white">
<div className="absolute inset-0 top-0 my-1 ml-1 mr-1 w-1 rounded-xl bg-primary" />
<p className="ml-4 mr-2 flex w-6 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
<img
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
totalOnboardingActionsDone === 1 ? "14" : ""
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
totalOnboardingActionsDone === 3 ? "43" : ""
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
totalOnboardingActionsDone === 5 ? "71" : ""
}.svg`}
height={58}
width={58}
alt="progress bar"
className="absolute right-2 -top-2"
/>
</div>
) : (
<Link href={`/home/${workspaceMapping[workspaceSelected as any]}`}>
<div className="mt-max relative flex h-10 cursor-pointer overflow-visible rounded bg-white/10 p-2.5 text-sm text-white hover:bg-primary-50/[0.15]">
<p className="flex w-10 items-center justify-center text-lg">
<FontAwesomeIcon icon={faBookOpen} />
</p>
Infisical Guide
<img
src={`/images/progress-${totalOnboardingActionsDone === 0 ? "0" : ""}${
totalOnboardingActionsDone === 1 ? "14" : ""
}${totalOnboardingActionsDone === 2 ? "28" : ""}${
totalOnboardingActionsDone === 3 ? "43" : ""
}${totalOnboardingActionsDone === 4 ? "57" : ""}${
totalOnboardingActionsDone === 5 ? "71" : ""
}.svg`}
height={58}
width={58}
alt="progress bar"
className="absolute right-2 -top-2"
/>
</div>
</Link>
)}
</div>
</nav>
</aside>
<AddWorkspaceDialog
isOpen={isOpen}
closeModal={closeModal}
submitModal={submitModal}
workspaceName={newWorkspaceName}
setWorkspaceName={setNewWorkspaceName}
error={error}
loading={loading}
/>
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">{children}</main>
</div>
</div>
<div className="flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 z-[200] md:hidden">
<FontAwesomeIcon icon={faMobile} className="mb-8 text-7xl text-gray-300" />
<p className="max-w-sm px-6 text-center text-lg text-gray-200">
{` ${t("common.no-mobile")} `}
</p>
</div>
</>
);
};
export default Layout;

View File

@@ -1,208 +0,0 @@
import { Fragment, useState } from "react";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Dialog, Transition } from "@headlessui/react";
import addAPIKey from "@app/pages/api/apiKey/addAPIKey";
import Button from "../buttons/Button";
import InputField from "../InputField";
import ListBox from "../Listbox";
const expiryMapping = {
"1 day": 86400,
"7 days": 604800,
"1 month": 2592000,
"6 months": 15552000,
"12 months": 31104000
};
type Props = {
isOpen: boolean;
closeModal: () => void;
// TODO: These never and any will be filled by single folder that contains types and hooks about the API
apiKeys: any[];
setApiKeys: (arg: any[]) => void;
};
// TODO: convert to TS
const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => {
const [apiKey, setApiKey] = useState("");
const [apiKeyName, setApiKeyName] = useState("");
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day");
const [apiKeyCopied, setApiKeyCopied] = useState(false);
const { t } = useTranslation();
const generateAPIKey = async () => {
const newApiKey = await addAPIKey({
name: apiKeyName,
expiresIn: expiryMapping[apiKeyExpiresIn as keyof typeof expiryMapping]
});
setApiKeys([...apiKeys, newApiKey.apiKeyData]);
setApiKey(newApiKey.apiKey);
};
function copyToClipboard() {
// Get the text field
const copyText = document.getElementById("apiKey") as HTMLInputElement;
// Select the text field
copyText.select();
copyText.setSelectionRange(0, 99999); // For mobile devices
// Copy the text inside the text field
navigator.clipboard.writeText(copyText.value);
setApiKeyCopied(true);
setTimeout(() => setApiKeyCopied(false), 2000);
// Alert the copied text
// alert("Copied the text: " + copyText.value);
}
const closeAddApiKeyModal = () => {
closeModal();
setApiKeyName("");
setApiKey("");
};
return (
<div className="z-50">
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
{apiKey === "" ? (
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400"
>
{t("section.api-key.add-dialog.title")}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
{t("section.api-key.add-dialog.description")}
</p>
</div>
</div>
<div className="mb-2 max-h-28">
<InputField
label={t("section.api-key.add-dialog.name")}
onChangeHandler={setApiKeyName}
type="varName"
value={apiKeyName}
placeholder=""
isRequired
/>
</div>
<div className="max-h-28">
<ListBox
isSelected={apiKeyExpiresIn}
onChange={setApiKeyExpiresIn}
data={["1 day", "7 days", "1 month", "6 months", "12 months"]}
isFull
text={`${t("common.expired-in")}: `}
/>
</div>
<div className="max-w-max">
<div className="mt-6 flex w-max flex-col justify-start">
<Button
onButtonPressed={() => generateAPIKey()}
color="mineshaft"
text={t("section.api-key.add-dialog.add") as string}
textDisabled={t("section.api-key.add-dialog.add") as string}
size="md"
active={apiKeyName !== ""}
/>
</div>
</div>
</Dialog.Panel>
) : (
<Dialog.Panel className="w-full max-w-md transform rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400"
>
{t("section.api-key.add-dialog.copy-service-token")}
</Dialog.Title>
<div className="mt-2 mb-4">
<div className="flex flex-col">
<p className="text-sm text-gray-500">
{t("section.api-key.add-dialog.copy-service-token-description")}
</p>
</div>
</div>
<div className="w-full">
<div className="mt-2 mr-2 flex h-20 w-full items-center justify-end rounded-md bg-white/[0.07] text-base text-gray-400">
<input
type="text"
value={apiKey}
disabled
id="apiKey"
className="invisible w-full min-w-full bg-white/0 py-2 px-2 text-gray-400 outline-none"
/>
<div className="w-full max-w-md break-words bg-white/0 py-2 pl-14 pr-2 text-sm text-gray-400 outline-none">
{apiKey}
</div>
<div className="group relative inline-block h-full font-normal text-gray-400 underline duration-200 hover:text-primary">
<button
type="button"
onClick={copyToClipboard}
className="h-full border-l border-white/20 py-2 pl-3.5 pr-4 duration-200 hover:bg-white/[0.12]"
>
{apiKeyCopied ? (
<FontAwesomeIcon icon={faCheck} className="pr-0.5" />
) : (
<FontAwesomeIcon icon={faCopy} />
)}
</button>
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-chicago-900 px-3 py-2 text-center text-sm text-gray-400 duration-300 group-hover:flex group-hover:animate-popup">
{t("common.click-to-copy")}
</span>
</div>
</div>
</div>
<div className="mt-6 flex w-max flex-col justify-start">
<Button
onButtonPressed={() => closeAddApiKeyModal()}
color="mineshaft"
text="Close"
size="md"
/>
</div>
</Dialog.Panel>
)}
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
export default AddApiKeyDialog;

View File

@@ -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"
/>

View File

@@ -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>

View File

@@ -1,85 +0,0 @@
import { faX } from "@fortawesome/free-solid-svg-icons";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
import guidGenerator from "../../utilities/randomId";
import Button from "../buttons/Button";
interface TokenProps {
_id: string;
name: string;
expiresAt: string;
}
interface ServiceTokensProps {
data: TokenProps[];
setApiKeys: (value: TokenProps[]) => void;
}
/**
* This is the component that we utilize for the api key table
* @param {object} obj
* @param {any[]} obj.data - current state of the api key table
* @param {function} obj.setApiKeys - updating the state of the api key table
* @returns
*/
const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => {
const { createNotification } = useNotificationContext();
return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
<div className="absolute rounded-t-md w-full h-12 bg-white/5" />
<table className="w-full my-1">
<thead className="text-bunker-300 text-sm font-light">
<tr>
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th>
<th className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
<th aria-label="button" />
</tr>
</thead>
<tbody>
{data?.length > 0 ? (
data?.map((row) => (
<tr
key={guidGenerator()}
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100"
>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{row.name}
</td>
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300">
{new Date(row.expiresAt).toUTCString()}
</td>
<td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button
onButtonPressed={() => {
deleteAPIKey({ apiKeyId: row._id });
setApiKeys(data.filter((token) => token._id !== row._id));
createNotification({
text: `'${row.name}' API key has been revoked.`,
type: "error"
});
}}
color="red"
size="icon-sm"
icon={faX}
/>
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm">
No API keys yet
</td>
</tr>
)}
</tbody>
</table>
</div>
);
};
export default ApiKeyTable;

View File

@@ -1,12 +1,11 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
import { plans } from "public/data/frequentConstants";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Select, SelectItem } from "@app/components/v2";
import { useSubscription } from "@app/context";
import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission";
import getOrganizationSubscriptions from "@app/pages/api/organization/GetOrgSubscription";
import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace";
import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace";
import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey";
@@ -40,13 +39,12 @@ type EnvironmentProps = {
* @returns
*/
const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoading }: Props) => {
const { subscription } = useSubscription();
const [roleSelected, setRoleSelected] = useState(
Array(userData?.length).fill(userData.map((user) => user.role))
);
const host = window.location.origin;
const router = useRouter();
const [myRole, setMyRole] = useState("member");
const [currentPlan, setCurrentPlan] = useState("");
const [workspaceEnvs, setWorkspaceEnvs] = useState<EnvironmentProps[]>([]);
const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false);
const { createNotification } = useNotificationContext();
@@ -128,7 +126,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
denials = [];
}
if (currentPlan !== plans.professional && host === "https://app.infisical.com" && workspaceId !== "63ea8121b6e2b0543ba79616") {
if (subscription?.rbac === false) {
setIsUpgradeModalOpen(true);
} else {
const allDenials = userData[index].deniedPermissions
@@ -167,14 +165,6 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
(async () => {
const result = await getProjectInfo({ projectId: workspaceId });
setWorkspaceEnvs(result.environments);
const orgId = localStorage.getItem("orgData.id") as string;
const subscriptions = await getOrganizationSubscriptions({
orgId
});
if (subscriptions) {
setCurrentPlan(subscriptions.data[0].plan.product);
}
})();
}, [userData, myUser]);
@@ -208,11 +198,13 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa
return (
<div className="table-container relative mb-6 mt-1 min-w-max rounded-md border border-mineshaft-600 bg-bunker">
<div className="absolute h-[3.1rem] w-full rounded-t-md bg-white/5" />
<UpgradePlanModal
isOpen={isUpgradeModalOpen}
onClose={closeUpgradeModal}
text="You can change user permissions if you switch to Infisical's Professional plan."
/>
{subscription && (
<UpgradePlanModal
isOpen={isUpgradeModalOpen}
onClose={closeUpgradeModal}
text={subscription.slug === null ? "You can use RBAC under an Enterprise license" : "You can use RBAC if you switch to Infisical's Team Plan."}
/>
)}
<table className="my-0.5 w-full">
<thead className="text-xs font-light text-gray-400 bg-mineshaft-800">
<tr>

View File

@@ -1,86 +0,0 @@
import React from "react";
import StripeRedirect from "@app/pages/api/organization/StripeRedirect";
import { tempLocalStorage } from "../utilities/checks/tempLocalStorage";
interface Props {
plan: {
name: string;
price: string;
priceExplanation?: string;
text: string;
subtext?: string;
buttonTextMain: string;
buttonTextSecondary: string;
current: boolean;
};
}
export default function Plan({ plan }: Props) {
return (
<div
className={`relative flex flex-col justify-between border min-w-fit w-96 rounded-lg h-68 mr-4 bg-mineshaft-800 ${
plan.name !== "Starter" && plan.current === true ? "border-2 border-primary" : "border-mineshaft-600"
}
`}
>
<div className="flex flex-col">
<div className="flex flex-row justify-between items-center relative z-10">
<p className="px-6 py-4 text-3xl font-semibold text-gray-400">{plan.name}</p>
</div>
<div className="flex flwx-row items-end justify-start mb-4">
<p className="pl-6 text-3xl font-semibold text-primary">{plan.price}</p>
<p className="pl-3 mb-1 text-lg text-gray-400">{plan.priceExplanation}</p>
</div>
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.text}</p>
<p className="relative z-10 max-w-fit px-6 text-base text-gray-400">{plan.subtext}</p>
</div>
<div className="flex flex-row items-center">
{plan.current === false ? (
<>
{plan.buttonTextMain === "Schedule a Demo" ? (
<a href="/scheduledemo" target='_blank rel="noopener"'>
<div className="relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 hover:text-black hover:border-primary text-gray-400 font-semibold hover:bg-primary bg-bunker duration-200 cursor-pointer rounded-md flex w-max">
{plan.buttonTextMain}
</div>
</a>
) : (
<div
className={`relative z-10 mx-5 mt-3 mb-4 py-2 px-4 border border-1 border-mineshaft-600 text-gray-400 font-semibold ${
plan.buttonTextMain === "Downgrade"
? "hover:bg-red hover:text-white hover:border-red"
: "hover:bg-primary hover:text-black hover:border-primary"
} bg-bunker duration-200 cursor-pointer rounded-md flex w-max`}
>
<button
type="button"
onClick={() =>
StripeRedirect({
orgId: tempLocalStorage("orgData.id")
})
}
>
{plan.buttonTextMain}
</button>
</div>
)}
<a href="https://infisical.com/pricing" target='_blank rel="noopener"'>
<div className="relative z-10 text-gray-400 font-semibold hover:text-primary duration-200 cursor-pointer mb-0.5">
{plan.buttonTextSecondary}
</div>
</a>
</>
) : (
<div
className={`h-8 w-full rounded-b-md flex justify-center items-center z-10 ${
plan.name !== "Starter" && plan.current === true ? "bg-primary" : "bg-mineshaft-400"
}`}
>
<p className="text-xs text-black font-semibold">CURRENT PLAN</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -1,10 +1,11 @@
import { useState } from "react";
import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import axios from "axios"
import attemptLogin from "@app/components/utilities/attemptLogin";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import Error from "../basic/Error";
import attemptCliLogin from "../utilities/attemptCliLogin";
@@ -31,7 +32,8 @@ export default function InitialLoginStep({
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const handleLogin = async () => {
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
try {
if (!email || !password) {
return;
@@ -84,9 +86,11 @@ export default function InitialLoginStep({
setIsLoading(false);
return;
}
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
router.push(`/org/${userOrg}/overview`);
}
}
@@ -98,7 +102,7 @@ export default function InitialLoginStep({
setIsLoading(false);
}
return <div className='flex flex-col mx-auto w-full justify-center items-center'>
return <form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button
@@ -143,7 +147,7 @@ export default function InitialLoginStep({
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button
onClick={async () => handleLogin()}
type="submit"
size="sm"
isFullWidth
className='h-12'
@@ -180,5 +184,5 @@ export default function InitialLoginStep({
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
</Link>
</div>
</div>
</form>
}

View File

@@ -1,133 +0,0 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Error from "@app/components/basic/Error";
import attemptLogin from "@app/components/utilities/attemptLogin";
import { Button, Input } from "../v2";
/**
* 1st step of login - user enters their username and password
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {Function} obj.setEmail - function to set the email of user
* @param {String} obj.password - password of user
* @param {String} obj.setPassword - function to set the password of user
* @param {Function} obj.setStep - function to set the login flow step
* @returns
*/
export default function LoginStep({
email,
setEmail,
password,
setPassword,
setStep
}: {
email: string;
setEmail: (email: string) => void;
password: string;
setPassword: (password: string) => void;
setStep: (step: number) => void;
}) {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false);
const { t } = useTranslation();
const handleLogin = async () => {
try {
if (!email || !password) {
return;
}
setIsLoading(true);
const isLoginSuccessful = await attemptLogin({
email,
password,
});
if (isLoginSuccessful && isLoginSuccessful.success) {
// case: login was successful
if (isLoginSuccessful.mfaEnabled) {
// case: login requires MFA step
setStep(2);
setIsLoading(false);
return;
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
}
} catch (err) {
setLoginError(true);
}
setIsLoading(false);
}
return (
<form onSubmit={(e) => e.preventDefault()}>
<div className="w-full mx-auto h-full px-6">
<p className="text-xl w-max mx-auto flex justify-center text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 mb-6">
{t("login.login")}
</p>
<div className="flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-12"
/>
</div>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="h-12"
/>
</div>
</div>
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[22rem] px-2 mt-4 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full">
<Button
onClick={async () => handleLogin()}
size="sm"
isFullWidth
className='h-14'
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
> {String(t("login.login"))} </Button>
</div>
</div>
</div>
<div className="text-bunker-400 text-sm flex flex-row w-max mx-auto">
<Link href="/verify-email">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>{t("login.forgot-password")}</span>
</Link>
</div>
{false && (
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
{t("common.maintenance-alert")}
</div>
)}
</form>
);
}

View File

@@ -8,6 +8,7 @@ import axios from "axios"
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa"
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
import { useSendMfaToken } from "@app/hooks/api/auth";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import Error from "../basic/Error";
import { Button } from "../v2";
@@ -101,7 +102,7 @@ export default function MFAStep({
// cli page
router.push("/cli-redirect");
}
}else{
} else {
const isLoginSuccessful = await attemptLoginMfa({
email,
password,
@@ -111,7 +112,11 @@ export default function MFAStep({
if (isLoginSuccessful) {
setIsLoading(false);
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0] && userOrgs[0]._id;
// case: login does not require MFA step
router.push(`/org/${userOrg}/overview`);
}
}
@@ -180,6 +185,7 @@ export default function MFAStep({
className='h-14'
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
> {String(t("mfa.verify"))} </Button>
</div>
</div>
@@ -187,7 +193,7 @@ export default function MFAStep({
<div className="flex flex-row items-baseline gap-1 text-sm">
<span className="text-bunker-400">{t("signup.step2-resend-alert")}</span>
<div className="mt-2 text-bunker-400 text-md flex flex-row">
<button disabled={isLoading} onClick={handleResendMfaCode} type="button">
<button disabled={isLoadingResend} onClick={handleResendMfaCode} type="button">
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>
{isLoadingResend
? t("signup.step2-resend-progress")

View File

@@ -5,6 +5,7 @@ import { useRouter } from "next/router";
import Error from "@app/components/basic/Error";
import attemptLogin from "@app/components/utilities/attemptLogin";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import SecurityClient from "../utilities/SecurityClient";
import { Button, Input } from "../v2";
@@ -50,7 +51,9 @@ export default function PasswordInputStep({
}
// case: login does not require MFA step
router.push(`/dashboard/${localStorage.getItem("projectData.id")}`);
const userOrgs = await getOrganizations();
const userOrg = userOrgs[0]._id;
router.push(`/org/${userOrg?._id}/overview`);
}
} catch (err) {
setLoginError(true);

View File

@@ -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:support@infisical.com"
]
];
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>
);
}

View File

@@ -57,11 +57,17 @@ export default function NavHeader({
);
return (
<div className="ml-4 flex flex-row items-center pt-6">
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
<div className="flex flex-row items-center pt-6">
<div className="mr-2 flex h-5 w-5 items-center justify-center rounded-md bg-primary text-black text-sm">
{currentOrg?.name?.charAt(0)}
</div>
<div className="text-sm font-semibold text-bunker-300">{currentOrg?.name}</div>
<Link
passHref
legacyBehavior
href={`/org/${currentOrg?._id}/overview`}
>
<a className="text-sm font-semibold text-primary/80 hover:text-primary pl-0.5">{currentOrg?.name}</a>
</Link>
{isProjectRelated && (
<>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-xs text-gray-400" />
@@ -79,7 +85,7 @@ export default function NavHeader({
<Link
passHref
legacyBehavior
href={{ pathname: "/dashboard/[id]", query: { id: router.query.id } }}
href={{ pathname: "/project/[id]/secrets", query: { id: router.query.id } }}
>
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
</Link>
@@ -120,7 +126,7 @@ export default function NavHeader({
{index + 1 === folders?.length ? (
<span className="text-sm font-semibold text-bunker-300">{name}</span>
) : (
<Link passHref legacyBehavior href={{ pathname: "/dashboard/[id]", query }}>
<Link passHref legacyBehavior href={{ pathname: "/project/[id]/secrets", query }}>
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
{name === "root" ? selectedEnv?.name : name}
</a>

View File

@@ -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

View File

@@ -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

View File

@@ -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 }) => {

View File

@@ -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

View File

@@ -0,0 +1,108 @@
/* eslint-disable new-cap */
import crypto from "crypto";
import jsrp from "jsrp";
import changePassword2 from "@app/pages/api/auth/ChangePassword2";
import SRP1 from "@app/pages/api/auth/SRP1";
import Aes256Gcm from "./cryptography/aes-256-gcm";
import { deriveArgonKey } from "./cryptography/crypto";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
type Params = {
email: string;
currentPassword: string;
newPassword: string;
}
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
return new Promise((resolve, reject) => {
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
let serverPublicKey; let salt;
try {
const clientPublicKey = clientOldPassword.getPublicKey();
const res = await SRP1({ clientPublicKey });
serverPublicKey = res.serverPublicKey;
salt = res.salt;
clientOldPassword.setSalt(salt);
clientOldPassword.setServerPublicKey(serverPublicKey);
const clientProof = clientOldPassword.getProof();
clientNewPassword.init({ username: email, password: newPassword }, async () => {
clientNewPassword.createVerifier(async (err, result) => {
try {
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: localStorage.getItem("PRIVATE_KEY") as string,
secret: key
});
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
await changePassword2({
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
});
saveTokenToLocalStorage({
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
});
resolve();
} catch (err2) {
console.error(err2);
reject(err2);
}
});
});
} catch (err) {
console.error(err);
reject(err);
}
});
});
}
export default attemptChangePassword;

View File

@@ -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 || ""

View File

@@ -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 });

View File

@@ -61,7 +61,8 @@ const buttonVariants = cva(
{
colorSchema: "primary",
variant: "star",
className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100"
className:
"bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100"
},
{
colorSchema: "primary",
@@ -76,12 +77,14 @@ const buttonVariants = cva(
{
colorSchema: "primary",
variant: "outline_bg",
className: "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200"
className:
"bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200"
},
{
colorSchema: "secondary",
variant: "star",
className: "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white"
className:
"bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white"
},
{
colorSchema: "danger",
@@ -163,13 +166,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
type="button"
className={twMerge(
buttonVariants({
className,
colorSchema,
size,
variant,
isRounded,
isDisabled,
isFullWidth
isFullWidth,
className
})
)}
disabled={isDisabled}
@@ -193,7 +196,15 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
>
{leftIcon}
</div>
<span className={twMerge("transition-all", isFullWidth ? "w-full" : "w-min", loadingToggleClass)}>{children}</span>
<span
className={twMerge(
"transition-all",
isFullWidth ? "w-full" : "w-min",
loadingToggleClass
)}
>
{children}
</span>
<div
className={twMerge(
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",

View File

@@ -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>

View File

@@ -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>

View File

@@ -13,7 +13,7 @@ type Props = {
};
const inputVariants = cva(
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-[0.05rem] hover:ring-bunker-400/60 duration-100",
"input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100",
{
variants: {
size: {

View File

@@ -48,7 +48,7 @@ export const MenuItem = <T extends ElementType = "button">({
>
<li
className={twMerge(
"group px-1 py-2.5 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
"group px-1 py-2 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
isSelected && "bg-mineshaft-600 hover:bg-mineshaft-600",
isDisabled && "hover:bg-transparent cursor-not-allowed",
className
@@ -56,16 +56,16 @@ export const MenuItem = <T extends ElementType = "button">({
>
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm">
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
<div className={`${isSelected ? "visisble" : "invisible"} absolute w-[0.25rem] rounded-md h-8 bg-primary`}/>
<div className={`${isSelected ? "visisble" : "invisible"} -left-[0.28rem] absolute w-[0.07rem] rounded-md h-5 bg-primary`}/>
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
<Lottie
lottieRef={iconRef}
style={{ width: 24, height: 24 }}
style={{ width: 22, height: 22 }}
// eslint-disable-next-line import/no-dynamic-require
animationData={require(`../../../../public/lotties/${icon}.json`)}
loop={false}
autoplay={false}
className="my-auto ml-3 mr-3"
className="my-auto ml-[0.1rem] mr-3"
/>
<span className="flex-grow text-left">{children}</span>
</Item>
@@ -76,6 +76,53 @@ export const MenuItem = <T extends ElementType = "button">({
)
};
export const SubMenuItem = <T extends ElementType = "button">({
children,
icon,
className,
isDisabled,
isSelected,
as: Item = "button",
description,
// wrapping in forward ref with generic component causes the loss of ts definitions on props
inputRef,
...props
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
const iconRef = useRef()
return(
<a
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
>
<li
className={twMerge(
"group px-1 py-1 mt-0.5 font-inter flex flex-col text-sm text-mineshaft-300 hover:text-mineshaft-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
isDisabled && "hover:bg-transparent cursor-not-allowed",
className
)}
>
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm pl-6">
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}>
<Lottie
lottieRef={iconRef}
style={{ width: 16, height: 16 }}
// eslint-disable-next-line import/no-dynamic-require
animationData={require(`../../../../public/lotties/${icon}.json`)}
loop={false}
autoplay={false}
className="my-auto ml-[0.1rem] mr-3"
/>
<span className="flex-grow text-left text-sm">{children}</span>
</Item>
{description && <span className="mt-2 text-xs">{description}</span>}
</motion.span>
</li>
</a>
)
};
MenuItem.displayName = "MenuItem";
export type MenuGroupProps = {

View File

@@ -72,12 +72,12 @@ export const TabsObject = () => {
>
Windows
</Tabs.Trigger>
<Tabs.Trigger
{/* <Tabs.Trigger
className="bg-bunker-700 px-5 h-10 flex-1 flex items-center justify-center text-sm leading-none text-bunker-300 select-none first:rounded-tl-md last:rounded-tr-md data-[state=active]:text-primary data-[state=active]:font-medium data-[state=active]:focus:relative data-[state=active]:border-b data-[state=active]:border-primary outline-none cursor-default"
value="tab3"
>
Arch Linux
</Tabs.Trigger>
</Tabs.Trigger> */}
<a
target='_blank'
rel="noopener noreferrer"
@@ -88,7 +88,7 @@ export const TabsObject = () => {
</a>
</Tabs.List>
<Tabs.Content
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none cursor-default"
value="tab1"
>
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCode" />
@@ -105,10 +105,10 @@ export const TabsObject = () => {
</a>. </p>
</Tabs.Content>
<Tabs.Content
className="grow p-5 bg-bunker-700 rounded-b-md outline-none"
className="grow p-5 pt-0 bg-bunker-700 rounded-b-md outline-none"
value="tab2"
>
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeW" />
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="scoop bucket add org https://github.com/Infisical/scoop-infisical.git" id="downloadCodeW" />
<div className='font-mono text-sm px-3 py-2 mt-2 bg-bunker rounded-md border border-mineshaft-600 flex flex-row items-center justify-between'>
<input disabled value="scoop install infisical" id="downloadCodeW2" className='w-full bg-transparent text-bunker-200'/>
<button
@@ -138,22 +138,5 @@ export const TabsObject = () => {
here
</a>. </p>
</Tabs.Content>
<Tabs.Content
className="grow p-5 bg-bunker-700 rounded-b-md outline-none cursor-default"
value="tab3"
>
<CodeItem isCopied={downloadCodeCopied} setIsCopied={setDownloadCodeCopied} textExplanation="1. Download CLI" code="brew install infisical/get-cli/infisical" id="downloadCodeL" />
<CodeItem isCopied={loginCodeCopied} setIsCopied={setLoginCodeCopied} textExplanation="2. Login" code="infisical login" id="loginCodeL" />
<CodeItem isCopied={initCodeCopied} setIsCopied={setInitCodeCopied} textExplanation="3. Choose Project" code="infisical init" id="initCodeL" />
<CodeItem isCopied={runCodeCopied} setIsCopied={setRunCodeCopied} textExplanation="4. Done! Now, you can prepend your usual start script with:" code="infisical run -- [YOUR USUAL CODE START SCRIPT GOES HERE]" id="runCodeL" />
<p className='text-bunker-300 text-sm mt-2'>You can find example of start commands for different frameworks <a
className='text-primary underline underline-offset-2'
target="_blank"
rel="noopener noreferrer"
href='https://infisical.com/docs/integrations/overview'
>
here
</a>. </p>
</Tabs.Content>
</Tabs.Root>
};

View File

@@ -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,

View File

@@ -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>
)

View File

@@ -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>(

View File

@@ -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

View File

@@ -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">

View File

@@ -1,7 +1,6 @@
export {
useGetAuthToken,
useGetCommonPasswords,
useRevokeAllSessions,
useSendMfaToken,
useSendMfaToken,
useVerifyMfaToken
} from "./queries";
} from "./queries"

View File

@@ -51,15 +51,6 @@ export const useGetAuthToken = () =>
retry: 0
});
export const useRevokeAllSessions = () => {
return useMutation({
mutationFn: async () => {
const { data } = await apiRequest.delete("/api/v1/auth/sessions");
return data;
}
});
}
const fetchCommonPasswords = async () => {
const { data } = await apiRequest.get("/api/v1/auth/common-passwords");
return data || [];

View File

@@ -6,6 +6,7 @@ export * from "./integrations";
export * from "./keys";
export * from "./organization";
export * from "./secretFolders";
export * from "./secretImports";
export * from "./secrets";
export * from "./secretSnapshots";
export * from "./serviceAccounts";
@@ -13,4 +14,5 @@ export * from "./serviceTokens";
export * from "./subscriptions";
export * from "./tags";
export * from "./users";
export * from "./webhooks";
export * from "./workspace";

View File

@@ -12,5 +12,7 @@ export {
useGetOrgPlanTable,
useGetOrgPmtMethods,
useGetOrgTaxIds,
useGetOrgTrialUrl,
useRenameOrg,
useUpdateOrgBillingDetails} from "./queries";
useUpdateOrgBillingDetails
} from "./queries";

View File

@@ -10,8 +10,9 @@ import {
PlanBillingInfo,
PmtMethod,
ProductsTable,
RenameOrgDTO,
TaxID} from "./types";
RenameOrgDTO,
TaxID
} from "./types";
const organizationKeys = {
getUserOrganization: ["organization"] as const,
@@ -47,6 +48,24 @@ export const useRenameOrg = () => {
});
};
export const useGetOrgTrialUrl = () => {
return useMutation({
mutationFn: async ({
orgId,
success_url
}: {
orgId: string;
success_url: string;
}) => {
const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${orgId}/session/trial`, {
success_url
})
return url;
}
});
};
export const useGetOrgPlanBillingInfo = (organizationId: string) => {
return useQuery({
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),

View File

@@ -0,0 +1,2 @@
export { useCreateSecretImport, useDeleteSecretImport, useUpdateSecretImport } from "./mutation";
export { useGetImportedSecrets, useGetSecretImports } from "./queries";

View File

@@ -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)
);
}
});
};

View File

@@ -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]
)
});

View File

@@ -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;
};

View File

@@ -24,6 +24,10 @@ export const secretKeys = {
{ workspaceId, env, folderId },
"secrets"
],
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
};
@@ -81,76 +85,79 @@ export const useGetProjectSecrets = ({
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const sharedSecrets: DecryptedSecret[] = [];
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
select: useCallback(
(data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const sharedSecrets: DecryptedSecret[] = [];
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
sharedSecrets.push(decryptedSecret);
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
sharedSecrets.push(decryptedSecret);
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
});
sharedSecrets.forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
}
});
return { secrets: sharedSecrets };
}, [decryptFileKey])
});
sharedSecrets.forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
}
});
return { secrets: sharedSecrets };
},
[decryptFileKey]
)
});
export const useGetProjectSecretsByKey = ({
@@ -167,82 +174,85 @@ export const useGetProjectSecretsByKey = ({
// right now secretpath is passed as folderid as only this is used in overview
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
const uniqSecKeys: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
select: useCallback(
(data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
const uniqSecKeys: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
sharedSecrets[secretKey].push(decryptedSecret);
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
});
Object.keys(sharedSecrets).forEach((secName) => {
sharedSecrets[secName].forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
sharedSecrets[secretKey].push(decryptedSecret);
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
});
});
Object.keys(sharedSecrets).forEach((secName) => {
sharedSecrets[secName].forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
}
});
});
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
}, [decryptFileKey])
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
},
[decryptFileKey]
)
});
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
@@ -263,29 +273,32 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
enabled: Boolean(dto.secretId && dto.decryptFileKey),
queryKey: secretKeys.getSecretVersion(dto.secretId),
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
select: useCallback((data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
select: useCallback(
(data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data
.map((el) => ({
createdAt: el.createdAt,
id: el._id,
value: decryptSymmetric({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key
})
}))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}, [])
return data
.map((el) => ({
createdAt: el.createdAt,
id: el._id,
value: decryptSymmetric({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key
})
}))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
},
[dto.decryptFileKey]
)
});
export const useBatchSecretsOp = () => {

View File

@@ -34,6 +34,7 @@ export type DecryptedSecret = {
valueOverride?: string;
idOverride?: string;
overrideAction?: string;
folderId?: string;
};
export type EncryptedSecretVersion = {
@@ -98,6 +99,7 @@ export type GetProjectSecretsDTO = {
folderId?: string;
secretPath?: string;
isPaused?: boolean;
include_imports?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void;
};

View File

@@ -1,9 +1,13 @@
export type ServiceTokenScope = {
environment: string;
secretPath: string;
};
export type ServiceToken = {
_id: string;
name: string;
workspace: string;
environment: string;
secretPath: string;
scopes: ServiceTokenScope[];
user: string;
expiresAt: string;
createdAt: string;
@@ -14,9 +18,8 @@ export type ServiceToken = {
export type CreateServiceTokenDTO = {
name: string;
workspaceId: string;
environment: string;
scopes: ServiceTokenScope[];
expiresIn: number;
secretPath: string;
encryptedKey: string;
iv: string;
tag: string;

View File

@@ -13,4 +13,7 @@ export type SubscriptionPlan = {
workspaceLimit: number;
workspacesUsed: number;
environmentLimit: number;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null;
has_used_trial: boolean;
};

View File

@@ -8,6 +8,7 @@ export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"
export type { SubscriptionPlan } from "./subscriptions/types";
export type { WsTag } from "./tags/types";
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from "./users/types";
export type { TWebhook } from "./webhooks/types";
export type {
CreateEnvironmentDTO,
CreateWorkspaceDTO,

View File

@@ -2,11 +2,15 @@ export {
fetchOrgUsers,
useAddUserToOrg,
useAddUserToWs,
useCreateAPIKey,
useDeleteAPIKey,
useDeleteOrgMembership,
useGetMyAPIKeys,
useGetMySessions,
useGetOrgUsers,
useGetUser,
useGetUserAction,
useLogoutUser,
useRegisterUserAction,
useUpdateOrgUserRole
} from "./queries";
useRevokeMySessions,
useUpdateOrgUserRole} from "./queries";

View File

@@ -12,16 +12,21 @@ import {
AddUserToOrgDTO,
AddUserToWsDTO,
AddUserToWsRes,
APIKeyData,
CreateAPIKeyRes,
DeletOrgMembershipDTO,
OrgUser,
RenameUserDTO,
TokenVersion,
UpdateOrgUserRoleDTO,
User
} from "./types";
User} from "./types";
const userKeys = {
getUser: ["user"] as const,
userAction: ["user-action"] as const,
getOrgUsers: (orgId: string) => [{ orgId }, "user"]
getOrgUsers: (orgId: string) => [{ orgId }, "user"],
myAPIKeys: ["api-keys"] as const,
mySessions: ["sessions"] as const
};
export const fetchUserDetails = async () => {
@@ -41,6 +46,18 @@ const fetchUserAction = async (action: string) => {
return data.userAction;
};
export const useRenameUser = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, RenameUserDTO>({
mutationFn: ({ newName }) =>
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
};
export const useGetUserAction = (action: string) =>
useQuery({
queryKey: userKeys.userAction,
@@ -167,3 +184,90 @@ export const useLogoutUser = () =>
localStorage.setItem("PRIVATE_KEY", "");
}
});
export const useGetMyAPIKeys = () => {
return useQuery({
queryKey: userKeys.myAPIKeys,
queryFn: async () => {
const { data } = await apiRequest.get<APIKeyData[]>(
"/api/v2/users/me/api-keys"
);
return data;
},
enabled: true
});
}
export const useCreateAPIKey = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
name,
expiresIn
}: {
name: string;
expiresIn: number;
}) => {
const { data } = await apiRequest.post<CreateAPIKeyRes>(
"/api/v2/users/me/api-keys",
{
name,
expiresIn
}
);
return data;
},
onSuccess() {
queryClient.invalidateQueries(userKeys.myAPIKeys);
}
});
}
export const useDeleteAPIKey = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (apiKeyDataId: string) => {
const { data } = await apiRequest.delete(
`/api/v2/users/me/api-keys/${apiKeyDataId}`
);
return data;
},
onSuccess() {
queryClient.invalidateQueries(userKeys.myAPIKeys);
}
});
}
export const useGetMySessions = () => {
return useQuery({
queryKey: userKeys.mySessions,
queryFn: async () => {
const { data } = await apiRequest.get<TokenVersion[]>(
"/api/v2/users/me/sessions"
);
return data;
},
enabled: true
});
}
export const useRevokeMySessions = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
console.log("useRevokeAllSessions 1");
const { data } = await apiRequest.delete(
"/api/v2/users/me/sessions"
);
console.log("useRevokeAllSessions 2: ", data);
return data;
},
onSuccess() {
queryClient.invalidateQueries(userKeys.mySessions);
}
});
}

View File

@@ -61,3 +61,31 @@ export type AddUserToOrgDTO = {
inviteeEmail: string;
organizationId: string;
};
export type CreateAPIKeyRes = {
apiKey: string;
apiKeyData: APIKeyData;
}
export type RenameUserDTO = {
newName: string;
};
export type APIKeyData = {
_id: string;
name: string;
user: string;
lastUsed: string;
createdAt: string;
expiresAt: string;
}
export type TokenVersion = {
_id: string;
user: string;
userAgent: string;
ip: string;
lastUsed: string;
createdAt: string;
updatedAt: string;
}

View File

@@ -0,0 +1,2 @@
export { useCreateWebhook, useDeleteWebhook, useTestWebhook, useUpdateWebhook } from "./mutation";
export { useGetWebhooks } from "./query";

View File

@@ -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));
}
});
};

View File

@@ -0,0 +1,26 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TWebhook } from "./types";
export const queryKeys = {
getWebhooks: (workspaceId: string) => ["webhooks", { workspaceId }]
};
const fetchWebhooks = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ webhooks: TWebhook[] }>("/api/v1/webhooks", {
params: {
workspaceId
}
});
return data.webhooks;
};
export const useGetWebhooks = (workspaceId: string) =>
useQuery({
queryKey: queryKeys.getWebhooks(workspaceId),
queryFn: () => fetchWebhooks(workspaceId),
enabled: Boolean(workspaceId)
});

View File

@@ -0,0 +1,36 @@
export type TWebhook = {
_id: string;
workspace: string;
environment: string;
secretPath: string;
url: string;
lastStatus: "success" | "failed";
lastRunErrorMessage?: string;
isDisabled: boolean;
createdAt: string;
updatedAt: string;
};
export type TCreateWebhookDto = {
workspaceId: string;
environment: string;
webhookUrl: string;
webhookSecretKey?: string;
secretPath: string;
};
export type TUpdateWebhookDto = {
webhookId: string;
workspaceId: string;
isDisabled?: boolean;
};
export type TDeleteWebhookDto = {
webhookId: string;
workspaceId: string;
};
export type TTestWebhookDTO = {
webhookId: string;
workspaceId: string;
};

View File

@@ -1,4 +1,5 @@
export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useToggle } from "./useToggle";

View File

@@ -0,0 +1,49 @@
import { useCallback } from "react";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const REGEX = /([$]{.*?})/g;
export const useSyntaxHighlight = () => {
const syntaxHighlight = useCallback((text: string, isHidden?: boolean) => {
if (isHidden) {
if (!text) {
return "EMPTY";
}
return text
.split("")
.slice(0, 200)
.map((el, i) =>
el === "\n" ? (
el
) : (
<FontAwesomeIcon
key={`${text}_${el}_${i + 1}`}
className="mr-0.5 text-xxs"
icon={faCircle}
/>
)
);
}
// append a space on last new line this to show new line in ui for code component
const val = text.at(-1) === "\n" ? text.concat(" ") : text;
if (val?.length === 0) return <span className="font-mono text-bunker-400/80">EMPTY</span>;
return val?.split(REGEX).map((word, i) =>
word.match(REGEX) !== null ? (
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
$&#123;
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
&#125;
</span>
) : (
<span key={`${word}_${i + 1}`} className="ph-no-capture">
{word}
</span>
)
);
}, []);
return syntaxHighlight;
};

View File

@@ -7,15 +7,16 @@
// @ts-nocheck
import crypto from "crypto";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { faBookOpen, faMobile, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faAngleDown, faArrowLeft, faArrowUpRightFromSquare, faBook, faCheck, faEnvelope, faInfinity, faMobile, faPlus, faQuestion } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import queryString from "query-string";
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
@@ -25,6 +26,9 @@ import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypt
import {
Button,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
FormControl,
Input,
Menu,
@@ -37,16 +41,35 @@ import {
} from "@app/components/v2";
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
import { usePopUp } from "@app/hooks";
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useUploadWsKey } from "@app/hooks/api";
import getOrganizations from "@app/pages/api/organization/getOrgs";
import getOrganizationUserProjects from "@app/pages/api/organization/GetOrgUserProjects";
import { Navbar } from "./components/NavBar";
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useGetOrgTrialUrl, useLogoutUser, useUploadWsKey } from "@app/hooks/api";
interface LayoutProps {
children: React.ReactNode;
}
const supportOptions = [
[
<FontAwesomeIcon key={1} className="pr-4 text-sm" icon={faSlack} />,
"Support Forum",
"https://infisical.com/slack"
],
[
<FontAwesomeIcon key={2} className="pr-4 text-sm" icon={faBook} />,
"Read Docs",
"https://infisical.com/docs/documentation/getting-started/introduction"
],
[
<FontAwesomeIcon key={3} className="pr-4 text-sm" icon={faGithub} />,
"GitHub Issues",
"https://github.com/Infisical/infisical/issues"
],
[
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support",
"mailto:support@infisical.com"
]
];
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>

View File

@@ -21,7 +21,9 @@ import {TFunction} from "i18next";
import guidGenerator from "@app/components/utilities/randomId";
import { useOrganization, useSubscription,useUser } from "@app/context";
import { useLogoutUser } from "@app/hooks/api";
import {
useGetOrgTrialUrl,
useLogoutUser} from "@app/hooks/api";
const supportOptions = (t: TFunction) => [
[
@@ -64,8 +66,9 @@ export interface IUser {
export const Navbar = () => {
const router = useRouter();
const { subscription } = useSubscription();
const { currentOrg, orgs } = useOrganization();
const { mutateAsync } = useGetOrgTrialUrl();
const { user } = useUser();
const logout = useLogoutUser();
@@ -96,16 +99,15 @@ export const Navbar = () => {
};
return (
<div className="z-[70] flex w-full flex-row justify-between border-b border-mineshaft-500 bg-mineshaft-900 text-white">
<div className="m-auto mx-4 flex items-center justify-start">
<div className="flex flex-row items-center">
<div className="flex justify-center py-4">
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
</div>
<a href="#" className="mx-2 text-2xl font-semibold text-white">
Infisical
</a>
<div className="z-[70] border-b border-mineshaft-500 bg-mineshaft-900 text-white">
<div className="flex w-full justify-between px-4">
<div className="flex flex-row items-center">
<div className="flex justify-center py-4">
<Image src="/images/logotransparent.png" height={23} width={57} alt="logo" />
</div>
<a href="#" className="mx-2 text-2xl font-semibold text-white">
Infisical
</a>
</div>
<div className="relative z-40 mx-2 flex items-center justify-start">
<a
@@ -178,7 +180,7 @@ export const Navbar = () => {
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={() => router.push(`/settings/personal/${router.query.id}`)}
onClick={() => router.push("/personal-settings")}
className="mx-1 my-1 flex cursor-pointer flex-row items-center rounded-md px-1 hover:bg-white/5"
>
<div className="flex h-8 w-9 items-center justify-center rounded-full bg-white/10 text-gray-300">
@@ -190,7 +192,7 @@ export const Navbar = () => {
{" "}
{user?.firstName} {user?.lastName}
</p>
<p className="px-2 pb-1 text-xs text-gray-400"> {user?.email}</p>
<p className="px-2 pb-1 text-xs text-gray-400">{user?.email}</p>
</div>
<FontAwesomeIcon
icon={faGear}
@@ -314,6 +316,28 @@ export const Navbar = () => {
</Transition>
</Menu>
</div>
</div>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<div className="w-full mx-auto border-t border-mineshaft-500 text-center">
<button
type="button"
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg._id,
success_url: window.location.href
});
window.location.href = url;
}}
className="text-center py-4 text-sm mx-auto"
>
You are currently on the <span className="font-semibold">Starter</span> plan. Unlock the full power of Infisical on the <span className="font-semibold">Pro Free Trial &rarr;</span>
</button>
</div>
)}
</div>
);
};
};

View File

@@ -1 +0,0 @@
export { Navbar } from "./NavBar";

View File

@@ -21,11 +21,10 @@ export default function Custom404() {
</a>{" "}
and we`ll fix it!{" "}
</p>
<Link
href='/dashboard'
className='bg-mineshaft-500 mt-8 py-2 px-4 rounded-md hover:bg-primary diration-200 hover:text-black cursor-pointer font-semibold'
>
Go to Dashboard
<Link href='/dashboard'>
<div className="mt-8 bg-mineshaft-500 py-2 px-4 rounded-md hover:bg-primary diration-200 hover:text-black font-semibold cursor-default">
Go to Dashboard
</div>
</Link>
<Image
src='/images/dragon-404.svg'

View File

@@ -77,8 +77,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<AuthProvider>
<WorkspaceProvider>
<OrgProvider>
<OrgProvider>
<WorkspaceProvider>
<SubscriptionProvider>
<UserProvider>
<NotificationProvider>
@@ -90,8 +90,8 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
</NotificationProvider>
</UserProvider>
</SubscriptionProvider>
</OrgProvider>
</WorkspaceProvider>
</WorkspaceProvider>
</OrgProvider>
</AuthProvider>
</TooltipProvider>
</QueryClientProvider>

View File

@@ -1,33 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
name: string;
expiresIn: number;
}
/**
* This route adds an API key for the user
* @param {object} obj
* @param {string} obj.name - name of the API key
* @param {string} obj.expiresIn - how soon the API key expires in ms
* @returns
*/
const addAPIKey = ({ name, expiresIn }: Props) =>
SecurityClient.fetchCall("/api/v2/api-key/", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
name,
expiresIn
})
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to add API key");
return undefined;
});
export default addAPIKey;

View File

@@ -1,27 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
interface Props {
apiKeyId: string;
}
/**
* This route revokes the API key with id [apiKeyId]
* @param {object} obj
* @param {string} obj.apiKeyId - id of the API key to delete
* @returns
*/
const deleteAPIKey = ({ apiKeyId }: Props) =>
SecurityClient.fetchCall(`/api/v2/api-key/${apiKeyId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return res.json();
}
console.log("Failed to delete API key");
return undefined;
});
export default deleteAPIKey;

View File

@@ -1,22 +0,0 @@
import SecurityClient from "@app/components/utilities/SecurityClient";
/**
* This route gets API keys for the user
* @param {*} param0
* @returns
*/
const getAPIKeys = () =>
SecurityClient.fetchCall("/api/v2/api-key", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
}).then(async (res) => {
if (res && res.status === 200) {
return (await res.json()).apiKeyData;
}
console.log("Failed to get API keys");
return undefined;
});
export default getAPIKeys;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,4 +1,5 @@
import Head from "next/head";
import Image from "next/image";
export default function CliRedirect() {
return (
@@ -8,9 +9,12 @@ export default function CliRedirect() {
<link rel='icon' href='/infisical.ico' />
</Head>
<div className='flex flex-col items-center justify-center text-gray-200 h-screen w-screen'>
<p className='text-4xl mt-32'>Head back to your terminal!</p>
<p className='mt-2 mb-1 text-lg'>
You&apos;ve successfully logged into infisical-cli
<div className="mb-8 flex justify-center">
<Image src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div>
<p className='text-3xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center'>Head back to your terminal</p>
<p className='mb-1 text-lg text-light text-mineshaft-400'>
You&apos;ve successfully logged in to the Infisical CLI
</p>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import getWorkspaces from "./api/workspace/getWorkspaces";
import getOrganizations from "./api/organization/getOrgs";
export default function DashboardRedirect() {
const router = useRouter();
@@ -12,14 +12,14 @@ export default function DashboardRedirect() {
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
(async () => {
let userWorkspace;
let userOrg;
try {
if (localStorage.getItem("projectData.id")) {
router.push(`/dashboard/${ localStorage.getItem("projectData.id")}`);
if (localStorage.getItem("orgData.id")) {
router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
} else {
const userWorkspaces = await getWorkspaces();
userWorkspace = userWorkspaces[0]._id;
router.push(`/dashboard/${ userWorkspace}`);
const userOrgs = await getOrganizations();
userOrg = userOrgs[0]._id;
router.push(`/org/${userOrg}/overview`);
}
} catch (error) {
console.log("Error - Not logged in yet");

View File

@@ -1,244 +0,0 @@
import { useEffect, useState } from "react";
import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { faSlack } from "@fortawesome/free-brands-svg-icons";
import {
faCheckCircle,
faHandPeace,
faNetworkWired,
faPlug,
faPlus,
faStar,
faUserPlus
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { TabsObject } from "@app/components/v2/Tabs";
import registerUserAction from "../api/userActions/registerUserAction";
type ItemProps = {
text: string;
subText: string;
complete: boolean;
icon: IconProp;
time: string;
userAction?: string;
link?: string;
};
const learningItem = ({
text,
subText,
complete,
icon,
time,
userAction,
link
}: ItemProps): JSX.Element => {
if (link) {
return (
<a
target={`${link.includes("https") ? "_blank" : "_self"}`}
rel="noopener noreferrer"
className={`w-full ${complete && "opacity-30 duration-200 hover:opacity-100"}`}
href={link}
>
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async () => {
if (userAction && userAction !== "first_time_secrets_pushed") {
await registerUserAction({
action: userAction
});
}
}}
className="group relative mb-3 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-bunker-500 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-mineshaft-700"
>
<div className="mr-4 flex flex-row items-center">
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
{complete && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-primary" />
</div>
)}
<div className="flex flex-col items-start">
<div className="mt-0.5 text-xl font-semibold">{text}</div>
<div className="text-sm font-normal">{subText}</div>
</div>
</div>
<div
className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}
>
{complete ? "Complete!" : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
</div>
</a>
);
}
return (
<div
onKeyDown={() => null}
role="button"
tabIndex={0}
onClick={async () => {
if (userAction) {
await registerUserAction({
action: userAction
});
}
}}
className="relative my-1.5 flex h-[5.5rem] w-full cursor-pointer items-center justify-between overflow-hidden rounded-md border border-dashed border-bunker-400 bg-bunker-700 py-2 pl-2 pr-6 shadow-xl duration-200 hover:bg-bunker-500"
>
<div className="mr-4 flex flex-row items-center">
<FontAwesomeIcon icon={icon} className="mx-2 w-16 text-4xl" />
{complete && (
<div className="absolute left-11 top-10 h-7 w-7 rounded-full bg-bunker-700">
<FontAwesomeIcon
icon={faCheckCircle}
className="absolute left-12 top-16 h-5 w-5 text-4xl text-primary"
/>
</div>
)}
<div className="flex flex-col items-start">
<div className="mt-0.5 text-xl font-semibold">{text}</div>
<div className="mt-0.5 text-sm font-normal">{subText}</div>
</div>
</div>
<div className={`w-28 pr-4 text-right text-sm font-semibold ${complete && "text-primary"}`}>
{complete ? "Complete!" : `About ${time}`}
</div>
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
</div>
);
};
/**
* This tab is called Home because in the future it will include some company news,
* updates, roadmap, relavant blogs, etc. Currently it only has the setup instruction
* for the new users
*/
export default function Home() {
const router = useRouter();
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
const [hasUserStarred, setHasUserStarred] = useState(false);
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
const [usersInOrg, setUsersInOrg] = useState(false);
useEffect(() => {
onboardingCheck({
setHasUserClickedIntro,
setHasUserClickedSlack,
setHasUserPushedSecrets,
setHasUserStarred,
setUsersInOrg
});
}, []);
return (
<div className="mx-6 w-full pt-4 lg:mx-0">
<Head>
<title>Infisical Guide</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<div className="relative mx-auto flex max-w-3xl flex-col items-center px-6 py-6 text-lg text-gray-300 lg:max-w-4xl xl:max-w-5xl">
<div className="mt-12 w-full text-left text-5xl font-bold">Your quick start guide</div>
<div className="mb-14 w-full pt-2 pb-4 text-left text-lg text-bunker-300">
Click on the items below and follow the instructions.
</div>
<div className="absolute top-0 right-0 hidden h-min lg:block">
<Image
src="/images/dragon-book.svg"
height={250}
width={400}
alt="start guise dragon illustration"
/>
</div>
{learningItem({
text: "Get to know Infisical",
subText: "",
complete: hasUserClickedIntro,
icon: faHandPeace,
time: "3 min",
userAction: "intro_cta_clicked",
link: "https://www.youtube.com/watch?v=PK23097-25I"
})}
{learningItem({
text: "Add your secrets",
subText: "Click to see example secrets, and add your own.",
complete: hasUserPushedSecrets,
icon: faPlus,
time: "2 min",
userAction: "first_time_secrets_pushed",
link: `/dashboard/${router.query.id}`
})}
<div className="group relative mb-3 flex h-full w-full cursor-default flex-col items-center justify-between overflow-hidden rounded-md border border-mineshaft-600 bg-bunker-500 pl-2 pr-2 pt-4 pb-2 shadow-xl duration-200">
<div className="mb-4 flex w-full flex-row items-center pr-4">
<div className="mr-4 flex w-full flex-row items-center">
<FontAwesomeIcon icon={faNetworkWired} className="mx-2 w-16 text-4xl" />
{false && (
<div className="absolute left-12 top-10 flex h-7 w-7 items-center justify-center rounded-full bg-bunker-500 p-2 group-hover:bg-mineshaft-700">
<FontAwesomeIcon icon={faCheckCircle} className="h-5 w-5 text-4xl text-green" />
</div>
)}
<div className="flex flex-col items-start pl-0.5">
<div className="mt-0.5 text-xl font-semibold">Inject secrets locally</div>
<div className="text-sm font-normal">
Replace .env files with a more secure and efficient alternative.
</div>
</div>
</div>
<div className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}>
About 2 min
</div>
</div>
<TabsObject />
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
</div>
{learningItem({
text: "Integrate Infisical with your infrastructure",
subText: "Connect Infisical to various 3rd party services and platforms.",
complete: false,
icon: faPlug,
time: "15 min",
link: "https://infisical.com/docs/integrations/overview"
})}
{learningItem({
text: "Invite your teammates",
subText: "",
complete: usersInOrg,
icon: faUserPlus,
time: "2 min",
link: `/settings/org/${router.query.id}?invite`
})}
{learningItem({
text: "Join Infisical Slack",
subText: "Have any questions? Ask us!",
complete: hasUserClickedSlack,
icon: faSlack,
time: "1 min",
userAction: "slack_cta_clicked",
link: "https://join.slack.com/t/infisical-users/shared_invite/zt-1wehzfnzn-1aMo5JcGENJiNAC2SD8Jlg"
})}
{learningItem({
text: "Star Infisical on GitHub",
subText: "Like what we're doing? You know what to do! :)",
complete: hasUserStarred,
icon: faStar,
time: "1 min",
userAction: "star_cta_clicked",
link: "https://github.com/Infisical/infisical"
})}
</div>
</div>
);
}
Home.requireAuth = true;

View File

@@ -0,0 +1,80 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
export default function LaravelForgeCreateIntegrationPage() {
const router = useRouter();
const [apiKey, setApiKey] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [serverId, setServerId] = useState("");
const [serverIdErrorText, setServerIdErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
setServerIdErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("Access Token cannot be blank");
return;
}
if (serverId.length === 0) {
setServerIdErrorText("Server Id cannot be blank");
return;
}
setIsLoading(true);
const integrationAuth = await saveIntegrationAccessToken({
workspaceId: localStorage.getItem("projectData.id"),
integration: "laravel-forge",
accessId: serverId,
accessToken: apiKey,
url: null,
namespace: null
});
setIsLoading(false);
router.push(`/integrations/laravel-forge/create?integrationAuthId=${integrationAuth._id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Laravel Forge Integration</CardTitle>
<FormControl
label="Laravel Forge Access Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="Access Token" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<FormControl
label="Laravel Forge Server ID"
errorText={serverIdErrorText}
isError={serverIdErrorText !== "" ?? false}
>
<Input placeholder="123456" value={serverId} onChange={(e) => setServerId(e.target.value)} />
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to Laravel Forge
</Button>
</Card>
</div>
);
}
LaravelForgeCreateIntegrationPage.requireAuth = true;

View File

@@ -0,0 +1,155 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
import createIntegration from "../../api/integrations/createIntegration";
export default function LaravelForgeCreateIntegrationPage() {
const router = useRouter();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [targetApp, setTargetApp] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?._id) return;
setIsLoading(true);
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
app: targetApp,
appId:
integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
targetEnvironmentId: null,
targetService: null,
targetServiceId: null,
owner: null,
path: null,
region: null,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">Laravel Forge Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="Laravel Forge Sites" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-app-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No sites found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
LaravelForgeCreateIntegrationPage.requireAuth = true;

View File

@@ -10,12 +10,11 @@ import axios from "axios"
import InitialLoginStep from "@app/components/login/InitialLoginStep";
import MFAStep from "@app/components/login/MFAStep";
import PasswordInputStep from "@app/components/login/PasswordInputStep";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { useProviderAuth } from "@app/hooks/useProviderAuth";
import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import { fetchUserDetails } from "~/hooks/api/users/queries";
import getWorkspaces from "./api/workspace/getWorkspaces";
import getOrganizations from "./api/organization/getOrgs";
export default function Login() {
const router = useRouter();
@@ -44,10 +43,10 @@ export default function Login() {
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
let userWorkspace;
try {
const userWorkspaces = await getWorkspaces();
userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
const userOrgs = await getOrganizations();
// userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
const userOrg = userOrgs[0] && userOrgs[0]._id;
// user details
const userDetails = await fetchUserDetails()
@@ -62,7 +61,7 @@ export default function Login() {
const instance = axios.create()
await instance.post(cliUrl, { email: userDetails.email, privateKey: localStorage.getItem("PRIVATE_KEY"), JTWToken: getAuthToken() })
}
router.push(`/dashboard/${userWorkspace}`);
router.push(`/org/${userOrg}/overview`);
} catch (error) {
console.log("Error - Not logged in yet");
}

View File

@@ -1,55 +0,0 @@
import { useEffect } from "react";
import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router";
import Button from "@app/components/basic/buttons/Button";
import getOrganizationUserProjects from "./api/organization/GetOrgUserProjects";
export default function NoProjects() {
const router = useRouter();
const redirectUser = async () => {
const workspaces = await getOrganizationUserProjects({
orgId: String(localStorage.getItem("orgData.id"))
});
if (workspaces.length > 0) {
router.push(`/dashboard/${workspaces[0]._id}`);
}
};
useEffect(() => {
// on initial load - run auth check
(async () => {
await redirectUser();
})();
}, []);
return (
<div className="mr-auto flex h-full w-11/12 flex-col items-center justify-center text-center text-lg text-gray-300">
<Head>
<title>No Projects | Infisical</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<div className="mb-6 mt-16 mr-16">
<Image src="/images/dragon-cant-find.svg" height={270} width={436} alt="google logo" />
</div>
<div className="mb-8 rounded-md bg-mineshaft-900 border border-mineshaft-700 px-4 py-6 text-bunker-300 shadow-xl">
<div className="max-w-md">
You are not part of any projects in this organization yet. When you are, they will appear
here.
</div>
<div className="mt-4 max-w-md">
Create a new project, or ask other organization members to give you necessary permissions.
</div>
<div className="mx-2 mt-6">
<Button text="Check again" onButtonPressed={redirectUser} size="md" color="mineshaft" />
</div>
</div>
</div>
);
}
NoProjects.requireAuth = true;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,6 +1,6 @@
import crypto from "crypto";
import { useState } from "react";
import { FormEvent, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/router";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
@@ -24,6 +24,7 @@ const client = new jsrp.client();
export default function PasswordReset() {
const [verificationToken, setVerificationToken] = useState("");
const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false);
const [backupKey, setBackupKey] = useState("");
const [privateKey, setPrivateKey] = useState("");
const [newPassword, setNewPassword] = useState("");
@@ -38,7 +39,8 @@ export default function PasswordReset() {
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
// Unencrypt the private key with a backup key
const getEncryptedKeyHandler = async () => {
const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
@@ -57,7 +59,8 @@ export default function PasswordReset() {
};
// If everything is correct, reset the password
const resetPasswordHandler = async () => {
const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errorCheck = passwordCheck({
password: newPassword,
setPasswordErrorLength,
@@ -125,6 +128,7 @@ export default function PasswordReset() {
if (response?.status === 200) {
router.push("/login");
}
setLoading(false)
});
}
);
@@ -162,7 +166,7 @@ export default function PasswordReset() {
// Input backup key
const stepInputBackupKey = (
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<form onSubmit={getEncryptedKeyHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter your backup key
</p>
@@ -186,18 +190,19 @@ export default function PasswordReset() {
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button
type="submit"
text="Submit Backup Key"
onButtonPressed={() => getEncryptedKeyHandler()}
onButtonPressed={() => {}}
size="lg"
/>
</div>
</div>
</div>
</form>
);
// Enter new password
const stepEnterNewPassword = (
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<form onSubmit={resetPasswordHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter new password
</p>
@@ -269,13 +274,15 @@ export default function PasswordReset() {
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button
type="submit"
text="Submit New Password"
onButtonPressed={() => resetPasswordHandler()}
onButtonPressed={() => setLoading(true)}
size="lg"
loading={loading}
/>
</div>
</div>
</div>
</form>
);
return (

View File

@@ -0,0 +1,20 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { PersonalSettingsPage } from "@app/views/Settings/PersonalSettingsPage";
export default function PersonalSettings() {
const { t } = useTranslation();
return (
<div className="bg-bunker-800 text-white h-full">
<Head>
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<PersonalSettingsPage />
</div>
);
}
PersonalSettings.requireAuth = true;

View File

@@ -5,14 +5,13 @@ import { useRouter } from "next/router";
import Button from "@app/components/basic/buttons/Button";
import EventFilter from "@app/components/basic/EventFilter";
import NavHeader from "@app/components/navigation/NavHeader";
import { UpgradePlanModal } from "@app/components/v2";
import { useSubscription } from "@app/context";
import ActivitySideBar from "@app/ee/components/ActivitySideBar";
import { usePopUp } from "@app/hooks/usePopUp";
import getProjectLogs from "../../ee/api/secrets/GetProjectLogs";
import ActivityTable from "../../ee/components/ActivityTable";
import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs";
import ActivityTable from "../../../../ee/components/ActivityTable";
interface LogData {
_id: string;
@@ -152,18 +151,17 @@ export default function Activity() {
};
return (
<div className="mx-6 lg:mx-0 w-full h-full">
<div className="mx-auto w-full h-full max-w-7xl">
<Head>
<title>Audit Logs</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<NavHeader pageName="Audit Logs" isProjectRelated />
{currentSidebarAction && (
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
)}
<div className="flex flex-col justify-between items-start mx-4 mt-6 mb-4 text-xl max-w-5xl px-2">
<div className="flex flex-row justify-start items-center text-3xl">
<div className="flex flex-col justify-between items-start mx-4 mb-4 text-xl px-2">
<div className="flex flex-row justify-start items-center text-3xl mt-6">
<p className="font-semibold mr-4 text-bunker-100">{t("activity.title")}</p>
</div>
<p className="mr-4 text-base text-gray-400">{t("activity.subtitle")}</p>

View File

@@ -9,20 +9,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "@app/components/basic/buttons/Button";
import AddProjectMemberDialog from "@app/components/basic/dialog/AddProjectMemberDialog";
import ProjectUsersTable from "@app/components/basic/table/ProjectUsersTable";
import NavHeader from "@app/components/navigation/NavHeader";
import guidGenerator from "@app/components/utilities/randomId";
import { Input } from "@app/components/v2";
import {
decryptAssymmetric,
encryptAssymmetric
} from "../../components/utilities/cryptography/crypto";
import getOrganizationUsers from "../api/organization/GetOrgUsers";
import getUser from "../api/user/getUser";
} from "../../../../components/utilities/cryptography/crypto";
import getOrganizationUsers from "../../../api/organization/GetOrgUsers";
import getUser from "../../../api/user/getUser";
// import DeleteUserDialog from '@app/components/basic/dialog/DeleteUserDialog';
import addUserToWorkspace from "../api/workspace/addUserToWorkspace";
import getWorkspaceUsers from "../api/workspace/getWorkspaceUsers";
import uploadKeys from "../api/workspace/uploadKeys";
import addUserToWorkspace from "../../../api/workspace/addUserToWorkspace";
import getWorkspaceUsers from "../../../api/workspace/getWorkspaceUsers";
import uploadKeys from "../../../api/workspace/uploadKeys";
interface UserProps {
firstName: string;
@@ -149,12 +148,11 @@ export default function Users() {
};
return userList ? (
<div className="flex max-w-[calc(100vw-240px)] flex-col justify-start bg-bunker-800 md:h-screen">
<div className="flex max-w-7xl mx-auto flex-col justify-start bg-bunker-800 md:h-screen">
<Head>
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<NavHeader pageName={t("settings.members.title")} isProjectRelated />
<div className="flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl mb-4">
<p className="mr-4 font-semibold text-white">{t("settings.members.title")}</p>
</div>
@@ -172,7 +170,7 @@ export default function Users() {
setEmail={setEmail}
/>
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
<div className="absolute right-4 top-36 flex w-full flex-row items-start px-6 pb-1">
<div className="flex w-full flex-row items-start px-6 pb-1">
<div className="flex w-full max-w-sm flex flex-row ml-auto">
<Input
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
@@ -192,7 +190,7 @@ export default function Users() {
/>
</div>
</div>
<div className="no-scrollbar::-webkit-scrollbar block overflow-x-scroll px-6 pb-6 no-scrollbar">
<div className="block overflow-x-auto px-6 pb-6">
<ProjectUsersTable
userData={userList}
changeData={setUserList}

View File

@@ -8,7 +8,7 @@ import { useRouter } from "next/router";
import { Button, Input } from "@app/components/v2";
import { isLoggedIn } from "@app/reactQuery";
import getWorkspaces from "./api/workspace/getWorkspaces";
import getOrganizations from "./api/organization/getOrgs";
export default function Login() {
const router = useRouter();
@@ -18,11 +18,11 @@ export default function Login() {
useEffect(() => {
// TODO(akhilmhdh): workspace will be controlled by a workspace context
const redirectToDashboard = async () => {
let userWorkspace;
let userOrg;
try {
const userWorkspaces = await getWorkspaces();
userWorkspace = userWorkspaces[0] && userWorkspaces[0]._id;
router.push(`/dashboard/${userWorkspace}`);
const userOrgs = await getOrganizations();
userOrg = userOrgs[0] && userOrgs[0]._id;
router.push(`/org/${userOrg}/overview`);
} catch (error) {
console.log("Error - Not logged in yet");
}

View File

@@ -0,0 +1,15 @@
import { useEffect } from "react";
import { useRouter } from "next/router"
export default function SecretScanning() {
const router = useRouter();
useEffect(()=>{
router.push(`${router.asPath.split("secret-scanning")[0]}/org/${localStorage.getItem("orgData.id")}/secret-scanning${router.asPath.split("secret-scanning")[1]}`)
}, [])
return <div/>;
}
SecretScanning.requireAuth = true;

View File

@@ -1,324 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { useRouter } from "next/router";
import { faBan,faCheck, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Button from "@app/components/basic/buttons/Button";
import InputField from "@app/components/basic/InputField";
import ListBox from "@app/components/basic/Listbox";
import ApiKeyTable from "@app/components/basic/table/ApiKeyTable";
import NavHeader from "@app/components/navigation/NavHeader";
import checkPassword from "@app/components/utilities/checks/checkPassword";
import changePassword from "@app/components/utilities/cryptography/changePassword";
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import {
useGetCommonPasswords,
useRevokeAllSessions} from "@app/hooks/api";
import { SecuritySection } from "@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection";
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
import getAPIKeys from "../../api/apiKey/getAPIKeys";
import getUser from "../../api/user/getUser";
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function PersonalSettings() {
const { data: commonPasswords } = useGetCommonPasswords();
const [personalEmail, setPersonalEmail] = useState("");
const [personalName, setPersonalName] = useState("");
const [currentPasswordError, setCurrentPasswordError] = useState(false);
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [backupPassword, setBackupPassword] = useState("");
const [passwordChanged, setPasswordChanged] = useState(false);
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
const [backupKeyError, setBackupKeyError] = useState(false);
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
const [apiKeys, setApiKeys] = useState<any[]>([]);
const [errors, setErrors] = useState<Errors>({});
const revokeAllSessions = useRevokeAllSessions();
const { t, i18n } = useTranslation();
const router = useRouter();
const lang = router.locale ?? "en";
const setLanguage = async (to: string) => {
router.push(router.asPath, router.asPath, { locale: to });
localStorage.setItem("lang", to);
i18n.changeLanguage(to);
};
useEffect(() => {
const load = async () => {
try {
const user = await getUser();
setApiKeys(await getAPIKeys());
setPersonalEmail(user.email);
setPersonalName(`${user.firstName} ${user.lastName}`);
} catch (err) {
console.error(err);
}
};
load();
}, []);
const closeAddApiKeyModal = () => {
setIsAddApiKeyDialogOpen(false);
};
return (
<div className="flex max-h-screen flex-col justify-between bg-bunker-800 text-white">
<Head>
<title>{t("common.head-title", { title: t("settings.personal.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<AddApiKeyDialog
isOpen={isAddApiKeyDialogOpen}
closeModal={closeAddApiKeyModal}
apiKeys={apiKeys}
setApiKeys={setApiKeys}
/>
<div className="flex flex-row">
<div className="max-h-screen w-full pb-2">
<NavHeader pageName={t("settings.personal.title")} isProjectRelated={false} />
<div className="ml-6 mt-8 mb-6 flex max-w-5xl flex-row items-center justify-between text-xl">
<div className="flex flex-col items-start justify-start text-3xl">
<p className="mr-4 font-semibold text-gray-200">{t("settings.personal.title")}</p>
<p className="mr-4 text-base font-normal text-gray-400">
{t("settings.personal.description")}
</p>
</div>
</div>
<div className="ml-6 mr-6 flex max-w-5xl flex-col text-mineshaft-50">
<div className="mb-6 mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-6 pb-6">
<p className="self-start text-xl font-semibold">
{t("settings.personal.change-language")}
</p>
<div className="w-ful mt-4 max-h-28">
<ListBox
isSelected={lang}
onChange={setLanguage}
data={["en", "ko", "fr", "es"]}
text={`${t("common.language")}: `}
/>
</div>
</div>
<SecuritySection />
<div className="mt-2 mb-8 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-4">
<div className="flex w-full flex-row justify-between">
<div className="flex w-full flex-col">
<p className="mb-3 text-xl font-semibold">
{t("settings.personal.api-keys.title")}
</p>
<p className="text-sm text-gray-400">
{t("settings.personal.api-keys.description")}
</p>
</div>
<div className="mt-2 w-40">
<Button
text={String(t("settings.personal.api-keys.add-new"))}
onButtonPressed={() => {
setIsAddApiKeyDialogOpen(true);
}}
color="mineshaft"
icon={faPlus}
size="md"
/>
</div>
</div>
<ApiKeyTable data={apiKeys} setApiKeys={setApiKeys as any} />
</div>
<div className="mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
<div className="flex w-full max-w-3xl flex-col justify-between">
<p className="mb-3 min-w-max text-xl font-semibold">
{t("section.password.change")}
</p>
</div>
</div>
<div className="w-full max-w-xl">
<InputField
label={t("section.password.current") as string}
onChangeHandler={(password) => {
setCurrentPassword(password);
}}
type="password"
value={currentPassword}
isRequired
error={currentPasswordError}
errorText={t("section.password.current-wrong") as string}
autoComplete="current-password"
id="current-password"
/>
<div className="py-2" />
<InputField
label={t("section.password.new") as string}
onChangeHandler={(password) => {
setNewPassword(password);
checkPassword({
password,
commonPasswords,
setErrors
});
}}
type="password"
value={newPassword}
isRequired
error={Object.keys(errors).length > 0}
autoComplete="new-password"
id="new-password"
/>
</div>
{Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-2 text-sm text-gray-400">{t("section.password.validate-base")}</div>
{Object.keys(errors).map((key) => {
if (errors[key as keyof Errors]) {
return (
<div className="ml-1 flex flex-row items-top justify-start" key={key}>
<div>
<FontAwesomeIcon
icon={faXmark}
className="text-md text-red ml-0.5 mr-2.5"
/>
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div>
)}
<div className="mt-3 flex w-52 flex-row items-center pr-3">
<Button
text={t("section.password.change") as string}
onButtonPressed={() => {
const errorCheck = checkPassword({
password: newPassword,
commonPasswords,
setErrors
});
if (!errorCheck) {
changePassword(
personalEmail,
currentPassword,
newPassword,
setCurrentPasswordError,
setPasswordChanged,
setCurrentPassword,
setNewPassword
);
}
}}
active={Object.keys(errors).length === 0 && currentPassword !== ""}
color="mineshaft"
size="md"
textDisabled={t("section.password.change") as string}
/>
<FontAwesomeIcon
icon={faCheck}
className={`ml-4 text-3xl text-primary ${
passwordChanged ? "opacity-100" : "opacity-0"
} duration-300`}
/>
</div>
</div>
<div className="mb-6 mt-2 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pb-6 pt-2">
<div className="my-4 flex w-full flex-row justify-between">
<p className="text-xl font-semibold w-full">
Sessions
</p>
<div className="w-40">
<Button
text="Revoke all"
onButtonPressed={async () => {
await revokeAllSessions.mutateAsync();
router.push("/login");
}}
color="mineshaft"
icon={faBan}
size="md"
/>
</div>
</div>
<p className="mb-5 text-sm text-mineshaft-300">
Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your account out all active sessions across all browsers and CLIs.
</p>
</div>
<div className="mt-2 mb-6 flex w-full flex-col items-start rounded-md bg-white/5 px-6 pt-5 pb-6">
<div className="flex w-full max-w-5xl flex-row items-center justify-between">
<div className="flex w-full max-w-3xl flex-col justify-between">
<p className="mb-3 min-w-max text-xl font-semibold">
{t("settings.personal.emergency.name")}
</p>
<p className="min-w-max text-sm text-mineshaft-300">
{t("settings.personal.emergency.text1")}
</p>
<p className="mb-5 min-w-max text-sm text-mineshaft-300">
{t("settings.personal.emergency.text2")}
</p>
</div>
</div>
<div className="mb-4 w-full max-w-xl">
<InputField
label={t("section.password.current") as string}
onChangeHandler={setBackupPassword}
type="password"
value={backupPassword}
isRequired
error={backupKeyError}
errorText={t("section.password.current-wrong") as string}
autoComplete="current-password"
id="current-password"
/>
</div>
<div className="mt-3 flex w-60 flex-row items-center">
<Button
text={t("settings.personal.emergency.download") as string}
onButtonPressed={() => {
issueBackupKey({
email: personalEmail,
password: backupPassword,
personalName,
setBackupKeyError,
setBackupKeyIssued
});
}}
color="mineshaft"
size="md"
active={backupPassword !== ""}
textDisabled={t("settings.personal.emergency.download") as string}
/>
<FontAwesomeIcon
icon={faCheck}
className={`ml-4 text-3xl text-primary ${
backupKeyIssued ? "opacity-100" : "opacity-0"
} duration-300`}
/>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
PersonalSettings.requireAuth = true;

Some files were not shown because too many files have changed in this diff Show More