feat: resolved a lot of ts issues, planning to migrate to v5 tanstack query

This commit is contained in:
=
2025-01-06 14:55:09 +05:30
parent 701eb7cfc6
commit cb22ee315e
67 changed files with 798 additions and 1197 deletions
+27 -20
View File
@@ -42,9 +42,10 @@
"@radix-ui/react-toast": "^1.2.3", "@radix-ui/react-toast": "^1.2.3",
"@radix-ui/react-tooltip": "^1.1.5", "@radix-ui/react-tooltip": "^1.1.5",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^5.62.7",
"@tanstack/react-router": "^1.87.9", "@tanstack/react-router": "^1.87.9",
"@tanstack/virtual-file-routes": "^1.87.6", "@tanstack/virtual-file-routes": "^1.87.6",
"@tanstack/zod-adapter": "^1.91.0",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
@@ -3628,9 +3629,9 @@
} }
}, },
"node_modules/@tanstack/query-core": { "node_modules/@tanstack/query-core": {
"version": "4.36.1", "version": "5.62.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.7.tgz",
"integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==", "integrity": "sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
@@ -3638,30 +3639,19 @@
} }
}, },
"node_modules/@tanstack/react-query": { "node_modules/@tanstack/react-query": {
"version": "4.36.1", "version": "5.62.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.7.tgz",
"integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==", "integrity": "sha512-+xCtP4UAFDTlRTYyEjLx0sRtWyr5GIk7TZjZwBu4YaNahi3Rt2oMyRqfpfVrtwsqY2sayP4iXVCwmC+ZqqFmuw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tanstack/query-core": "4.36.1", "@tanstack/query-core": "5.62.7"
"use-sync-external-store": "^1.2.0"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^18 || ^19"
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-native": "*"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
} }
}, },
"node_modules/@tanstack/react-router": { "node_modules/@tanstack/react-router": {
@@ -3855,6 +3845,23 @@
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
} }
}, },
"node_modules/@tanstack/zod-adapter": {
"version": "1.91.0",
"resolved": "https://registry.npmjs.org/@tanstack/zod-adapter/-/zod-adapter-1.91.0.tgz",
"integrity": "sha512-OccU1RwZ7svIyztcWrVreurWUwaTv98wAWTjzAOS6fe8Ajl9oLT9+mvj4vdi5gn5ycKyu3xkrIPhLIp5fGLZcQ==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-router": ">=1.43.2",
"zod": "^3.23.8"
}
},
"node_modules/@types/argon2-browser": { "node_modules/@types/argon2-browser": {
"version": "1.18.4", "version": "1.18.4",
"resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.4.tgz", "resolved": "https://registry.npmjs.org/@types/argon2-browser/-/argon2-browser-1.18.4.tgz",
+2 -1
View File
@@ -46,9 +46,10 @@
"@radix-ui/react-toast": "^1.2.3", "@radix-ui/react-toast": "^1.2.3",
"@radix-ui/react-tooltip": "^1.1.5", "@radix-ui/react-tooltip": "^1.1.5",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^5.62.7",
"@tanstack/react-router": "^1.87.9", "@tanstack/react-router": "^1.87.9",
"@tanstack/virtual-file-routes": "^1.87.6", "@tanstack/virtual-file-routes": "^1.87.6",
"@tanstack/zod-adapter": "^1.91.0",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"argon2-browser": "^1.18.0", "argon2-browser": "^1.18.0",
@@ -90,7 +90,12 @@ export default function NavHeader({
)} )}
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" /> <FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-3 text-sm text-gray-400" />
{pageName === "Secrets" ? ( {pageName === "Secrets" ? (
<Link to={{ pathname: "/project/[id]/secrets/overview", query: { id: navigate.query.id } }}> <Link
to={{
pathname: "/project/$projectId/secrets/overview",
params: { id: currentWorkspace?.id }
}}
>
<a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a> <a className="text-sm font-semibold text-primary/80 hover:text-primary">{pageName}</a>
</Link> </Link>
) : ( ) : (
@@ -1,13 +1,13 @@
import { FC } from "react"; import { FC } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import z from "zod"; import z from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2"; import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api"; import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
import { useNavigate } from "@tanstack/react-router";
const schema = z const schema = z
.object({ .object({
@@ -58,7 +58,7 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
}); });
navigate({ navigate({
to: `/organization/$organizationId/${ProjectType.SecretManager}` as const, to: `/organization/$organizationId/${ProjectType.SecretManager}/overview` as const,
params: { params: {
organizationId: organization.id organizationId: organization.id
} }
@@ -38,7 +38,7 @@ const attemptChangePassword = ({ email, currentPassword, newPassword }: Params):
const clientProof = clientOldPassword.getProof(); const clientProof = clientOldPassword.getProof();
clientNewPassword.init({ username: email, password: newPassword }, async () => { clientNewPassword.init({ username: email, password: newPassword }, async () => {
clientNewPassword.createVerifier(async (err, result) => { clientNewPassword.createVerifier(async (_err, result) => {
try { try {
const derivedKey = await deriveArgonKey({ const derivedKey = await deriveArgonKey({
password: newPassword, password: newPassword,
@@ -1,63 +0,0 @@
/* eslint-disable prefer-template */
/* eslint-disable prefer-rest-params */
/* eslint-disable @typescript-eslint/no-unused-expressions */
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unexpected-multiline */
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable vars-on-top */
/* eslint-disable no-var */
/* eslint-disable func-names */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { INTERCOMid as APPid } from "@app/components/utilities/config";
// Loads Intercom with the snippet
// This must be run before boot, it initializes window.Intercom
// prettier-ignore
export const load = () => {
(function(){
var w=window;
var ic=w.Intercom;
if(typeof ic==="function"){
ic("reattach_activator");
ic("update",w.intercomSettings);
} else {
var d=document;
var i=function() {
i.c(arguments);
};
i.q=[];
i.c=function(args) {
i.q.push(args);
};
w.Intercom=i;
var l=function() {
var s=d.createElement("script");
s.type="text/javascript";
s.async=true;
s.src="https://widget.intercom.io/widget/" + APPid;
var x=d.getElementsByTagName("script")[0];
x.parentNode.insertBefore(s, x);
};
if (document.readyState==="complete") {
l();
} else if (w.attachEvent) {
w.attachEvent("onload",l);
} else {
w.addEventListener("load",l,false);
}
}
})();
}
// Initializes Intercom
export const boot = (options = {}) => {
window && window.Intercom && window.Intercom("boot", { appid: APPid, ...options });
};
export const update = () => {
window && window.Intercom && window.Intercom("update");
};
@@ -1,40 +0,0 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useUser } from "@app/context";
import { boot as bootIntercom, load as loadIntercom, update as updateIntercom } from "./intercom";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const IntercomProvider = ({ children }: { children: any }) => {
const { user } = useUser();
const router = useRouter();
if (typeof window !== "undefined") {
loadIntercom();
bootIntercom({
name: `${user?.firstName || ""} ${user?.lastName || ""}`,
email: user?.email || "",
created_at: Math.floor((new Date(user?.createdAt)?.getTime() || 0) / 1000)
});
}
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleRouteChange = (url: string) => {
if (typeof window !== "undefined") {
updateIntercom();
}
};
router.events.on("routeChangeStart", handleRouteChange);
// If the component is unmounted, unsubscribe
// from the event with the `off` method:
return () => {
router.events.off("routeChangeStart", handleRouteChange);
};
}, [router.events]);
return children;
};
@@ -39,7 +39,7 @@ export const SecretPathInput = ({
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { folderNames: folders } = useGetFoldersByEnv({ const { folderNames: folders } = useGetFoldersByEnv({
path: secretPath, path: secretPath,
environments: [environment || currentWorkspace?.environments?.[0].slug], environments: [environment || currentWorkspace?.environments?.[0].slug || ""],
projectId: workspaceId projectId: workspaceId
}); });
@@ -1,54 +1,5 @@
import { createContext, ReactNode } from "react";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { useGetUserOrgPermissions } from "@app/hooks/api";
import { OrgUser } from "@app/hooks/api/types";
import { useOrganization } from "../OrganizationContext";
import { TOrgPermission } from "./types";
type Props = {
children: ReactNode;
};
const OrgPermissionContext = createContext<null | {
permission: TOrgPermission;
membership: OrgUser | null;
}>(null);
export const OrgPermissionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { data: permission, isLoading } = useGetUserOrgPermissions({ orgId });
if (isLoading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
decoding="async"
loading="lazy"
alt="infisical loading indicator"
/>
</div>
);
}
if (!permission) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
Failed to load user permissions
</div>
);
}
return (
<OrgPermissionContext.Provider value={permission}>{children}</OrgPermissionContext.Provider>
);
};
export const useOrgPermission = () => { export const useOrgPermission = () => {
const ctx = useRouteContext({ const ctx = useRouteContext({
from: "/_authenticate/_org_details", from: "/_authenticate/_org_details",
@@ -1,3 +1,3 @@
export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext"; export { useOrgPermission } from "./OrgPermissionContext";
export type { TOrgPermission } from "./types"; export type { TOrgPermission } from "./types";
export { OrgPermissionActions, OrgPermissionSubjects } from "./types"; export { OrgPermissionActions, OrgPermissionSubjects } from "./types";
@@ -1,40 +1,5 @@
import { createContext, ReactNode, useContext, useMemo } from "react";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { useGetOrganizations } from "@app/hooks/api";
import { Organization } from "@app/hooks/api/types";
type TOrgContext = {
orgs?: Organization[];
currentOrg?: Organization;
isLoading: boolean;
};
const OrgContext = createContext<TOrgContext | null>(null);
type Props = {
children: ReactNode;
};
export const OrgProvider = ({ children }: Props): JSX.Element => {
const { data: userOrgs, isLoading } = useGetOrganizations();
// const currentWsOrgID = currentWorkspace?.organization;
const currentWsOrgID = localStorage.getItem("orgData.id");
// memorize the workspace details for the context
const value = useMemo<TOrgContext>(
() => ({
orgs: userOrgs,
currentOrg: (userOrgs || []).find(({ id }) => id === currentWsOrgID),
isLoading
}),
[currentWsOrgID, userOrgs, isLoading]
);
return <OrgContext.Provider value={value}>{children}</OrgContext.Provider>;
};
export const useOrganization = () => { export const useOrganization = () => {
const currentOrg = useRouteContext({ const currentOrg = useRouteContext({
from: "/_authenticate/_org_details", from: "/_authenticate/_org_details",
@@ -1 +1 @@
export { OrgProvider, useOrganization } from "./OrganizationContext"; export { useOrganization } from "./OrganizationContext";
@@ -1,86 +1,11 @@
import { createContext, ReactNode, useEffect, useMemo } from "react"; import { adminQueryKeys, fetchServerConfig } from "@app/hooks/api/admin/queries";
import { Helmet } from "react-helmet"; import { useSuspenseQuery } from "@tanstack/react-query";
import { useNavigate, useRouteContext } from "@tanstack/react-router";
import { ContentLoader } from "@app/components/v2/ContentLoader";
import { useGetServerConfig } from "@app/hooks/api";
import { TServerConfig } from "@app/hooks/api/admin/types";
type TServerConfigContext = {
config: TServerConfig;
};
const ServerConfigContext = createContext<TServerConfigContext | null>(null);
type Props = {
children: ReactNode;
};
export const ServerConfigProvider = ({ children }: Props): JSX.Element => {
const navigate = useNavigate();
const { data, isLoading } = useGetServerConfig();
// memorize the workspace details for the context
const value = useMemo<TServerConfigContext>(() => {
return {
config: data!
};
}, [data]);
useEffect(() => {
if (!isLoading && data && !data.initialized && !data.isMigrationModeOn) {
navigate({ to: "/admin/signup" });
}
}, [isLoading, data]);
if (!isLoading && data?.isMigrationModeOn) {
return (
<div className="relative mx-auto flex h-screen w-full flex-col items-center justify-center space-y-8 bg-bunker-800 px-8 text-mineshaft-50 dark:[color-scheme:dark]">
<Helmet>
<title>Infisical Maintenance Mode</title>
<link rel="icon" href="/infisical.ico" />
</Helmet>
<img
src="/images/maintenance.png"
height={175}
width={300}
alt="maintenance mode"
className="w-[40rem]"
/>
<p className="mx-8 mb-4 flex justify-center bg-gradient-to-tr from-mineshaft-300 to-white bg-clip-text text-4xl font-bold text-transparent md:mx-16">
Scheduled Maintenance
</p>
<div className="mt-2 text-center text-lg text-bunker-300">
Infisical is undergoing planned maintenance. <br /> No action is required on your end —
your applications will continue to fetch secrets.
<br /> If you have questions, please{" "}
<a
className="text-bunker-300 underline decoration-primary-800 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/slack"
target="_blank"
rel="noopener noreferrer"
>
join our Slack community
</a>
.
</div>
</div>
);
}
if (isLoading) {
return (
<div className="bg-bunker-800">
<ContentLoader text="Loading configurations" />
</div>
);
}
return <ServerConfigContext.Provider value={value}>{children}</ServerConfigContext.Provider>;
};
export const useServerConfig = () => { export const useServerConfig = () => {
const config = useRouteContext({ from: "__root__", select: (el) => el.serverConfig }); const { data: config } = useSuspenseQuery({
queryKey: adminQueryKeys.serverConfig(),
queryFn: fetchServerConfig
});
return { config }; return { config };
}; };
@@ -1 +1 @@
export { ServerConfigProvider, useServerConfig } from "./ServerConfigContext"; export { useServerConfig } from "./ServerConfigContext";
@@ -1,41 +1,5 @@
import { createContext, ReactNode, useContext, useMemo } from "react";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { useGetOrgSubscription } from "@app/hooks/api";
import { SubscriptionPlan } from "@app/hooks/api/types";
import { useOrganization } from "../OrganizationContext";
type TSubscriptionContext = {
subscription?: SubscriptionPlan;
isLoading: boolean;
};
const SubscriptionContext = createContext<TSubscriptionContext | null>(null);
type Props = {
children: ReactNode;
};
export const SubscriptionProvider = ({ children }: Props): JSX.Element => {
const { currentOrg } = useOrganization();
const { data, isLoading } = useGetOrgSubscription({
orgID: currentOrg?.id || ""
});
// memorize the workspace details for the context
const value = useMemo<TSubscriptionContext>(
() => ({
subscription: data,
isLoading
}),
[data, isLoading]
);
return <SubscriptionContext.Provider value={value}>{children}</SubscriptionContext.Provider>;
};
export const useSubscription = () => { export const useSubscription = () => {
const subscription = useRouteContext({ const subscription = useRouteContext({
from: "/_authenticate/_org_details", from: "/_authenticate/_org_details",
@@ -1 +1 @@
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext"; export { useSubscription } from "./SubscriptionContext";
@@ -1,49 +1,5 @@
import { createContext, ReactNode, useContext, useMemo } from "react";
import { useRouteContext } from "@tanstack/react-router"; import { useRouteContext } from "@tanstack/react-router";
import { useGetUser } from "@app/hooks/api";
import { User, UserEnc } from "@app/hooks/api/types";
type TUserContext = {
user: User & UserEnc;
isLoading: boolean;
};
const UserContext = createContext<TUserContext | null>(null);
type Props = {
children: ReactNode;
};
export const UserProvider = ({ children }: Props): JSX.Element => {
const { data, isLoading } = useGetUser();
// memorize the workspace details for the context
const value = useMemo<TUserContext>(() => {
return {
user: data!,
isLoading
};
}, [data, isLoading]);
if (isLoading) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
<img
src="/images/loading/loading.gif"
height={70}
width={120}
decoding="async"
loading="lazy"
alt="infisical loading indicator"
/>
</div>
);
}
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
};
export const useUser = () => { export const useUser = () => {
const user = useRouteContext({ from: "/_authenticate", select: (el) => el.user })!; const user = useRouteContext({ from: "/_authenticate", select: (el) => el.user })!;
@@ -1 +1 @@
export { UserProvider, useUser } from "./UserContext"; export { useUser } from "./UserContext";
+4 -5
View File
@@ -1,9 +1,8 @@
export { AuthProvider } from "./AuthContext"; export { AuthProvider } from "./AuthContext";
export { OrgProvider, useOrganization } from "./OrganizationContext"; export { useOrganization } from "./OrganizationContext";
export type { TOrgPermission } from "./OrgPermissionContext"; export type { TOrgPermission } from "./OrgPermissionContext";
export { export {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionProvider,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrgPermission useOrgPermission
} from "./OrgPermissionContext"; } from "./OrgPermissionContext";
@@ -16,7 +15,7 @@ export {
ProjectPermissionSub, ProjectPermissionSub,
useProjectPermission useProjectPermission
} from "./ProjectPermissionContext"; } from "./ProjectPermissionContext";
export { ServerConfigProvider, useServerConfig } from "./ServerConfigContext"; export { useServerConfig } from "./ServerConfigContext";
export { SubscriptionProvider, useSubscription } from "./SubscriptionContext"; export { useSubscription } from "./SubscriptionContext";
export { UserProvider, useUser } from "./UserContext"; export { useUser } from "./UserContext";
export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext"; export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext";
+3 -4
View File
@@ -60,16 +60,15 @@ export const initProjectHelper = async ({ projectName }: { projectName: string }
return project; return project;
}; };
export const getProjectHomePage = (workspace: Workspace) => { export const getProjectHomePage = (workspace: Workspace) => {
if (workspace.type === ProjectType.SecretManager) { if (workspace.type === ProjectType.SecretManager) {
return `/${workspace.type}/${workspace.id}/secrets/overview`; return `/${workspace.type}/$projectId/secrets/overview` as const;
} }
if (workspace.type === ProjectType.CertificateManager) { if (workspace.type === ProjectType.CertificateManager) {
return `/${workspace.type}/${workspace.id}/certificates`; return `/${workspace.type}/$projectId/certificates` as const;
} }
return `/${workspace.type}/${workspace.id}/kms`; return `/${workspace.type}/$projectId/kms` as const;
}; };
export const getProjectTitle = (type: ProjectType) => { export const getProjectTitle = (type: ProjectType) => {
+3 -4
View File
@@ -89,10 +89,9 @@ export const useSelectOrganization = () => {
return data; return data;
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries([ queryClient.invalidateQueries({
organizationKeys.getUserOrganizations, queryKey: [organizationKeys.getUserOrganizations, workspaceKeys.getAllUserWorkspace]
workspaceKeys.getAllUserWorkspace });
]);
} }
}); });
}; };
@@ -21,7 +21,9 @@ export const useImportEnvKey = () => {
"Content-Type": "multipart/form-data" "Content-Type": "multipart/form-data"
}, },
onUploadProgress: (progressEvent) => { onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total); const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent?.total || 1)
);
console.log(`Upload Progress: ${percentCompleted}%`); console.log(`Upload Progress: ${percentCompleted}%`);
} }
}); });
@@ -143,7 +143,7 @@ const fetchSecretApprovalRequestList = async ({
export const useGetSecretApprovalRequests = ({ export const useGetSecretApprovalRequests = ({
workspaceId, workspaceId,
environment, environment,
options = object, options = {},
status, status,
limit = 20, limit = 20,
committer committer
@@ -1,5 +1,7 @@
import { apiRequest } from "@app/config/request";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TSecretScanningGitRisks } from "./types"; import { TSecretScanningGitRisks } from "./types";
export const secretScanningQueryKeys = { export const secretScanningQueryKeys = {
@@ -49,9 +49,10 @@ export const useGetActiveSharedSecretById = ({
hashedHex: string | null; hashedHex: string | null;
password?: string; password?: string;
}) => { }) => {
return useQuery<TViewSharedSecretResponse>( return useQuery({
secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }), queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
async () => {
queryFn: async () => {
const { data } = await apiRequest.post<TViewSharedSecretResponse>( const { data } = await apiRequest.post<TViewSharedSecretResponse>(
`/api/v1/secret-sharing/public/${sharedSecretId}`, `/api/v1/secret-sharing/public/${sharedSecretId}`,
{ {
@@ -61,9 +62,8 @@ export const useGetActiveSharedSecretById = ({
); );
return data; return data;
},
{
enabled: Boolean(sharedSecretId)
} }
); }, {
enabled: Boolean(sharedSecretId)
});
}; };
@@ -148,14 +148,18 @@ export const usePerformSecretRollback = () => {
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, directory }) => { onSuccess: (_, { workspaceId, environment, directory }) => {
queryClient.invalidateQueries([ queryClient.invalidateQueries({
{ workspaceId, environment, secretPath: directory }, queryKey: [
"secrets" { workspaceId, environment, secretPath: directory },
]); "secrets"
queryClient.invalidateQueries([ ]
"secret-folders", });
{ projectId: workspaceId, environment, path: directory } queryClient.invalidateQueries({
]); queryKey: [
"secret-folders",
{ projectId: workspaceId, environment, path: directory }
]
});
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.list({ workspaceId, environment, directory }) secretSnapshotKeys.list({ workspaceId, environment, directory })
); );
@@ -148,9 +148,12 @@ export const useGetUserWorkspaces = ({
includeRoles?: boolean; includeRoles?: boolean;
type?: ProjectType | "all"; type?: ProjectType | "all";
} = {}) => } = {}) =>
useQuery(workspaceKeys.getAllUserWorkspace(type || ""), () => useQuery({
fetchUserWorkspaces(includeRoles, type) queryKey: workspaceKeys.getAllUserWorkspace(type || ""),
);
queryFn: () =>
fetchUserWorkspaces(includeRoles, type)
});
const fetchUserWorkspaceMemberships = async (orgId: string) => { const fetchUserWorkspaceMemberships = async (orgId: string) => {
const { data } = await apiRequest.get<Record<string, Workspace[]>>( const { data } = await apiRequest.get<Record<string, Workspace[]>>(
@@ -324,7 +327,9 @@ export const useDeleteWorkspace = () => {
}, },
onSuccess: (dto) => { onSuccess: (dto) => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type)); queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace(dto.type));
queryClient.invalidateQueries(["org-admin-projects"]); queryClient.invalidateQueries({
queryKey: ["org-admin-projects"]
});
} }
}); });
}; };
@@ -74,7 +74,9 @@ export const OrganizationLayout = () => {
<div className="px-1"> <div className="px-1">
<Menu className="mt-4"> <Menu className="mt-4">
<Link <Link
to={`/organization/$organizationId/${ProjectType.SecretManager}` as const} to={
`/organization/$organizationId/${ProjectType.SecretManager}/overview` as const
}
params={{ organizationId: currentOrg.id }} params={{ organizationId: currentOrg.id }}
activeOptions={{ exact: true }} activeOptions={{ exact: true }}
> >
@@ -86,7 +88,7 @@ export const OrganizationLayout = () => {
</Link> </Link>
<Link <Link
to={ to={
`/organization/$organizationid/${ProjectType.CertificateManager}` as const `/organization/$organizationId/${ProjectType.CertificateManager}/overview` as const
} }
params={{ organizationId: currentOrg.id }} params={{ organizationId: currentOrg.id }}
activeOptions={{ exact: true }} activeOptions={{ exact: true }}
@@ -98,7 +100,7 @@ export const OrganizationLayout = () => {
)} )}
</Link> </Link>
<Link <Link
to={`/organization/$organizationId/${ProjectType.KMS}` as const} to={`/organization/$organizationId/${ProjectType.KMS}/overview` as const}
params={{ organizationId: currentOrg.id }} params={{ organizationId: currentOrg.id }}
activeOptions={{ exact: true }} activeOptions={{ exact: true }}
> >
@@ -32,7 +32,9 @@ export const PersonalSettingsLayout = () => {
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div className="flex-grow"> <div className="flex-grow">
<Link <Link
to={`/organization/$organizationId/${ProjectType.SecretManager}` as const} to={
`/organization/$organizationId/${ProjectType.SecretManager}/overview` as const
}
params={{ organizationId }} params={{ organizationId }}
> >
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100"> <div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
+1 -1
View File
@@ -30,7 +30,7 @@ const RootPage = () => {
export const Route = createRootRouteWithContext<TRouterContext>()({ export const Route = createRootRouteWithContext<TRouterContext>()({
component: RootPage, component: RootPage,
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
const serverConfig = await context.queryClient.fetchQuery({ const serverConfig = await context.queryClient.ensureQueryData({
queryKey: adminQueryKeys.serverConfig(), queryKey: adminQueryKeys.serverConfig(),
queryFn: fetchServerConfig queryFn: fetchServerConfig
}); });
@@ -1,6 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { format } from "date-fns"; import { format } from "date-fns";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
@@ -31,7 +32,6 @@ import {
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks"; import { useDebounce } from "@app/hooks";
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api"; import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
import { useNavigate } from "@tanstack/react-router";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
export const OrgAdminProjects = withPermission( export const OrgAdminProjects = withPermission(
@@ -1,38 +1,31 @@
import { useState } from 'react' import { useState } from "react";
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from '@app/components/v2' import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgAdminProjects } from './-components/OrgAdminProjects' import { OrgAdminProjects } from "./-components/OrgAdminProjects";
enum TabSections { enum TabSections {
Projects = 'projects', Projects = "projects"
} }
const OrgAdminPage = () => { const OrgAdminPage = () => {
const { t } = useTranslation() const { t } = useTranslation();
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Projects) const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Projects);
return ( return (
<> <>
<Helmet> <Helmet>
<title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
{t('common.head-title', { title: t('settings.org.title') })}
</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
<div className="w-full max-w-6xl px-6"> <div className="w-full max-w-6xl px-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-3xl font-semibold text-gray-200"> <p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p>
Organization Admin Console
</p>
</div> </div>
<Tabs <Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
value={activeTab}
onValueChange={(el) => setActiveTab(el as TabSections)}
>
<TabList> <TabList>
<Tab value={TabSections.Projects}>Projects</Tab> <Tab value={TabSections.Projects}>Projects</Tab>
</TabList> </TabList>
@@ -43,11 +36,11 @@ const OrgAdminPage = () => {
</div> </div>
</div> </div>
</> </>
) );
} };
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/admin/"
)({ )({
component: OrgAdminPage, component: OrgAdminPage
}) });
@@ -1,52 +1,50 @@
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import { OrgPermissionActions, OrgPermissionSubjects } from '@app/context' import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from '@app/hoc' import { withPermission } from "@app/hoc";
import { BillingTabGroup } from './-components' import { BillingTabGroup } from "./-components";
const BillngPage = withPermission( const BillngPage = withPermission(
() => { () => {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl px-6">
<div className="my-6"> <div className="my-6">
<p className="text-3xl font-semibold text-gray-200"> <p className="text-3xl font-semibold text-gray-200">{t("billing.title")}</p>
{t('billing.title')}
</p>
<div /> <div />
</div> </div>
<BillingTabGroup /> <BillingTabGroup />
</div> </div>
</div> </div>
) );
}, },
{ {
action: OrgPermissionActions.Read, action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.Billing, subject: OrgPermissionSubjects.Billing
}, }
) );
const BillingRoute = () => { const BillingRoute = () => {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<div className="h-full bg-bunker-800"> <div className="h-full bg-bunker-800">
<Helmet> <Helmet>
<title>{t('common.head-title', { title: t('billing.title') })}</title> <title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" /> <meta property="og:image" content="/images/message.png" />
</Helmet> </Helmet>
<BillngPage /> <BillngPage />
</div> </div>
) );
} };
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/billing/"
)({ )({
component: BillingRoute, component: BillingRoute
}) });
@@ -1,12 +1,12 @@
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from '@fortawesome/free-solid-svg-icons' import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { createFileRoute, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from 'tailwind-merge' import { twMerge } from "tailwind-merge";
import { createNotification } from '@app/components/notifications' import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from '@app/components/permissions' import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
@@ -16,81 +16,74 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
Spinner, Spinner,
Tooltip, Tooltip,
UpgradePlanModal, UpgradePlanModal
} from '@app/components/v2' } from "@app/components/v2";
import { import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
OrgPermissionActions, import { withPermission } from "@app/hoc";
OrgPermissionSubjects, import { useDeleteGroup } from "@app/hooks/api";
useOrganization, import { useGetGroupById } from "@app/hooks/api/groups/queries";
} from '@app/context' import { usePopUp } from "@app/hooks/usePopUp";
import { withPermission } from '@app/hoc'
import { useDeleteGroup } from '@app/hooks/api'
import { useGetGroupById } from '@app/hooks/api/groups/queries'
import { usePopUp } from '@app/hooks/usePopUp'
import { GroupCreateUpdateModal } from './-components/GroupCreateUpdateModal' import { GroupCreateUpdateModal } from "./-components/GroupCreateUpdateModal";
import { GroupMembersSection } from './-components/GroupMembersSection' import { GroupMembersSection } from "./-components/GroupMembersSection";
import { GroupDetailsSection } from './-components' import { GroupDetailsSection } from "./-components";
export enum TabSections { export enum TabSections {
Member = 'members', Member = "members",
Groups = 'groups', Groups = "groups",
Roles = 'roles', Roles = "roles",
Identities = 'identities', Identities = "identities"
} }
const GroupPage = withPermission( const GroupPage = withPermission(
() => { () => {
const navigate = useNavigate() const navigate = useNavigate();
const params = useParams({ const params = useParams({
from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId', from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/"
}) });
const groupId = params.groupId as string const groupId = params.groupId as string;
const { currentOrg } = useOrganization() const { currentOrg } = useOrganization();
const { data, isLoading } = useGetGroupById(groupId) const { data, isLoading } = useGetGroupById(groupId);
const { mutateAsync: deleteMutateAsync } = useDeleteGroup() const { mutateAsync: deleteMutateAsync } = useDeleteGroup();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
usePopUp(['groupCreateUpdate', 'deleteGroup', 'upgradePlan'] as const) "groupCreateUpdate",
"deleteGroup",
"upgradePlan"
] as const);
const onDeleteGroupSubmit = async ({ const onDeleteGroupSubmit = async ({ name, id }: { name: string; id: string }) => {
name,
id,
}: {
name: string
id: string
}) => {
try { try {
await deleteMutateAsync({ await deleteMutateAsync({
id, id
}) });
createNotification({ createNotification({
text: `Successfully deleted the ${name} group`, text: `Successfully deleted the ${name} group`,
type: 'success', type: "success"
}) });
navigate({ navigate({
to: '/organization/$organizationId/members' as const, to: "/organization/$organizationId/members" as const,
params: { params: {
organizationId: currentOrg.id, organizationId: currentOrg.id
}, },
search: { search: {
selectedTab: TabSections.Groups, selectedTab: TabSections.Groups
}, }
}) });
} catch (err) { } catch (err) {
console.error(err) console.error(err);
createNotification({ createNotification({
text: `Failed to delete the ${name} group`, text: `Failed to delete the ${name} group`,
type: 'error', type: "error"
}) });
} }
handlePopUpClose('deleteGroup') handlePopUpClose("deleteGroup");
} };
if (isLoading) return <Spinner size="sm" className="ml-2 mt-2" /> if (isLoading) return <Spinner size="sm" className="ml-2 mt-2" />;
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
@@ -102,23 +95,21 @@ const GroupPage = withPermission(
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />} leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => { onClick={() => {
navigate({ navigate({
to: '/organization/$organizationId/members' as const, to: "/organization/$organizationId/members" as const,
params: { params: {
organizationId: currentOrg.id, organizationId: currentOrg.id
}, },
search: { search: {
selectedTab: TabSections.Groups, selectedTab: TabSections.Groups
}, }
}) });
}} }}
className="mb-4" className="mb-4"
> >
Groups Groups
</Button> </Button>
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white"> <p className="text-3xl font-semibold text-white">{data.group.name}</p>
{data.group.name}
</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -128,23 +119,19 @@ const GroupPage = withPermission(
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Groups}>
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Groups}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
!isAllowed && !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
'pointer-events-none cursor-not-allowed opacity-50',
)} )}
onClick={async () => { onClick={async () => {
handlePopUpOpen('groupCreateUpdate', { handlePopUpOpen("groupCreateUpdate", {
groupId, groupId,
name: data.group.name, name: data.group.name,
slug: data.group.slug, slug: data.group.slug,
role: data.group.role, role: data.group.role
}) });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -160,14 +147,14 @@ const GroupPage = withPermission(
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
isAllowed isAllowed
? 'hover:!bg-red-500 hover:!text-white' ? "hover:!bg-red-500 hover:!text-white"
: 'pointer-events-none cursor-not-allowed opacity-50', : "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => { onClick={async () => {
handlePopUpOpen('deleteGroup', { handlePopUpOpen("deleteGroup", {
id: groupId, id: groupId,
name: data.group.name, name: data.group.name
}) });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -180,15 +167,9 @@ const GroupPage = withPermission(
</div> </div>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<GroupDetailsSection <GroupDetailsSection groupId={groupId} handlePopUpOpen={handlePopUpOpen} />
groupId={groupId}
handlePopUpOpen={handlePopUpOpen}
/>
</div> </div>
<GroupMembersSection <GroupMembersSection groupId={groupId} groupSlug={data.group.slug} />
groupId={groupId}
groupSlug={data.group.slug}
/>
</div> </div>
</div> </div>
)} )}
@@ -200,46 +181,40 @@ const GroupPage = withPermission(
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteGroup.isOpen} isOpen={popUp.deleteGroup.isOpen}
title={`Are you sure want to delete the group named ${ title={`Are you sure want to delete the group named ${
(popUp?.deleteGroup?.data as { name: string })?.name || '' (popUp?.deleteGroup?.data as { name: string })?.name || ""
}?`} }?`}
onChange={(isOpen) => handlePopUpToggle('deleteGroup', isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteGroup", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
onDeleteGroupSubmit( onDeleteGroupSubmit(popUp?.deleteGroup?.data as { name: string; id: string })
popUp?.deleteGroup?.data as { name: string; id: string },
)
} }
/> />
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={ text={(popUp.upgradePlan?.data as { description: string })?.description}
(popUp.upgradePlan?.data as { description: string })?.description
}
/> />
</div> </div>
) );
}, },
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Groups }, { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Groups }
) );
const GroupDetailPage = () => { const GroupDetailPage = () => {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<> <>
<Helmet> <Helmet>
<title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
{t('common.head-title', { title: t('settings.org.title') })}
</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<GroupPage /> <GroupPage />
</> </>
) );
} };
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/groups/$groupId/"
)({ )({
component: GroupDetailPage, component: GroupDetailPage
}) });
@@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useState } from 'react' import { useState } from "react";
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from '@fortawesome/free-solid-svg-icons' import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { createFileRoute, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from 'tailwind-merge' import { twMerge } from "tailwind-merge";
import { createNotification } from '@app/components/notifications' import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from '@app/components/permissions' import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
@@ -17,168 +17,155 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
Tooltip, Tooltip,
UpgradePlanModal, UpgradePlanModal
} from '@app/components/v2' } from "@app/components/v2";
import { import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
OrgPermissionActions, import { withPermission } from "@app/hoc";
OrgPermissionSubjects,
useOrganization,
} from '@app/context'
import { withPermission } from '@app/hoc'
import { import {
IdentityAuthMethod, IdentityAuthMethod,
useDeleteIdentity, useDeleteIdentity,
useGetIdentityById, useGetIdentityById,
useRevokeIdentityTokenAuthToken, useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret, useRevokeIdentityUniversalAuthClientSecret
} from '@app/hooks/api' } from "@app/hooks/api";
import { Identity } from '@app/hooks/api/identities/types' import { Identity } from "@app/hooks/api/identities/types";
import { usePopUp } from '@app/hooks/usePopUp' import { usePopUp } from "@app/hooks/usePopUp";
import { TabSections } from '@app/types/org' import { TabSections } from "@app/types/org";
import { IdentityAuthMethodModal } from '../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal' import { IdentityAuthMethodModal } from "../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
import { IdentityModal } from '../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityModal' import { IdentityModal } from "../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityModal";
import { IdentityUniversalAuthClientSecretModal } from '../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal' import { IdentityUniversalAuthClientSecretModal } from "../../members/-components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal";
import { import {
IdentityAuthenticationSection, IdentityAuthenticationSection,
IdentityClientSecretModal, IdentityClientSecretModal,
IdentityDetailsSection, IdentityDetailsSection,
IdentityProjectsSection, IdentityProjectsSection,
IdentityTokenListModal, IdentityTokenListModal,
IdentityTokenModal, IdentityTokenModal
} from './-components' } from "./-components";
export const IdentitySection = withPermission( export const IdentitySection = withPermission(
() => { () => {
const navigate = useNavigate() const navigate = useNavigate();
const params = useParams({ const params = useParams({
from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId', from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/"
}) });
const identityId = params.identityId as string const identityId = params.identityId as string;
const { currentOrg } = useOrganization() const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || '' const orgId = currentOrg?.id || "";
const { data } = useGetIdentityById(identityId) const { data } = useGetIdentityById(identityId);
const { mutateAsync: deleteIdentity } = useDeleteIdentity() const { mutateAsync: deleteIdentity } = useDeleteIdentity();
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken() const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const { mutateAsync: revokeClientSecret } = const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
useRevokeIdentityUniversalAuthClientSecret()
const [selectedAuthMethod, setSelectedAuthMethod] = useState< const [selectedAuthMethod, setSelectedAuthMethod] = useState<
Identity['authMethods'][number] | null Identity["authMethods"][number] | null
>(null) >(null);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
usePopUp([ "identity",
'identity', "deleteIdentity",
'deleteIdentity', "identityAuthMethod",
'identityAuthMethod', "revokeAuthMethod",
'revokeAuthMethod', "token",
'token', "tokenList",
'tokenList', "revokeToken",
'revokeToken', "clientSecret",
'clientSecret', "revokeClientSecret",
'revokeClientSecret', "universalAuthClientSecret", // list of client secrets
'universalAuthClientSecret', // list of client secrets "upgradePlan"
'upgradePlan', ] as const);
] as const)
const onDeleteIdentitySubmit = async (id: string) => { const onDeleteIdentitySubmit = async (id: string) => {
try { try {
await deleteIdentity({ await deleteIdentity({
identityId: id, identityId: id,
organizationId: orgId, organizationId: orgId
}) });
createNotification({ createNotification({
text: 'Successfully deleted identity', text: "Successfully deleted identity",
type: 'success', type: "success"
}) });
handlePopUpClose('deleteIdentity') handlePopUpClose("deleteIdentity");
navigate({ navigate({
to: `/organization/${orgId}/members` as const, to: "/organization/$organizationId/members",
search: { params: {
selectedTab: TabSections.Identities, organizationId: orgId
}, },
}) search: {
selectedTab: TabSections.Identities
}
});
} catch (err) { } catch (err) {
console.error(err) console.error(err);
const error = err as any const error = err as any;
const text = const text = error?.response?.data?.message ?? "Failed to delete identity";
error?.response?.data?.message ?? 'Failed to delete identity'
createNotification({ createNotification({
text, text,
type: 'error', type: "error"
}) });
} }
} };
const onRevokeTokenSubmit = async ({ const onRevokeTokenSubmit = async ({
identityId: parentIdentityId, identityId: parentIdentityId,
tokenId, tokenId,
name, name
}: { }: {
identityId: string identityId: string;
tokenId: string tokenId: string;
name: string name: string;
}) => { }) => {
try { try {
await revokeToken({ await revokeToken({
identityId: parentIdentityId, identityId: parentIdentityId,
tokenId, tokenId
}) });
handlePopUpClose('revokeToken') handlePopUpClose("revokeToken");
createNotification({ createNotification({
text: `Successfully revoked token ${name ?? ''}`, text: `Successfully revoked token ${name ?? ""}`,
type: 'success', type: "success"
}) });
} catch (err) { } catch (err) {
console.error(err) console.error(err);
const error = err as any const error = err as any;
const text = const text = error?.response?.data?.message ?? "Failed to delete identity";
error?.response?.data?.message ?? 'Failed to delete identity'
createNotification({ createNotification({
text, text,
type: 'error', type: "error"
}) });
} }
} };
const onDeleteClientSecretSubmit = async ({ const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
clientSecretId,
}: {
clientSecretId: string
}) => {
try { try {
if ( if (!data?.identity.id || selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) return;
!data?.identity.id ||
selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH
)
return
await revokeClientSecret({ await revokeClientSecret({
identityId: data?.identity.id, identityId: data?.identity.id,
clientSecretId, clientSecretId
}) });
handlePopUpToggle('revokeClientSecret', false) handlePopUpToggle("revokeClientSecret", false);
createNotification({ createNotification({
text: 'Successfully deleted client secret', text: "Successfully deleted client secret",
type: 'success', type: "success"
}) });
} catch (err) { } catch (err) {
console.error(err) console.error(err);
createNotification({ createNotification({
text: 'Failed to delete client secret', text: "Failed to delete client secret",
type: 'error', type: "error"
}) });
} }
} };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
@@ -190,20 +177,21 @@ export const IdentitySection = withPermission(
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />} leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => { onClick={() => {
navigate({ navigate({
to: `/organization/${orgId}/members` as const, to: "/organization/$organizationId/members",
search: { params: {
selectedTab: TabSections.Identities, organizationId: orgId
}, },
}) search: {
selectedTab: TabSections.Identities
}
});
}} }}
className="mb-4" className="mb-4"
> >
Identities Identities
</Button> </Button>
<div className="mb-4 flex items-center justify-between"> <div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white"> <p className="text-3xl font-semibold text-white">{data.identity.name}</p>
{data.identity.name}
</p>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <div className="hover:text-primary-400 data-[state=open]:text-primary-400">
@@ -220,16 +208,15 @@ export const IdentitySection = withPermission(
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
!isAllowed && !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
'pointer-events-none cursor-not-allowed opacity-50',
)} )}
onClick={async () => { onClick={async () => {
handlePopUpOpen('identity', { handlePopUpOpen("identity", {
identityId, identityId,
name: data.identity.name, name: data.identity.name,
role: data.role, role: data.role,
customRole: data.customRole, customRole: data.customRole
}) });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -244,15 +231,14 @@ export const IdentitySection = withPermission(
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
!isAllowed && !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
'pointer-events-none cursor-not-allowed opacity-50',
)} )}
onClick={async () => { onClick={async () => {
handlePopUpOpen('identityAuthMethod', { handlePopUpOpen("identityAuthMethod", {
identityId, identityId,
name: data.identity.name, name: data.identity.name,
allAuthMethods: data.identity.authMethods, allAuthMethods: data.identity.authMethods
}) });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -268,14 +254,14 @@ export const IdentitySection = withPermission(
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
isAllowed isAllowed
? 'hover:!bg-red-500 hover:!text-white' ? "hover:!bg-red-500 hover:!text-white"
: 'pointer-events-none cursor-not-allowed opacity-50', : "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => { onClick={async () => {
handlePopUpOpen('deleteIdentity', { handlePopUpOpen("deleteIdentity", {
identityId, identityId,
name: data.identity.name, name: data.identity.name
}) });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -288,10 +274,7 @@ export const IdentitySection = withPermission(
</div> </div>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<IdentityDetailsSection <IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
identityId={identityId}
handlePopUpOpen={handlePopUpOpen}
/>
<IdentityAuthenticationSection <IdentityAuthenticationSection
selectedAuthMethod={selectedAuthMethod} selectedAuthMethod={selectedAuthMethod}
setSelectedAuthMethod={setSelectedAuthMethod} setSelectedAuthMethod={setSelectedAuthMethod}
@@ -309,19 +292,13 @@ export const IdentitySection = withPermission(
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
<IdentityTokenModal <IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityTokenListModal <IdentityTokenListModal
popUp={popUp} popUp={popUp}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
/> />
<IdentityClientSecretModal <IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
popUp={popUp}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityUniversalAuthClientSecretModal <IdentityUniversalAuthClientSecretModal
popUp={popUp} popUp={popUp}
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
@@ -329,87 +306,82 @@ export const IdentitySection = withPermission(
/> />
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={ text={(popUp.upgradePlan?.data as { description: string })?.description}
(popUp.upgradePlan?.data as { description: string })?.description
}
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteIdentity.isOpen} isOpen={popUp.deleteIdentity.isOpen}
title={`Are you sure want to delete ${ title={`Are you sure want to delete ${
(popUp?.deleteIdentity?.data as { name: string })?.name || '' (popUp?.deleteIdentity?.data as { name: string })?.name || ""
}?`} }?`}
onChange={(isOpen) => handlePopUpToggle('deleteIdentity', isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteApproved={() =>
onDeleteIdentitySubmit( onDeleteIdentitySubmit(
(popUp?.deleteIdentity?.data as { identityId: string }) (popUp?.deleteIdentity?.data as { identityId: string })?.identityId
?.identityId,
) )
} }
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.revokeToken.isOpen} isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${ title={`Are you sure want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || '' (popUp?.revokeToken?.data as { name: string })?.name || ""
}?`} }?`}
onChange={(isOpen) => handlePopUpToggle('revokeToken', isOpen)} onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => { onDeleteApproved={() => {
const revokeTokenData = popUp?.revokeToken?.data as { const revokeTokenData = popUp?.revokeToken?.data as {
identityId: string identityId: string;
tokenId: string tokenId: string;
name: string name: string;
} };
return onRevokeTokenSubmit(revokeTokenData) return onRevokeTokenSubmit(revokeTokenData);
}} }}
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen} isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${ title={`Are you sure want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string }) (popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })
?.clientSecretPrefix || '' ?.clientSecretPrefix || ""
}************?`} }************?`}
onChange={(isOpen) => handlePopUpToggle('revokeClientSecret', isOpen)} onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => { onDeleteApproved={() => {
const deleteClientSecretData = popUp?.revokeClientSecret?.data as { const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
clientSecretId: string clientSecretId: string;
clientSecretPrefix: string clientSecretPrefix: string;
} };
return onDeleteClientSecretSubmit({ return onDeleteClientSecretSubmit({
clientSecretId: deleteClientSecretData.clientSecretId, clientSecretId: deleteClientSecretData.clientSecretId
}) });
}} }}
/> />
</div> </div>
) );
}, },
{ {
action: OrgPermissionActions.Read, action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.Identity, subject: OrgPermissionSubjects.Identity
}, }
) );
const IdentityDetailPage = () => { const IdentityDetailPage = () => {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<> <>
<Helmet> <Helmet>
<title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
{t('common.head-title', { title: t('settings.org.title') })}
</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<IdentitySection /> <IdentitySection />
</> </>
) );
} };
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/identities/$identityId/"
)({ )({
component: IdentityDetailPage, component: IdentityDetailPage
}) });
@@ -1,13 +1,13 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import { ProjectType } from '@app/hooks/api/workspace/types' import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProductOverview } from '../secret-manager/overview' import { ProductOverview } from "../secret-manager/overview";
const KeyManagerOverviewPage = () => <ProductOverview type={ProjectType.KMS} /> const KeyManagerOverviewPage = () => <ProductOverview type={ProjectType.KMS} />;
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview', "/_authenticate/_org_details/_org-layout/organization/$organizationId/kms/overview"
)({ )({
component: KeyManagerOverviewPage, component: KeyManagerOverviewPage
}) });
@@ -15,4 +15,3 @@ export const OrgGroupsTab = () => {
</motion.div> </motion.div>
); );
}; };
@@ -8,6 +8,7 @@ import {
faUsers faUsers
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -37,7 +38,6 @@ import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api"; import { useGetOrganizationGroups, useGetOrgRoles, useUpdateGroup } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { useNavigate } from "@tanstack/react-router";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
@@ -1 +1 @@
export { OrgGroupsSection } from "./OrgGroupsSection"; export { OrgGroupsSection } from "./OrgGroupsSection";
@@ -1 +1 @@
export { OrgGroupsSection } from "./OrgGroupsSection"; export { OrgGroupsSection } from "./OrgGroupsSection";
@@ -1 +1 @@
export { OrgGroupsTab } from "./OrgGroupsTab"; export { OrgGroupsTab } from "./OrgGroupsTab";
@@ -6,6 +6,7 @@ import {
faServer faServer
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -37,7 +38,6 @@ import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types"; import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { useNavigate } from "@tanstack/react-router";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
@@ -254,7 +254,13 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
)} )}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigate.push(`/org/${organizationId}/identities/${id}`); navigate({
to: "/organization/$organizationId/identities/$identityId",
params: {
organizationId,
identityId: id
}
});
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -1,54 +1,40 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { useEffect, useState } from 'react' import { useEffect, useState } from "react";
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { createFileRoute, useNavigate, useSearch } from '@tanstack/react-router' import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { Tab, TabList, TabPanel, Tabs } from '@app/components/v2' import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from '@app/context' import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from '@app/hoc' import { withPermission } from "@app/hoc";
import { TabSections } from '@app/types/org' import { TabSections } from "@app/types/org";
import { import { OrgGroupsTab, OrgIdentityTab, OrgMembersTab, OrgRoleTabSection } from "./-components";
OrgGroupsTab,
OrgIdentityTab,
OrgMembersTab,
OrgRoleTabSection,
} from './-components'
export const MembersSection = withPermission( export const MembersSection = withPermission(
() => { () => {
const navigate = useNavigate({ const navigate = useNavigate({
from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/members', from: "/organization/$organizationId/members"
}) });
const search = useSearch({ const selectedTab = useSearch({
from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/members', from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/members/",
}) select: (el) => el.selectedTab,
const selectedTab = search.selectedTab as TabSections structuralSharing: true
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Member) });
useEffect(() => {
if (
selectedTab &&
Object.values(TabSections).includes(selectedTab as TabSections)
) {
setActiveTab(selectedTab)
}
}, [selectedTab])
const updateSelectedTab = (tab: string) => { const updateSelectedTab = (tab: string) => {
navigate({ navigate({
search: { ...search, selectedTab: tab }, search: { selectedTab: tab }
}) });
} };
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6"> <div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<p className="mb-4 mr-4 text-3xl font-semibold text-white"> <p className="mb-4 mr-4 text-3xl font-semibold text-white">Organization Access Control</p>
Organization Access Control <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
</p>
<Tabs value={activeTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={TabSections.Member}>Users</Tab> <Tab value={TabSections.Member}>Users</Tab>
<Tab value={TabSections.Groups}>Groups</Tab> <Tab value={TabSections.Groups}>Groups</Tab>
@@ -74,29 +60,32 @@ export const MembersSection = withPermission(
</Tabs> </Tabs>
</div> </div>
</div> </div>
) );
}, },
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member }, { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Member }
) );
const MembersPage = () => { const MembersPage = () => {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<> <>
<Helmet> <Helmet>
<title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
{t('common.head-title', { title: t('settings.org.title') })}
</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<MembersSection /> <MembersSection />
</> </>
) );
} };
const MembersPageQuerySchema = z.object({
selectedTab: z.string().catch(TabSections.Member)
});
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/members/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/members/"
)({ )({
component: MembersPage, component: MembersPage,
}) validateSearch: zodValidator(MembersPageQuerySchema)
});
@@ -7,6 +7,8 @@ import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@a
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { OrgUser } from "@app/hooks/api/types"; import { OrgUser } from "@app/hooks/api/types";
import { LogsSection } from "../../../../audit-logs/-components";
type Props = { type Props = {
orgMembership: OrgUser; orgMembership: OrgUser;
}; };
@@ -1,6 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { faTrash } from "@fortawesome/free-solid-svg-icons"; import { faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2"; import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
@@ -8,7 +9,6 @@ import { useWorkspace } from "@app/context";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { TWorkspaceUser } from "@app/hooks/api/types"; import { TWorkspaceUser } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { useNavigate } from "@tanstack/react-router";
import { TabSections } from "@app/types/org"; import { TabSections } from "@app/types/org";
type Props = { type Props = {
@@ -1,15 +1,16 @@
import { ProjectType } from '@app/hooks/api/workspace/types' import { createFileRoute, redirect } from "@tanstack/react-router";
import { createFileRoute, redirect } from '@tanstack/react-router'
import { ProjectType } from "@app/hooks/api/workspace/types";
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/overview/"
)({ )({
beforeLoad: ({ context }) => { beforeLoad: ({ context }) => {
throw redirect({ throw redirect({
to: `/organization/$organizationId/${ProjectType.SecretManager}` as const, to: `/organization/$organizationId/${ProjectType.SecretManager}/overview` as const,
params: { params: {
organizationId: context.organizationId, organizationId: context.organizationId
}, }
}) });
}, }
}) });
@@ -1,6 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -9,7 +10,6 @@ import { useOrganization } from "@app/context";
import { useCreateOrgRole, useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api"; import { useCreateOrgRole, useGetOrgRole, useUpdateOrgRole } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
import { useNavigate } from "@tanstack/react-router";
const schema = z const schema = z
.object({ .object({
@@ -104,7 +104,7 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
handlePopUpToggle("role", false); handlePopUpToggle("role", false);
navigate({ navigate({
to: "/organization/$organizationId/roles/roleId", to: "/organization/$organizationId/roles/$roleId",
params: { params: {
organizationId: orgId, organizationId: orgId,
roleId: newRole.id roleId: newRole.id
@@ -1,12 +1,12 @@
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { faChevronLeft, faEllipsis } from '@fortawesome/free-solid-svg-icons' import { faChevronLeft, faEllipsis } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { createFileRoute, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from 'tailwind-merge' import { twMerge } from "tailwind-merge";
import { createNotification } from '@app/components/notifications' import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from '@app/components/permissions' import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
DeleteActionModal, DeleteActionModal,
@@ -14,79 +14,70 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
Tooltip, Tooltip
} from '@app/components/v2' } from "@app/components/v2";
import { import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
OrgPermissionActions, import { withPermission } from "@app/hoc";
OrgPermissionSubjects, import { useDeleteOrgRole, useGetOrgRole } from "@app/hooks/api";
useOrganization, import { usePopUp } from "@app/hooks/usePopUp";
} from '@app/context' import { TabSections } from "@app/types/org";
import { withPermission } from '@app/hoc'
import { useDeleteOrgRole, useGetOrgRole } from '@app/hooks/api'
import { usePopUp } from '@app/hooks/usePopUp'
import { TabSections } from '@app/types/org'
import { import { RoleDetailsSection, RoleModal, RolePermissionsSection } from "./-components";
RoleDetailsSection,
RoleModal,
RolePermissionsSection,
} from './-components'
export const RolePage = withPermission( export const RolePage = withPermission(
() => { () => {
const navigate = useNavigate() const navigate = useNavigate();
const params = useParams({ const params = useParams({
from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId', from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/"
}) });
const roleId = params.roleId as string const roleId = params.roleId as string;
const { currentOrg } = useOrganization() const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || '' const orgId = currentOrg?.id || "";
const { data } = useGetOrgRole(orgId, roleId) const { data } = useGetOrgRole(orgId, roleId);
const { mutateAsync: deleteOrgRole } = useDeleteOrgRole() const { mutateAsync: deleteOrgRole } = useDeleteOrgRole();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
usePopUp(['role', 'deleteOrgRole'] as const) "role",
"deleteOrgRole"
] as const);
const onDeleteOrgRoleSubmit = async () => { const onDeleteOrgRoleSubmit = async () => {
try { try {
if (!orgId || !roleId) return if (!orgId || !roleId) return;
await deleteOrgRole({ await deleteOrgRole({
orgId, orgId,
id: roleId, id: roleId
}) });
createNotification({ createNotification({
text: 'Successfully deleted organization role', text: "Successfully deleted organization role",
type: 'success', type: "success"
}) });
handlePopUpClose('deleteOrgRole') handlePopUpClose("deleteOrgRole");
navigate({ navigate({
to: '/organization/$organizationId/members' as const, to: "/organization/$organizationId/members" as const,
params: { params: {
organizationId: currentOrg.id, organizationId: currentOrg.id
}, },
search: { search: {
selectedTab: TabSections.Roles, selectedTab: TabSections.Roles
}, }
}) });
} catch (err) { } catch (err) {
console.error(err) console.error(err);
const error = err as any const error = err as any;
const text = const text = error?.response?.data?.message ?? "Failed to delete organization role";
error?.response?.data?.message ?? 'Failed to delete organization role'
createNotification({ createNotification({
text, text,
type: 'error', type: "error"
}) });
} }
} };
const isCustomRole = !['admin', 'member', 'no-access'].includes( const isCustomRole = !["admin", "member", "no-access"].includes(data?.slug ?? "");
data?.slug ?? '',
)
return ( return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
@@ -98,14 +89,14 @@ export const RolePage = withPermission(
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />} leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => { onClick={() => {
navigate({ navigate({
to: '/organization/$organizationId/members' as const, to: "/organization/$organizationId/members" as const,
params: { params: {
organizationId: currentOrg.id, organizationId: currentOrg.id
}, },
search: { search: {
selectedTab: TabSections.Roles, selectedTab: TabSections.Roles
}, }
}) });
}} }}
className="mb-4" className="mb-4"
> >
@@ -123,20 +114,16 @@ export const RolePage = withPermission(
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan <OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Role}>
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Role}
>
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
!isAllowed && !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
'pointer-events-none cursor-not-allowed opacity-50',
)} )}
onClick={async () => { onClick={async () => {
handlePopUpOpen('role', { handlePopUpOpen("role", {
roleId, roleId
}) });
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -152,11 +139,11 @@ export const RolePage = withPermission(
<DropdownMenuItem <DropdownMenuItem
className={twMerge( className={twMerge(
isAllowed isAllowed
? 'hover:!bg-red-500 hover:!text-white' ? "hover:!bg-red-500 hover:!text-white"
: 'pointer-events-none cursor-not-allowed opacity-50', : "pointer-events-none cursor-not-allowed opacity-50"
)} )}
onClick={async () => { onClick={async () => {
handlePopUpOpen('deleteOrgRole') handlePopUpOpen("deleteOrgRole");
}} }}
disabled={!isAllowed} disabled={!isAllowed}
> >
@@ -170,10 +157,7 @@ export const RolePage = withPermission(
</div> </div>
<div className="flex"> <div className="flex">
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<RoleDetailsSection <RoleDetailsSection roleId={roleId} handlePopUpOpen={handlePopUpOpen} />
roleId={roleId}
handlePopUpOpen={handlePopUpOpen}
/>
</div> </div>
<RolePermissionsSection roleId={roleId} /> <RolePermissionsSection roleId={roleId} />
</div> </div>
@@ -182,34 +166,32 @@ export const RolePage = withPermission(
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteOrgRole.isOpen} isOpen={popUp.deleteOrgRole.isOpen}
title={`Are you sure want to delete the organization role ${data?.name ?? ''}?`} title={`Are you sure want to delete the organization role ${data?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle('deleteOrgRole', isOpen)} onChange={(isOpen) => handlePopUpToggle("deleteOrgRole", isOpen)}
deleteKey="confirm" deleteKey="confirm"
onDeleteApproved={() => onDeleteOrgRoleSubmit()} onDeleteApproved={() => onDeleteOrgRoleSubmit()}
/> />
</div> </div>
) );
}, },
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Role }, { action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Role }
) );
const OrgRoleRoute = () => { const OrgRoleRoute = () => {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<> <>
<Helmet> <Helmet>
<title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
{t('common.head-title', { title: t('settings.org.title') })}
</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<RolePage /> <RolePage />
</> </>
) );
} };
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/roles/$roleId/"
)({ )({
component: OrgRoleRoute, component: OrgRoleRoute
}) });
@@ -1,9 +1,9 @@
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex // REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
import { ReactNode, useMemo, useState } from 'react' import { ReactNode, useMemo, useState } from "react";
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { faFolderOpen, faStar } from '@fortawesome/free-regular-svg-icons' import { faFolderOpen, faStar } from "@fortawesome/free-regular-svg-icons";
import { import {
faArrowDownAZ, faArrowDownAZ,
faArrowRight, faArrowRight,
@@ -14,14 +14,14 @@ import {
faMagnifyingGlass, faMagnifyingGlass,
faPlus, faPlus,
faSearch, faSearch,
faStar as faSolidStar, faStar as faSolidStar
} from '@fortawesome/free-solid-svg-icons' } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, useNavigate } from '@tanstack/react-router' import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { createNotification } from '@app/components/notifications' import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from '@app/components/permissions' import { OrgPermissionCan } from "@app/components/permissions";
import { NewProjectModal } from '@app/components/projects' import { NewProjectModal } from "@app/components/projects";
import { import {
Button, Button,
IconButton, IconButton,
@@ -29,89 +29,86 @@ import {
Pagination, Pagination,
Skeleton, Skeleton,
Tooltip, Tooltip,
UpgradePlanModal, UpgradePlanModal
} from '@app/components/v2' } from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrganization, useOrganization,
useSubscription, useSubscription
} from '@app/context' } from "@app/context";
import { getProjectHomePage } from '@app/helpers/project' import { getProjectHomePage } from "@app/helpers/project";
import { usePagination, useResetPageHelper } from '@app/hooks' import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetUserWorkspaces } from '@app/hooks/api' import { useGetUserWorkspaces } from "@app/hooks/api";
import { OrderByDirection } from '@app/hooks/api/generic/types' import { OrderByDirection } from "@app/hooks/api/generic/types";
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries"; // import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
import { useFetchServerStatus } from '@app/hooks/api/serverDetails' import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { Workspace } from '@app/hooks/api/types' import { Workspace } from "@app/hooks/api/types";
import { useUpdateUserProjectFavorites } from '@app/hooks/api/users/mutation' import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from '@app/hooks/api/users/queries' import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { ProjectType } from '@app/hooks/api/workspace/types' import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from '@app/hooks/usePopUp' import { usePopUp } from "@app/hooks/usePopUp";
enum ProjectsViewMode { enum ProjectsViewMode {
GRID = 'grid', GRID = "grid",
LIST = 'list', LIST = "list"
} }
enum ProjectOrderBy { enum ProjectOrderBy {
Name = 'name', Name = "name"
} }
const formatTitle = (type: ProjectType) => { const formatTitle = (type: ProjectType) => {
if (type === ProjectType.SecretManager) return 'Secret Management' if (type === ProjectType.SecretManager) return "Secret Management";
if (type === ProjectType.CertificateManager) return 'Cert Management' if (type === ProjectType.CertificateManager) return "Cert Management";
return 'Key Management' return "Key Management";
} };
const formatDescription = (type: ProjectType) => { const formatDescription = (type: ProjectType) => {
if (type === ProjectType.SecretManager) if (type === ProjectType.SecretManager)
return 'Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.' return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc.";
if (type === ProjectType.CertificateManager) if (type === ProjectType.CertificateManager)
return 'Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.' return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
return 'Centralize the management of keys for cryptographic operations, such as encryption and decryption.' return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
} };
type Props = { type Props = {
type: ProjectType type: ProjectType;
} };
// #TODO: Update all the workspaceIds // #TODO: Update all the workspaceIds
export const ProductOverview = ({ type }: Props) => { export const ProductOverview = ({ type }: Props) => {
const { t } = useTranslation() const { t } = useTranslation();
const navigate = useNavigate() const navigate = useNavigate();
const { data: workspaces, isLoading: isWorkspaceLoading } = const { data: workspaces, isLoading: isWorkspaceLoading } = useGetUserWorkspaces({ type });
useGetUserWorkspaces({ type }) const { currentOrg } = useOrganization();
const { currentOrg } = useOrganization() const orgWorkspaces = workspaces || [];
const orgWorkspaces = workspaces || []
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } = const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
useGetUserProjectFavorites(currentOrg?.id) useGetUserProjectFavorites(currentOrg?.id);
const { mutateAsync: updateUserProjectFavorites } = const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
useUpdateUserProjectFavorites()
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
'addNewWs', "addNewWs",
'upgradePlan', "upgradePlan"
] as const) ] as const);
const [searchFilter, setSearchFilter] = useState('') const [searchFilter, setSearchFilter] = useState("");
const { data: serverDetails } = useFetchServerStatus() const { data: serverDetails } = useFetchServerStatus();
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>( const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
(localStorage.getItem('projectsViewMode') as ProjectsViewMode) || (localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
ProjectsViewMode.GRID, );
)
const { subscription } = useSubscription() const { subscription } = useSubscription();
const isAddingProjectsAllowed = subscription?.workspaceLimit const isAddingProjectsAllowed = subscription?.workspaceLimit
? subscription.workspacesUsed < subscription.workspaceLimit ? subscription.workspacesUsed < subscription.workspaceLimit
: true : true;
const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0 const isWorkspaceEmpty = !isProjectViewLoading && orgWorkspaces?.length === 0;
const { const {
setPage, setPage,
@@ -121,76 +118,72 @@ export const ProductOverview = ({ type }: Props) => {
offset, offset,
limit, limit,
toggleOrderDirection, toggleOrderDirection,
orderDirection, orderDirection
} = usePagination(ProjectOrderBy.Name, { initPerPage: 24 }) } = usePagination(ProjectOrderBy.Name, { initPerPage: 24 });
const filteredWorkspaces = useMemo( const filteredWorkspaces = useMemo(
() => () =>
orgWorkspaces orgWorkspaces
.filter((ws) => .filter((ws) => ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()))
ws?.name?.toLowerCase().includes(searchFilter.toLowerCase()),
)
.sort((a, b) => .sort((a, b) =>
orderDirection === OrderByDirection.ASC orderDirection === OrderByDirection.ASC
? a.name.toLowerCase().localeCompare(b.name.toLowerCase()) ? a.name.toLowerCase().localeCompare(b.name.toLowerCase())
: b.name.toLowerCase().localeCompare(a.name.toLowerCase()), : b.name.toLowerCase().localeCompare(a.name.toLowerCase())
), ),
[searchFilter, orderDirection, orgWorkspaces], [searchFilter, orderDirection, orgWorkspaces]
) );
useResetPageHelper({ useResetPageHelper({
setPage, setPage,
offset, offset,
totalCount: filteredWorkspaces.length, totalCount: filteredWorkspaces.length
}) });
const { workspacesWithFaveProp } = useMemo(() => { const { workspacesWithFaveProp } = useMemo(() => {
const workspacesWithFav = filteredWorkspaces const workspacesWithFav = filteredWorkspaces
.map((w): Workspace & { isFavorite: boolean } => ({ .map((w): Workspace & { isFavorite: boolean } => ({
...w, ...w,
isFavorite: Boolean(projectFavorites?.includes(w.id)), isFavorite: Boolean(projectFavorites?.includes(w.id))
})) }))
.sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite)) .sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite))
.slice(offset, limit * page) .slice(offset, limit * page);
return { return {
workspacesWithFaveProp: workspacesWithFav, workspacesWithFaveProp: workspacesWithFav
} };
}, [filteredWorkspaces, projectFavorites]) }, [filteredWorkspaces, projectFavorites]);
const addProjectToFavorites = async (projectId: string) => { const addProjectToFavorites = async (projectId: string) => {
try { try {
if (currentOrg?.id) { if (currentOrg?.id) {
await updateUserProjectFavorites({ await updateUserProjectFavorites({
orgId: currentOrg?.id, orgId: currentOrg?.id,
projectFavorites: [...(projectFavorites || []), projectId], projectFavorites: [...(projectFavorites || []), projectId]
}) });
} }
} catch { } catch {
createNotification({ createNotification({
text: 'Failed to add project to favorites.', text: "Failed to add project to favorites.",
type: 'error', type: "error"
}) });
} }
} };
const removeProjectFromFavorites = async (projectId: string) => { const removeProjectFromFavorites = async (projectId: string) => {
try { try {
if (currentOrg?.id) { if (currentOrg?.id) {
await updateUserProjectFavorites({ await updateUserProjectFavorites({
orgId: currentOrg?.id, orgId: currentOrg?.id,
projectFavorites: [ projectFavorites: [...(projectFavorites || []).filter((entry) => entry !== projectId)]
...(projectFavorites || []).filter((entry) => entry !== projectId), });
],
})
} }
} catch { } catch {
createNotification({ createNotification({
text: 'Failed to remove project from favorites.', text: "Failed to remove project from favorites.",
type: 'error', type: "error"
}) });
} }
} };
const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => ( const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
@@ -198,24 +191,22 @@ export const ProductOverview = ({ type }: Props) => {
onClick={() => { onClick={() => {
// TODO(rbr): fix this // TODO(rbr): fix this
navigate({ navigate({
to: getProjectHomePage(workspace), to: getProjectHomePage(workspace)
}) });
localStorage.setItem('projectData.id', workspace.id) localStorage.setItem("projectData.id", workspace.id);
}} }}
key={workspace.id} key={workspace.id}
className="flex h-40 min-w-72 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4" className="flex h-40 min-w-72 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
> >
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<div className="mt-0 truncate text-lg text-mineshaft-100"> <div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
{workspace.name}
</div>
{isFavorite ? ( {isFavorite ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={faSolidStar} icon={faSolidStar}
className="text-sm text-yellow-600 hover:text-mineshaft-400" className="text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
removeProjectFromFavorites(workspace.id) removeProjectFromFavorites(workspace.id);
}} }}
/> />
) : ( ) : (
@@ -223,8 +214,8 @@ export const ProductOverview = ({ type }: Props) => {
icon={faStar} icon={faStar}
className="text-sm text-mineshaft-400 hover:text-mineshaft-300" className="text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
addProjectToFavorites(workspace.id) addProjectToFavorites(workspace.id);
}} }}
/> />
)} )}
@@ -233,10 +224,10 @@ export const ProductOverview = ({ type }: Props) => {
<div <div
className="mb-2.5 mt-1 grow text-sm text-mineshaft-300" className="mb-2.5 mt-1 grow text-sm text-mineshaft-300"
style={{ style={{
overflow: 'hidden', overflow: "hidden",
display: '-webkit-box', display: "-webkit-box",
WebkitBoxOrient: 'vertical', WebkitBoxOrient: "vertical",
WebkitLineClamp: 2, WebkitLineClamp: 2
}} }}
> >
{workspace.description} {workspace.description}
@@ -250,7 +241,7 @@ export const ProductOverview = ({ type }: Props) => {
)} )}
<button type="button"> <button type="button">
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 px-4 py-2 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200"> <div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 px-4 py-2 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
Explore{' '} Explore{" "}
<FontAwesomeIcon <FontAwesomeIcon
icon={faArrowRight} icon={faArrowRight}
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0" className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
@@ -259,31 +250,25 @@ export const ProductOverview = ({ type }: Props) => {
</button> </button>
</div> </div>
</div> </div>
) );
const renderProjectListItem = ( const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
workspace: Workspace,
isFavorite: boolean,
index: number,
) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events // eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div <div
onClick={() => { onClick={() => {
// TODO(rbr): fix this // TODO(rbr): fix this
navigate({ navigate({
to: getProjectHomePage(workspace), to: getProjectHomePage(workspace)
}) });
localStorage.setItem('projectData.id', workspace.id) localStorage.setItem("projectData.id", workspace.id);
}} }}
key={workspace.id} key={workspace.id}
className={`group grid h-14 min-w-72 cursor-pointer grid-cols-6 border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ className={`group grid h-14 min-w-72 cursor-pointer grid-cols-6 border-l border-r border-t border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
index === 0 && 'rounded-t-md' index === 0 && "rounded-t-md"
}`} }`}
> >
<div className="flex items-center sm:col-span-3 lg:col-span-4"> <div className="flex items-center sm:col-span-3 lg:col-span-4">
<div className="truncate text-sm text-mineshaft-100"> <div className="truncate text-sm text-mineshaft-100">{workspace.name}</div>
{workspace.name}
</div>
</div> </div>
<div className="flex items-center justify-end sm:col-span-3 lg:col-span-2"> <div className="flex items-center justify-end sm:col-span-3 lg:col-span-2">
<div className="text-center text-sm text-mineshaft-300"> <div className="text-center text-sm text-mineshaft-300">
@@ -294,8 +279,8 @@ export const ProductOverview = ({ type }: Props) => {
icon={faSolidStar} icon={faSolidStar}
className="ml-6 text-sm text-yellow-600 hover:text-mineshaft-400" className="ml-6 text-sm text-yellow-600 hover:text-mineshaft-400"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
removeProjectFromFavorites(workspace.id) removeProjectFromFavorites(workspace.id);
}} }}
/> />
) : ( ) : (
@@ -303,16 +288,16 @@ export const ProductOverview = ({ type }: Props) => {
icon={faStar} icon={faStar}
className="ml-6 text-sm text-mineshaft-400 hover:text-mineshaft-300" className="ml-6 text-sm text-mineshaft-400 hover:text-mineshaft-300"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
addProjectToFavorites(workspace.id) addProjectToFavorites(workspace.id);
}} }}
/> />
)} )}
</div> </div>
</div> </div>
) );
let projectsComponents: ReactNode let projectsComponents: ReactNode;
if (filteredWorkspaces.length || isProjectViewLoading) { if (filteredWorkspaces.length || isProjectViewLoading) {
switch (projectsViewMode) { switch (projectsViewMode) {
@@ -339,14 +324,14 @@ export const ProductOverview = ({ type }: Props) => {
{!isProjectViewLoading && ( {!isProjectViewLoading && (
<> <>
{workspacesWithFaveProp.map((workspace) => {workspacesWithFaveProp.map((workspace) =>
renderProjectGridItem(workspace, workspace.isFavorite), renderProjectGridItem(workspace, workspace.isFavorite)
)} )}
</> </>
)} )}
</div> </div>
) );
break break;
case ProjectsViewMode.LIST: case ProjectsViewMode.LIST:
default: default:
projectsComponents = ( projectsComponents = (
@@ -356,19 +341,19 @@ export const ProductOverview = ({ type }: Props) => {
<div <div
key={`workspace-cards-loading-${i + 1}`} key={`workspace-cards-loading-${i + 1}`}
className={`group flex h-12 min-w-72 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${ className={`group flex h-12 min-w-72 cursor-pointer flex-row items-center justify-between border border-mineshaft-600 bg-mineshaft-800 px-6 hover:bg-mineshaft-700 ${
i === 0 && 'rounded-t-md' i === 0 && "rounded-t-md"
} ${i === 2 && 'rounded-b-md border-b'}`} } ${i === 2 && "rounded-b-md border-b"}`}
> >
<Skeleton className="w-full bg-mineshaft-600" /> <Skeleton className="w-full bg-mineshaft-600" />
</div> </div>
))} ))}
{!isProjectViewLoading && {!isProjectViewLoading &&
workspacesWithFaveProp.map((workspace, ind) => workspacesWithFaveProp.map((workspace, ind) =>
renderProjectListItem(workspace, workspace.isFavorite, ind), renderProjectListItem(workspace, workspace.isFavorite, ind)
)} )}
</div> </div>
) );
break break;
} }
} else if (orgWorkspaces.length && searchFilter) { } else if (orgWorkspaces.length && searchFilter) {
projectsComponents = ( projectsComponents = (
@@ -377,19 +362,15 @@ export const ProductOverview = ({ type }: Props) => {
icon={faSearch} icon={faSearch}
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400" className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/> />
<div className="text-center font-light"> <div className="text-center font-light">No projects match search...</div>
No projects match search...
</div>
</div> </div>
) );
} }
return ( return (
<div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen"> <div className="mx-auto flex max-w-7xl flex-col justify-start bg-bunker-800 md:h-screen">
<Helmet> <Helmet>
<title> <title>{t("common.head-title", { title: t("settings.members.title") })}</title>
{t('common.head-title', { title: t('settings.members.title') })}
</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
{!serverDetails?.redisConfigured && ( {!serverDetails?.redisConfigured && (
@@ -400,8 +381,8 @@ export const ProductOverview = ({ type }: Props) => {
icon={faExclamationCircle} icon={faExclamationCircle}
className="mr-4 p-4 text-2xl text-mineshaft-50" className="mr-4 p-4 text-2xl text-mineshaft-50"
/> />
Attention: Updated versions of Infisical now require Redis for full Attention: Updated versions of Infisical now require Redis for full functionality. Learn
functionality. Learn how to configure it how to configure it
<a <a
href="https://infisical.com/docs/self-hosting/configuration/redis" href="https://infisical.com/docs/self-hosting/configuration/redis"
rel="noopener noreferrer" rel="noopener noreferrer"
@@ -417,9 +398,7 @@ export const ProductOverview = ({ type }: Props) => {
)} )}
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0"> <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0">
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">
<p className="mr-4 text-3xl font-semibold text-white"> <p className="mr-4 text-3xl font-semibold text-white">{formatTitle(type)}</p>
{formatTitle(type)}
</p>
</div> </div>
<div> <div>
<p className="mr-4 mt-2 text-gray-400">{formatDescription(type)}</p> <p className="mr-4 mt-2 text-gray-400">{formatDescription(type)}</p>
@@ -437,9 +416,7 @@ export const ProductOverview = ({ type }: Props) => {
<IconButton <IconButton
className="min-w-[2.4rem] border-none hover:bg-mineshaft-600" className="min-w-[2.4rem] border-none hover:bg-mineshaft-600"
ariaLabel={`Sort ${ ariaLabel={`Sort ${
orderDirection === OrderByDirection.ASC orderDirection === OrderByDirection.ASC ? "descending" : "ascending"
? 'descending'
: 'ascending'
}`} }`}
variant="plain" variant="plain"
size="xs" size="xs"
@@ -447,11 +424,7 @@ export const ProductOverview = ({ type }: Props) => {
onClick={toggleOrderDirection} onClick={toggleOrderDirection}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={ icon={orderDirection === OrderByDirection.ASC ? faArrowDownAZ : faArrowUpZA}
orderDirection === OrderByDirection.ASC
? faArrowDownAZ
: faArrowUpZA
}
/> />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -460,15 +433,13 @@ export const ProductOverview = ({ type }: Props) => {
<IconButton <IconButton
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
localStorage.setItem('projectsViewMode', ProjectsViewMode.GRID) localStorage.setItem("projectsViewMode", ProjectsViewMode.GRID);
setProjectsViewMode(ProjectsViewMode.GRID) setProjectsViewMode(ProjectsViewMode.GRID);
}} }}
ariaLabel="grid" ariaLabel="grid"
size="xs" size="xs"
className={`${ className={`${
projectsViewMode === ProjectsViewMode.GRID projectsViewMode === ProjectsViewMode.GRID ? "bg-mineshaft-500" : "bg-transparent"
? 'bg-mineshaft-500'
: 'bg-transparent'
} min-w-[2.4rem] border-none hover:bg-mineshaft-600`} } min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
> >
<FontAwesomeIcon icon={faBorderAll} /> <FontAwesomeIcon icon={faBorderAll} />
@@ -476,24 +447,19 @@ export const ProductOverview = ({ type }: Props) => {
<IconButton <IconButton
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
localStorage.setItem('projectsViewMode', ProjectsViewMode.LIST) localStorage.setItem("projectsViewMode", ProjectsViewMode.LIST);
setProjectsViewMode(ProjectsViewMode.LIST) setProjectsViewMode(ProjectsViewMode.LIST);
}} }}
ariaLabel="list" ariaLabel="list"
size="xs" size="xs"
className={`${ className={`${
projectsViewMode === ProjectsViewMode.LIST projectsViewMode === ProjectsViewMode.LIST ? "bg-mineshaft-500" : "bg-transparent"
? 'bg-mineshaft-500'
: 'bg-transparent'
} min-w-[2.4rem] border-none hover:bg-mineshaft-600`} } min-w-[2.4rem] border-none hover:bg-mineshaft-600`}
> >
<FontAwesomeIcon icon={faList} /> <FontAwesomeIcon icon={faList} />
</IconButton> </IconButton>
</div> </div>
<OrgPermissionCan <OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
I={OrgPermissionActions.Create}
an={OrgPermissionSubjects.Workspace}
>
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
isDisabled={!isAllowed} isDisabled={!isAllowed}
@@ -501,9 +467,9 @@ export const ProductOverview = ({ type }: Props) => {
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => { onClick={() => {
if (isAddingProjectsAllowed) { if (isAddingProjectsAllowed) {
handlePopUpOpen('addNewWs') handlePopUpOpen("addNewWs");
} else { } else {
handlePopUpOpen('upgradePlan') handlePopUpOpen("upgradePlan");
} }
}} }}
className="ml-2" className="ml-2"
@@ -518,8 +484,8 @@ export const ProductOverview = ({ type }: Props) => {
<Pagination <Pagination
className={ className={
projectsViewMode === ProjectsViewMode.GRID projectsViewMode === ProjectsViewMode.GRID
? 'col-span-full !justify-start border-transparent bg-transparent pl-2' ? "col-span-full !justify-start border-transparent bg-transparent pl-2"
: 'rounded-b-md border border-mineshaft-600' : "rounded-b-md border border-mineshaft-600"
} }
perPage={perPage} perPage={perPage}
perPageList={[12, 24, 48, 96]} perPageList={[12, 24, 48, 96]}
@@ -536,36 +502,34 @@ export const ProductOverview = ({ type }: Props) => {
className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400" className="mb-4 mt-2 w-full text-center text-5xl text-mineshaft-400"
/> />
<div className="text-center font-light"> <div className="text-center font-light">
You are not part of any projects in this organization yet. When You are not part of any projects in this organization yet. When you are, they will
you are, they will appear here. appear here.
</div> </div>
<div className="mt-0.5 text-center font-light"> <div className="mt-0.5 text-center font-light">
Create a new project, or ask other organization members to give Create a new project, or ask other organization members to give you necessary
you necessary permissions. permissions.
</div> </div>
</div> </div>
)} )}
</div> </div>
<NewProjectModal <NewProjectModal
isOpen={popUp.addNewWs.isOpen} isOpen={popUp.addNewWs.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('addNewWs', isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
projectType={type} projectType={type}
/> />
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle('upgradePlan', isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You have exceeded the number of projects allowed on the free plan." text="You have exceeded the number of projects allowed on the free plan."
/> />
</div> </div>
) );
} };
const SecretManagerOverviewPage = () => ( const SecretManagerOverviewPage = () => <ProductOverview type={ProjectType.SecretManager} />;
<ProductOverview type={ProjectType.SecretManager} />
)
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview', "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-manager/overview"
)({ )({
component: SecretManagerOverviewPage, component: SecretManagerOverviewPage
}) });
@@ -1,73 +1,68 @@
import { useEffect } from 'react' import { useEffect } from "react";
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { createFileRoute, useSearch } from '@tanstack/react-router' import { createFileRoute, useSearch } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { OrgPermissionCan } from '@app/components/permissions' import { OrgPermissionCan } from "@app/components/permissions";
import { Button, NoticeBanner } from '@app/components/v2' import { Button, NoticeBanner } from "@app/components/v2";
import { import {
OrgPermissionActions, OrgPermissionActions,
OrgPermissionSubjects, OrgPermissionSubjects,
useOrganization, useOrganization,
useServerConfig, useServerConfig
} from '@app/context' } from "@app/context";
import { withPermission } from '@app/hoc' import { withPermission } from "@app/hoc";
import { import {
useCreateNewInstallationSession, useCreateNewInstallationSession,
useGetSecretScanningInstallationStatus, useGetSecretScanningInstallationStatus,
useLinkGitAppInstallationWithOrg, useLinkGitAppInstallationWithOrg
} from '@app/hooks/api/secretScanning' } from "@app/hooks/api/secretScanning";
import { SecretScanningLogsTable } from './-components' import { SecretScanningLogsTable } from "./-components";
const SecretScanning = withPermission( const SecretScanning = withPermission(
() => { () => {
const queryParams = useSearch({ const queryParams = useSearch({
from: '/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning', from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/"
}) });
const { config } = useServerConfig() const { config } = useServerConfig();
const { currentOrg } = useOrganization() const { currentOrg } = useOrganization();
const organizationId = currentOrg.id const organizationId = currentOrg.id;
const { mutateAsync: linkGitAppInstallationWithOrganization } = const { mutateAsync: linkGitAppInstallationWithOrganization } =
useLinkGitAppInstallationWithOrg() useLinkGitAppInstallationWithOrg();
const { mutateAsync: createNewIntegrationSession } = const { mutateAsync: createNewIntegrationSession } = useCreateNewInstallationSession();
useCreateNewInstallationSession() const { data: installationStatus, isLoading: isSecretScanningInstatllationStatusLoading } =
const { useGetSecretScanningInstallationStatus(organizationId);
data: installationStatus,
isLoading: isSecretScanningInstatllationStatusLoading,
} = useGetSecretScanningInstallationStatus(organizationId)
const integrationEnabled = const integrationEnabled =
!isSecretScanningInstatllationStatusLoading && !isSecretScanningInstatllationStatusLoading && installationStatus?.appInstallationCompleted;
installationStatus?.appInstallationCompleted
useEffect(() => { useEffect(() => {
const linkInstallation = async () => { const linkInstallation = async () => {
if ( if (queryParams.state && queryParams.installation_id) {
typeof queryParams.state === 'string' &&
typeof queryParams.installation_id === 'string'
) {
try { try {
const isLinked = await linkGitAppInstallationWithOrganization({ const isLinked = await linkGitAppInstallationWithOrganization({
installationId: queryParams.installation_id as string, installationId: queryParams.installation_id as string,
sessionId: queryParams.state as string, sessionId: queryParams.state as string
}) });
if (isLinked) { if (isLinked) {
window.location.reload() window.location.reload();
} }
console.log('installation verification complete') console.log("installation verification complete");
} catch (e) { } catch (e) {
console.log('app installation is stale, start new session', e) console.log("app installation is stale, start new session", e);
} }
} }
} };
linkInstallation() linkInstallation();
}, [queryParams.state, queryParams.installation_id]) }, [queryParams.state, queryParams.installation_id]);
const generateNewIntegrationSession = async () => { const generateNewIntegrationSession = async () => {
const session = await createNewIntegrationSession({ organizationId }) const session = await createNewIntegrationSession({ organizationId });
window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}` window.location.href = `https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`;
} };
return ( return (
<div> <div>
@@ -78,26 +73,20 @@ const SecretScanning = withPermission(
</Helmet> </Helmet>
<div className="flex h-full w-full justify-center bg-bunker-800 text-white"> <div className="flex h-full w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl px-6"> <div className="w-full max-w-7xl px-6">
<div className="mt-6 text-3xl font-semibold text-gray-200"> <div className="mt-6 text-3xl font-semibold text-gray-200">Secret Scanning</div>
Secret Scanning
</div>
<div className="mb-6 text-lg text-mineshaft-300"> <div className="mb-6 text-lg text-mineshaft-300">
Automatically monitor your GitHub activity and prevent secret Automatically monitor your GitHub activity and prevent secret leaks
leaks
</div> </div>
{config.isSecretScanningDisabled && ( {config.isSecretScanningDisabled && (
<NoticeBanner <NoticeBanner title="Secret scanning is in maintenance" className="mb-4">
title="Secret scanning is in maintenance" We are working on improving the performance of secret scanning due to increased
className="mb-4" usage.
>
We are working on improving the performance of secret scanning
due to increased usage.
</NoticeBanner> </NoticeBanner>
)} )}
<div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6"> <div className="relative mb-6 flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6">
<div className="flex flex-col items-start"> <div className="flex flex-col items-start">
<div className="mb-1 flex flex-row"> <div className="mb-1 flex flex-row">
Secret Scanning Status:{' '} Secret Scanning Status:{" "}
{integrationEnabled ? ( {integrationEnabled ? (
<p className="ml-1.5 font-semibold text-green">Enabled</p> <p className="ml-1.5 font-semibold text-green">Enabled</p>
) : ( ) : (
@@ -107,8 +96,8 @@ const SecretScanning = withPermission(
<div> <div>
{integrationEnabled ? ( {integrationEnabled ? (
<p className="text-mineshaft-300"> <p className="text-mineshaft-300">
Your GitHub organization is connected to Infisical, and is Your GitHub organization is connected to Infisical, and is being continuously
being continuously monitored for secret leaks. monitored for secret leaks.
</p> </p>
) : ( ) : (
<p className="text-mineshaft-300"> <p className="text-mineshaft-300">
@@ -148,16 +137,22 @@ const SecretScanning = withPermission(
</div> </div>
</div> </div>
</div> </div>
) );
}, },
{ {
action: OrgPermissionActions.Read, action: OrgPermissionActions.Read,
subject: OrgPermissionSubjects.SecretScanning, subject: OrgPermissionSubjects.SecretScanning
}, }
) );
const SecretScanningQueryParams = z.object({
state: z.string().catch(""),
installation_id: z.string().catch("")
});
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/secret-scanning/"
)({ )({
component: SecretScanning, component: SecretScanning,
}) validateSearch: zodValidator(SecretScanningQueryParams)
});
@@ -1 +1 @@
export { ImportTab } from "./ImportTab"; export { ImportTab } from "./ImportTab";
@@ -3,6 +3,7 @@ import { Controller, useForm } from "react-hook-form";
import { faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -35,7 +36,6 @@ import {
useGetOrganizationGroups useGetOrganizationGroups
} from "@app/hooks/api"; } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { useNavigate } from "@tanstack/react-router";
const schema = z.object({ const schema = z.object({
ldapGroupCN: z.string().min(1, "LDAP Group CN is required"), ldapGroupCN: z.string().min(1, "LDAP Group CN is required"),
@@ -24,7 +24,7 @@ const tabs = [
]; ];
export const OrgTabGroup = () => { export const OrgTabGroup = () => {
const search = useSearch({ const search = useSearch({
from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/settings" from: "/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/"
}); });
const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key); const [selectedTab, setSelectedTab] = useState(search.selectedTab || tabs[0].key);
@@ -1,7 +1,6 @@
import { faSlack } from "@fortawesome/free-brands-svg-icons"; import { faSlack } from "@fortawesome/free-brands-svg-icons";
import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons"; import { faEllipsis, faGear, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import axios from "axios"; import axios from "axios";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -45,7 +44,6 @@ export const OrgWorkflowIntegrationTab = withPermission(
] as const); ] as const);
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const navigate = useNavigate();
const { data: workflowIntegrations, isLoading: isWorkflowIntegrationsLoading } = const { data: workflowIntegrations, isLoading: isWorkflowIntegrationsLoading } =
useGetWorkflowIntegrations(currentOrg?.id); useGetWorkflowIntegrations(currentOrg?.id);
@@ -1,6 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import axios from "axios"; import axios from "axios";
import { z } from "zod"; import { z } from "zod";
@@ -14,7 +15,6 @@ import {
useUpdateSlackIntegration useUpdateSlackIntegration
} from "@app/hooks/api"; } from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas"; import { slugSchema } from "@app/lib/schemas";
import { useNavigate } from "@tanstack/react-router";
type Props = { type Props = {
id?: string; id?: string;
@@ -1,33 +1,29 @@
import { Helmet } from 'react-helmet' import { Helmet } from "react-helmet";
import { useTranslation } from 'react-i18next' import { useTranslation } from "react-i18next";
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { OrgTabGroup } from './-components' import { OrgTabGroup } from "./-components";
const SettingsOrg = () => { const SettingsOrg = () => {
const { t } = useTranslation() const { t } = useTranslation();
return ( return (
<> <>
<Helmet> <Helmet>
<title> <title>{t("common.head-title", { title: t("settings.org.title") })}</title>
{t('common.head-title', { title: t('settings.org.title') })}
</title>
<link rel="icon" href="/infisical.ico" /> <link rel="icon" href="/infisical.ico" />
</Helmet> </Helmet>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
<div className="w-full max-w-4xl px-6"> <div className="w-full max-w-4xl px-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-3xl font-semibold text-gray-200"> <p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
{t('settings.org.title')}
</p>
</div> </div>
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white"> <div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
<div className="w-full max-w-4xl px-6"> <div className="w-full max-w-4xl px-6">
<div className="mb-4"> <div className="mb-4">
<p className="text-3xl font-semibold text-gray-200"> <p className="text-3xl font-semibold text-gray-200">{t("settings.org.title")}</p>
{t('settings.org.title')}
</p>
</div> </div>
<OrgTabGroup /> <OrgTabGroup />
</div> </div>
@@ -35,11 +31,16 @@ const SettingsOrg = () => {
</div> </div>
</div> </div>
</> </>
) );
} };
export const SettingsOrgQueryParams = z.object({
selectedTab: z.string().catch("")
});
export const Route = createFileRoute( export const Route = createFileRoute(
'/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/', "/_authenticate/_org_details/_org-layout/organization/$organizationId/settings/"
)({ )({
component: SettingsOrg, component: SettingsOrg,
}) validateSearch: zodValidator(SettingsOrgQueryParams)
});
@@ -1,10 +1,12 @@
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const Route = createFileRoute("/_authenticate/_org_details/_org-layout/organization/")({ export const Route = createFileRoute("/_authenticate/_org_details/_org-layout/organization/")({
beforeLoad: ({ context }) => { beforeLoad: ({ context }) => {
redirect({ redirect({
throw: true, throw: true,
to: "/organization/$organizationId/secret-manager", to: `/organization/$organizationId/${ProjectType.SecretManager}/overview` as const,
params: { params: {
organizationId: String(context.organizationId) organizationId: String(context.organizationId)
} }
@@ -1,10 +1,10 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createFileRoute } from "@tanstack/react-router";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { createFileRoute } from "@tanstack/react-router";
const NoneOrganization = () => { const NoneOrganization = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -2,6 +2,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries"; import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { setAuthToken } from "@app/hooks/api/reactQuery"; import { setAuthToken } from "@app/hooks/api/reactQuery";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const Route = createFileRoute("/_restrict_login_signup")({ export const Route = createFileRoute("/_restrict_login_signup")({
beforeLoad: async ({ context }) => { beforeLoad: async ({ context }) => {
@@ -20,7 +21,7 @@ export const Route = createFileRoute("/_restrict_login_signup")({
throw redirect({ to: "/login/select-organization" }); throw redirect({ to: "/login/select-organization" });
} }
throw redirect({ throw redirect({
to: "/organization/$organizationId/secret-manager", to: `/organization/$organizationId/${ProjectType.SecretManager}/overview` as const,
params: { params: {
organizationId: data.organizationId organizationId: data.organizationId
} }
@@ -24,7 +24,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s
} else { } else {
// user is not part of any non-auth enforced orgs // user is not part of any non-auth enforced orgs
localStorage.removeItem("orgData.id"); localStorage.removeItem("orgData.id");
navigate({ to: "/org/none" }); navigate({ to: "/organization/none" });
} }
}; };
@@ -2,9 +2,11 @@ import { useEffect } from "react";
import { createFileRoute, useSearch } from "@tanstack/react-router"; import { createFileRoute, useSearch } from "@tanstack/react-router";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { z } from "zod";
import { zodValidator } from "@tanstack/zod-adapter";
const LoginProviderSuccess = () => { const LoginProviderSuccess = () => {
const search = useSearch({ from: "/login/provider/success" }); const search = useSearch({ from: "/_restrict_login_signup/login/provider/success" });
useEffect(() => { useEffect(() => {
SecurityClient.setProviderAuthToken(search.token); SecurityClient.setProviderAuthToken(search.token);
@@ -14,6 +16,11 @@ const LoginProviderSuccess = () => {
return <div />; return <div />;
}; };
const LoginProviderSuccessQuerySchema = z.object({
token: z.string()
});
export const Route = createFileRoute("/_restrict_login_signup/login/provider/success")({ export const Route = createFileRoute("/_restrict_login_signup/login/provider/success")({
component: LoginProviderSuccess component: LoginProviderSuccess,
validateSearch: zodValidator(LoginProviderSuccessQuerySchema)
}); });
@@ -180,7 +180,7 @@ const SelectOrganizationPage = () => {
// Case: User has no organizations. // Case: User has no organizations.
// This can happen if the user was previously a member, but the organization was deleted or the user was removed. // This can happen if the user was previously a member, but the organization was deleted or the user was removed.
if (organizations.data.length === 0) { if (organizations.data.length === 0) {
navigate({ to: "/org/none" }); navigate({ to: "/organization/none" });
} else if (organizations.data.length === 1) { } else if (organizations.data.length === 1) {
if (callbackPort) { if (callbackPort) {
handleCliRedirect(); handleCliRedirect();
@@ -2,13 +2,15 @@ import { useEffect, useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createFileRoute, Link, useSearch } from "@tanstack/react-router"; import { createFileRoute, Link, useSearch } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
import { z } from "zod";
import { PasswordStep } from "../-components"; import { PasswordStep } from "../-components";
const LoginSSOPage = () => { const LoginSSOPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const search = useSearch({ from: "/login/sso" }); const search = useSearch({ from: "/_restrict_login_signup/login/sso/" });
const token = search.token as string; const token = search.token as string;
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -58,6 +60,11 @@ const LoginSSOPage = () => {
); );
}; };
const LoginSSOQueryParamsSchema = z.object({
token: z.string()
});
export const Route = createFileRoute("/_restrict_login_signup/login/sso/")({ export const Route = createFileRoute("/_restrict_login_signup/login/sso/")({
component: LoginSSOPage component: LoginSSOPage,
validateSearch: zodValidator(LoginSSOQueryParamsSchema)
}); });
@@ -4,7 +4,9 @@ import { FormEvent, useState } from "react";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router"; import { createFileRoute, useNavigate, useSearch } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import jsrp from "jsrp"; import jsrp from "jsrp";
import { z } from "zod";
import InputField from "@app/components/basic/InputField"; import InputField from "@app/components/basic/InputField";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
@@ -389,6 +391,12 @@ const PasswordResetPage = () => {
); );
}; };
const PasswordResetPageQueryParamsSchema = z.object({
token: z.string(),
to: z.string()
});
export const Route = createFileRoute("/_restrict_login_signup/password-reset")({ export const Route = createFileRoute("/_restrict_login_signup/password-reset")({
component: PasswordResetPage component: PasswordResetPage,
validateSearch: zodValidator(PasswordResetPageQueryParamsSchema)
}); });
@@ -6,6 +6,7 @@ import jsrp from "jsrp";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import { Mfa } from "@app/components/auth/Mfa";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
@@ -16,7 +17,6 @@ import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/aut
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import ProjectService from "@app/services/ProjectService"; import ProjectService from "@app/services/ProjectService";
import { Mfa } from "@app/components/auth/Mfa";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
@@ -2,7 +2,9 @@ import { useEffect, useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createFileRoute, useSearch } from "@tanstack/react-router"; import { createFileRoute, useSearch } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
import { z } from "zod";
import { BackupPDFStep } from "./-components/BackupPDFStep"; import { BackupPDFStep } from "./-components/BackupPDFStep";
import { EmailConfirmationStep } from "./-components/EmailConfirmationStep"; import { EmailConfirmationStep } from "./-components/EmailConfirmationStep";
@@ -10,7 +12,7 @@ import { UserInfoSSOStep } from "./-components/UserInfoSSOStep";
const SignupSSOPage = () => { const SignupSSOPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const search = useSearch({ from: "/signup/sso/" }); const search = useSearch({ from: "/_restrict_login_signup/signup/sso/" });
const token = search.token as string; const token = search.token as string;
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
@@ -85,6 +87,11 @@ const SignupSSOPage = () => {
); );
}; };
const SignupSSOPageQueryParamsSchema = z.object({
token: z.string()
});
export const Route = createFileRoute("/_restrict_login_signup/signup/sso/")({ export const Route = createFileRoute("/_restrict_login_signup/signup/sso/")({
component: SignupSSOPage component: SignupSSOPage,
validateSearch: zodValidator(SignupSSOPageQueryParamsSchema)
}); });
@@ -7,9 +7,11 @@ import { Helmet } from "react-helmet";
import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, Link, useNavigate, useSearch } from "@tanstack/react-router"; import { createFileRoute, Link, useNavigate, useSearch } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import jsrp from "jsrp"; import jsrp from "jsrp";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
import { z } from "zod";
import { Mfa } from "@app/components/auth/Mfa"; import { Mfa } from "@app/components/auth/Mfa";
import InputField from "@app/components/basic/InputField"; import InputField from "@app/components/basic/InputField";
@@ -386,7 +388,7 @@ const SignupInvitePage = () => {
setBackupKeyIssued setBackupKeyIssued
}); });
navigate({ navigate({
to: `/organization/$organizationId/${ProjectType.SecretManager}` as const, to: `/organization/$organizationId/${ProjectType.SecretManager}/overview` as const,
params: { params: {
organizationId organizationId
} }
@@ -427,6 +429,13 @@ const SignupInvitePage = () => {
); );
}; };
const SignupInvitePageQueryParamsSchema = z.object({
token: z.string(),
to: z.string(),
organization_id: z.string()
});
export const Route = createFileRoute("/_restrict_login_signup/signupinvite")({ export const Route = createFileRoute("/_restrict_login_signup/signupinvite")({
component: SignupInvitePage component: SignupInvitePage,
validateSearch: zodValidator(SignupInvitePageQueryParamsSchema)
}); });