Merge branch 'heads/main' into ENG-3630

This commit is contained in:
Daniel Hougaard
2025-09-16 17:30:00 +04:00
1050 changed files with 30205 additions and 8323 deletions
@@ -3,23 +3,23 @@ import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { OrganizationsTable } from "./components";
import { ServerAdminsTable } from "./components";
export const OrganizationResourcesPage = () => {
export const AccessManagementPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
<title>{t("common.head-title", { title: "Access Control" })}</title>
</Helmet>
<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">
<PageHeader
title="Organizations"
description="Manage all organizations within your Infisical instance."
title="Access Control"
description="Manage server admins within your Infisical instance."
/>
<OrganizationsTable />
<ServerAdminsTable />
</div>
</div>
</div>
@@ -0,0 +1,141 @@
import { useMemo, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl, Modal, ModalContent } from "@app/components/v2";
import { useDebounce } from "@app/hooks";
import { useAdminGetUsers, useAdminGrantServerAdminAccess } from "@app/hooks/api";
import { User } from "@app/hooks/api/users/types";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onClose: () => void;
};
const getUserLabel = (user: Pick<User, "email" | "firstName" | "lastName" | "username" | "id">) => {
const { firstName, lastName, username, email } = user;
const name = `${firstName ?? ""} ${lastName ?? ""}`.trim();
const userEmail = email || username;
if (!name) return userEmail;
return `${name}${userEmail ? ` (${userEmail})` : ""}`;
};
const AddServerAdminSchema = z.object({
user: z.object({
id: z.string(),
firstName: z.string().nullish(),
lastName: z.string().nullish(),
email: z.string().nullish(),
username: z.string()
})
});
type FormData = z.infer<typeof AddServerAdminSchema>;
const Content = ({ onClose }: ContentProps) => {
const grantAdmin = useAdminGrantServerAdminAccess();
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(AddServerAdminSchema)
});
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { data, isFetching } = useAdminGetUsers(
{
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
},
{
placeholderData: (prev) => prev
}
);
const users = useMemo(() => data?.pages.flat().filter((user) => !user.superAdmin) ?? [], [data]);
const onSubmit = async ({ user }: FormData) => {
try {
await grantAdmin.mutateAsync(user.id);
createNotification({
type: "success",
text: "Successfully granted server admin status"
});
onClose();
} catch {
createNotification({
text: "Failed to grant server admin status",
type: "error"
});
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="User">
<FilterableSelect
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
className="w-full"
placeholder="Search users..."
options={users}
getOptionLabel={(user) => getUserLabel(user)}
getOptionValue={(user) => user.id}
value={field.value}
onChange={field.onChange}
onInputChange={(value) => {
setSearchUserFilter(value);
if (!value) setDebouncedSearchTerm("");
}}
/>
</FormControl>
)}
control={control}
name="user"
/>
<div className="flex w-full gap-4 pt-4">
<Button
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema="secondary"
>
Grant
</Button>
<Button onClick={() => onClose()} variant="plain" colorSchema="secondary">
Cancel
</Button>
</div>
</form>
);
};
export const AddServerAdminModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
bodyClassName="overflow-visible"
title="Grant Server Admin"
subTitle="Grant server admin status to a user"
>
<Content onClose={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,476 @@
import { Dispatch, SetStateAction, useState } from "react";
import {
faEllipsisV,
faMagnifyingGlass,
faPlus,
faShieldHalved,
faTrash,
faUsers,
faUserXmark,
faWarning,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { InfiniteData } from "@tanstack/react-query";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
Checkbox,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks";
import {
useAdminBulkDeleteUsers,
useAdminDeleteUser,
useAdminGetUsers,
useRemoveUserServerAdminAccess
} from "@app/hooks/api";
import { User } from "@app/hooks/api/users/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { AddServerAdminModal } from "@app/pages/admin/AccessManagementPage/components/AddServerAdminModal";
const removeServerAdminUpgradePlanMessage = "Removing Server Admin permissions from user";
const ServerAdminsPanelTable = ({
handlePopUpOpen,
users: usersPages,
isPending,
searchUserFilter,
setSearchUserFilter,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
selectedUsers,
setSelectedUsers
}: {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["removeUser", "upgradePlan", "addServerAdmin", "removeServerAdmin"]
>,
data?: {
username: string;
id: string;
message?: string;
}
) => void;
isPending: boolean;
users: InfiniteData<User[], unknown> | undefined;
searchUserFilter: string;
setSearchUserFilter: (filter: string) => void;
selectedUsers: User[];
setSelectedUsers: Dispatch<SetStateAction<User[]>>;
isFetchingNextPage: boolean;
fetchNextPage: () => void;
hasNextPage: boolean;
}) => {
const { subscription } = useSubscription();
const users = usersPages?.pages.flat();
const isEmpty = !isPending && !users?.length;
const selectedUserIds = selectedUsers.map((user) => user.id);
const isPageSelected = users?.length
? users.every((user) => selectedUserIds.includes(user.id))
: false;
// eslint-disable-next-line no-nested-ternary
const isPageIndeterminate = isPageSelected
? false
: users?.length
? users?.some((user) => selectedUserIds.includes(user.id))
: false;
return (
<>
<div className="flex items-center gap-x-2">
<Input
value={searchUserFilter}
onChange={(e) => setSearchUserFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search admins..."
className="flex-1"
/>
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addServerAdmin")}
>
Add Admin
</Button>
</div>
<div className="mt-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-5">
<Checkbox
id="member-page-select"
isChecked={isPageSelected || isPageIndeterminate}
isIndeterminate={isPageIndeterminate}
onCheckedChange={() => {
if (isPageSelected) {
setSelectedUsers((prev) =>
prev.filter((u) => !users?.find((user) => user.id === u.id))
);
} else {
setSelectedUsers((prev) => [
...prev,
...(users?.filter((u) => !prev.find((user) => user.id === u.id)) ?? [])
]);
}
}}
/>
</Th>
<Th className="w-5/12">Name</Th>
<Th className="w-1/2">Username</Th>
<Th className="w-2/12" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="users" />}
{!isPending &&
users?.map((user) => {
const { username, email, firstName, lastName, id } = user;
const name = firstName || lastName ? `${firstName} ${lastName}` : null;
const isSelected = selectedUserIds.includes(id);
return (
<Tr key={`user-${id}`} className="w-full">
<Td>
<Checkbox
id={`select-user-${id}`}
isChecked={isSelected}
onClick={(e) => {
e.stopPropagation();
setSelectedUsers((prev) =>
isSelected ? prev.filter((u) => u.id !== id) : [...prev, user]
);
}}
/>
</Td>
<Td className="w-5/12 max-w-0">
<p className="truncate">
{name ?? <span className="text-mineshaft-400">Not Set</span>}
</p>
</Td>
<Td className="w-5/12 max-w-0">
<p className="truncate">{username || email}</p>
</Td>
<Td>
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("removeUser", { username, id });
}}
icon={<FontAwesomeIcon icon={faUserXmark} />}
>
Remove User
</DropdownMenuItem>
<DropdownMenuItem
icon={
<div className="relative">
<FontAwesomeIcon icon={faShieldHalved} />
<FontAwesomeIcon
className="absolute -bottom-[0.01rem] -right-1"
size="2xs"
icon={faXmark}
/>
</div>
}
onClick={(e) => {
e.stopPropagation();
if (!subscription?.instanceUserManagement) {
handlePopUpOpen("upgradePlan", {
username,
id,
message: removeServerAdminUpgradePlanMessage
});
return;
}
handlePopUpOpen("removeServerAdmin", { username, id });
}}
>
Remove Server Admin
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No users found" icon={faUsers} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of List"}
</Button>
)}
</div>
</>
);
};
export const ServerAdminsTable = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"removeUser",
"upgradePlan",
"addServerAdmin",
"removeServerAdmin",
"removeUsers"
] as const);
const {
user: { id: userId }
} = useUser();
const { mutateAsync: deleteUser } = useAdminDeleteUser();
const { mutateAsync: deleteUsers } = useAdminBulkDeleteUsers();
const { mutateAsync: removeAdminAccess } = useRemoveUserServerAdminAccess();
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const {
data: users,
isPending,
isFetchingNextPage,
hasNextPage,
fetchNextPage
} = useAdminGetUsers({
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: true
});
const handleRemoveUser = async () => {
const { id } = popUp?.removeUser?.data as { id: string; username: string };
try {
await deleteUser(id);
createNotification({
type: "success",
text: "Successfully deleted user"
});
} catch {
createNotification({
type: "error",
text: "Error deleting user"
});
}
handlePopUpClose("removeUser");
};
const handleRemoveServerAdminAccess = async () => {
const { id } = popUp?.removeServerAdmin?.data as { id: string; username: string };
try {
await removeAdminAccess(id);
createNotification({
type: "success",
text: "Successfully removed server admin access from user"
});
} catch {
createNotification({
type: "error",
text: "Error removing server admin access from user"
});
}
handlePopUpClose("removeServerAdmin");
};
const handleRemoveUsers = async () => {
try {
await deleteUsers(selectedUsers.map((user) => user.id));
createNotification({
text: "Successfully removed users",
type: "success"
});
setSelectedUsers([]);
handlePopUpClose("removeUsers");
} catch {
createNotification({
text: "Failed to remove users",
type: "error"
});
}
};
return (
<>
<div
className={twMerge(
"h-0 flex-shrink-0 overflow-hidden transition-all",
selectedUsers.length > 0 && "h-16"
)}
>
<div className="flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
<div className="mr-2 text-sm">{selectedUsers.length} Selected</div>
<button
type="button"
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
onClick={() => setSelectedUsers([])}
>
Unselect All
</button>
<Button
variant="outline_bg"
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
className="ml-2"
onClick={() => {
if (!selectedUsers?.length) return;
handlePopUpOpen("removeUsers");
}}
size="xs"
>
Delete
</Button>
</div>
</div>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<ServerAdminsPanelTable
handlePopUpOpen={handlePopUpOpen}
users={users}
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
searchUserFilter={searchUserFilter}
setSearchUserFilter={setSearchUserFilter}
isPending={isPending}
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
fetchNextPage={fetchNextPage}
/>
<DeleteActionModal
isOpen={popUp.removeUser.isOpen}
deleteKey="remove"
title={`Are you sure you want to delete User with username ${
(popUp?.removeUser?.data as { id: string; username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)}
onDeleteApproved={handleRemoveUser}
/>
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
title={`Are you sure you want to remove Server Admin permissions from ${
(popUp?.removeServerAdmin?.data as { id: string; username: string })?.username || ""
}?`}
subTitle=""
onChange={(isOpen) => handlePopUpToggle("removeServerAdmin", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleRemoveServerAdminAccess}
buttonText="Remove Access"
/>
<AddServerAdminModal
isOpen={popUp.addServerAdmin.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addServerAdmin", isOpen)}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={`${popUp?.upgradePlan?.data?.message} is only available on Infisical's Pro plan and above.`}
/>
<DeleteActionModal
isOpen={popUp.removeUsers.isOpen}
title="Are you sure you want to delete the following users?"
onChange={(isOpen) => handlePopUpToggle("removeUsers", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleRemoveUsers()}
buttonText="Remove"
>
<div className="mt-4 text-sm text-mineshaft-400">
The following users will be deleted:
</div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc">
{selectedUsers?.map((user) => {
const email = user.email ?? user.username;
return (
<li key={user.id}>
<div className="flex items-center">
<p>
{user.firstName || user.lastName ? (
<>
{`${`${user.firstName} ${user.lastName}`.trim()} `}(
<span className="break-all">{email}</span>)
</>
) : (
<span className="break-all">{email}</span>
)}{" "}
</p>
{userId === user.id && (
<Tooltip content="Are you sure you want to remove yourself from this instance?">
<div className="inline-block">
<Badge
variant="primary"
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Deleting Yourself</span>
</Badge>
</div>
</Tooltip>
)}
</div>
</li>
);
})}
</ul>
</div>
</DeleteActionModal>
</div>
</>
);
};
@@ -0,0 +1 @@
export * from "./ServerAdminsTable";
@@ -1,11 +1,11 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { UserIdentitiesResourcesPage } from "./UserIdentitiesResourcesPage";
import { AccessManagementPage } from "./AccessManagementPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/user-identities"
"/_authenticate/_inject-org-details/admin/_admin-layout/access-management"
)({
component: UserIdentitiesResourcesPage,
component: AccessManagementPage,
beforeLoad: async () => {
return {
breadcrumbs: [
@@ -14,9 +14,9 @@ export const Route = createFileRoute(
link: linkOptions({ to: "/admin" })
},
{
label: "User Identities",
label: "Access Control",
link: linkOptions({
to: "/admin/resources/user-identities"
to: "/admin/access-management"
})
}
]
@@ -2,11 +2,13 @@ import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { useGetServerConfig } from "@app/hooks/api/admin";
import { GeneralPageForm } from "./components";
import { GeneralPageForm, UsageReportSection } from "./components";
export const GeneralPage = () => {
const { t } = useTranslation();
const { data: serverConfig } = useGetServerConfig();
return (
<div className="h-full bg-bunker-800">
@@ -19,7 +21,10 @@ export const GeneralPage = () => {
title="General"
description="Manage general settings for your Infisical instance."
/>
<GeneralPageForm />
<div className="space-y-6">
<GeneralPageForm />
{serverConfig?.isOfflineUsageReportsEnabled && <UsageReportSection />}
</div>
</div>
</div>
</div>
@@ -103,7 +103,7 @@ export const GeneralPageForm = () => {
return (
<form
className="space-y-8 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
className="space-y-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onFormSubmit)}
>
<div className="flex flex-col justify-start">
@@ -0,0 +1,53 @@
import { faDownload, faFileAlt, faSpinner } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Button, Card, CardTitle } from "@app/components/v2";
import { downloadFile } from "@app/helpers/download";
import { useGenerateUsageReport } from "@app/hooks/api/admin/mutation";
export const UsageReportSection = () => {
const generateUsageReport = useGenerateUsageReport();
const handleGenerateReport = async () => {
try {
const response = await generateUsageReport.mutateAsync();
const { csvContent, filename } = response;
downloadFile(csvContent, filename, "text/csv");
createNotification({
text: `Usage report downloaded: "${filename}"`,
type: "success"
});
} catch (error) {
console.error("Failed to generate usage report:", error);
createNotification({
text: "Failed to generate usage report. Please try again.",
type: "error"
});
}
};
return (
<Card className="p-6">
<CardTitle className="mb-4 flex items-center gap-3">
<FontAwesomeIcon icon={faFileAlt} />
Offline Usage Reports
</CardTitle>
<div className="mb-4 text-sm text-gray-400">
Generate secure usage reports for offline license compliance and billing verification.
</div>
<Button
onClick={handleGenerateReport}
className="w-fit"
isLoading={generateUsageReport.isPending}
leftIcon={<FontAwesomeIcon icon={generateUsageReport.isPending ? faSpinner : faDownload} />}
>
{generateUsageReport.isPending ? "Generating..." : "Generate Report"}
</Button>
</Card>
);
};
@@ -1 +1,2 @@
export { GeneralPageForm } from "./GeneralPageForm";
export { UsageReportSection } from "./UsageReportSection";
@@ -1,27 +0,0 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { MachineIdentitiesTable } from "./components";
export const MachineIdentitiesResourcesPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<PageHeader
title="Machine Identities"
description="Manage all machine identities within your Infisical instance."
/>
<MachineIdentitiesTable />
</div>
</div>
</div>
);
};
@@ -1 +0,0 @@
export { MachineIdentitiesTable } from "./MachineIdentitiesTable";
@@ -1,25 +0,0 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { MachineIdentitiesResourcesPage } from "./MachineIdentitiesResourcesPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/machine-identities"
)({
component: MachineIdentitiesResourcesPage,
beforeLoad: async () => {
return {
breadcrumbs: [
{
label: "Admin",
link: linkOptions({ to: "/admin" })
},
{
label: "Machine Identities",
link: linkOptions({
to: "/admin/resources/machine-identities"
})
}
]
};
}
});
@@ -1,396 +0,0 @@
import { useState } from "react";
import {
faBuilding,
faCircleQuestion,
faEllipsis,
faMagnifyingGlass
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useDebounce, usePopUp } from "@app/hooks";
import {
useAdminDeleteOrganization,
useAdminDeleteOrganizationMembership,
useAdminDeleteUser,
useAdminGetOrganizations
} from "@app/hooks/api";
import { OrganizationWithProjects } from "@app/hooks/api/admin/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const ViewMembersModalContent = ({
popUp,
handlePopUpOpen
}: {
popUp: UsePopUpState<["viewMembers"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>,
data?: {
username?: string;
membershipId?: string;
userId?: string;
orgName?: string;
orgId?: string;
organization?: OrganizationWithProjects;
}
) => void;
}) => {
const organization = popUp.viewMembers?.data?.organization as OrganizationWithProjects;
return (
<div className="space-y-2">
{organization?.members?.map((member) => (
<div className="flex items-center justify-between gap-2 rounded-md bg-mineshaft-700 px-4 py-2">
<div>
<div className="flex items-center gap-2">
<p className="text-xs text-mineshaft-100">
<div>
{member.user.firstName ? (
<div>
{member.user.firstName} {member.user.lastName}
</div>
) : (
<p className="text-mineshaft-400">Not set</p>
)}
</div>
<div className="flex gap-2 opacity-80">
<div>{member.user.username || member.user.email}</div>
<Badge variant="primary">
<div className="flex items-center gap-1">
<span className="capitalize">{member.role.replace("-", " ")}</span>
{Boolean(member.roleId) && (
<Tooltip content="This member has a custom role assigned.">
<FontAwesomeIcon icon={faCircleQuestion} className="text-xs" />
</Tooltip>
)}
</div>
</Badge>
</div>
</p>
</div>
</div>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<FontAwesomeIcon
icon={faEllipsis}
className="cursor-pointer text-sm text-mineshaft-400 transition-all hover:text-primary-500"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={() =>
handlePopUpOpen("deleteOrganizationMembership", {
membershipId: member.membershipId,
orgId: organization.id,
username: member.user.username,
orgName: organization.name
})
}
>
Remove From Organization
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handlePopUpOpen("deleteUser", { userId: member.user.id })}
>
Delete User
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
))}
</div>
);
};
const ViewMembersModal = ({
isOpen,
onOpenChange,
popUp,
handlePopUpOpen
}: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
popUp: UsePopUpState<["viewMembers", "deleteOrganizationMembership", "deleteUser"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>
) => void;
}) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
onOpenAutoFocus={(event) => {
event.preventDefault();
}}
title="Organization Members"
subTitle="View the members of the organization."
>
<ViewMembersModalContent popUp={popUp} handlePopUpOpen={handlePopUpOpen} />
</ModalContent>
</Modal>
);
};
const OrganizationsPanelTable = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: {
popUp: UsePopUpState<
["deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser"]
>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["deleteOrganization", "viewMembers", "deleteOrganizationMembership", "deleteUser"]
>,
data?: {
orgName?: string;
orgId?: string;
message?: string;
organization?: OrganizationWithProjects;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["deleteOrganization", "viewMembers"]>,
isOpen?: boolean
) => void;
}) => {
const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500);
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
useAdminGetOrganizations({
limit: 20,
searchTerm: debouncedSearchTerm
});
const isEmpty = !isPending && !data?.pages?.[0].length;
return (
<>
<div className="flex gap-2">
<Input
value={searchOrganizationsFilter}
onChange={(e) => setSearchOrganizationsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organizations..."
className="flex-1"
/>
</div>
<div className="mt-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-5/12">Name</Th>
<Th className="w-5/12">Members</Th>
<Th className="w-5/12">Projects</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="organizations" />}
{!isPending &&
data?.pages?.map((orgs) =>
orgs.map((org) => {
return (
<Tr key={`org-${org.id}`} className="w-full">
<Td className="w-5/12">
{org.name ? (
org.name
) : (
<span className="text-mineshaft-400">Not set</span>
)}
</Td>
<Td className="w-5/12">
{org.members.length} {org.members.length === 1 ? "member" : "members"}
<Button
variant="outline_bg"
size="xs"
className="ml-2"
onClick={() => handlePopUpOpen("viewMembers", { organization: org })}
>
View Members
</Button>
</Td>
<Td className="w-5/12">
{org.projects.length} {org.projects.length === 1 ? "project" : "projects"}
</Td>
<Td>
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteOrganization", {
orgId: org.id,
orgName: org.name
});
}}
>
Delete Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})
)}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No organizations found" icon={faBuilding} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of list"}
</Button>
)}
</div>
<ViewMembersModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
isOpen={popUp.viewMembers.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("viewMembers", isOpen)}
/>
</>
);
};
export const OrganizationsTable = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteOrganization",
"deleteOrganizationMembership",
"deleteUser",
"viewMembers"
] as const);
const { mutateAsync: deleteOrganization } = useAdminDeleteOrganization();
const { mutateAsync: deleteOrganizationMembership } = useAdminDeleteOrganizationMembership();
const { mutateAsync: deleteUser } = useAdminDeleteUser();
const handleDeleteOrganization = async () => {
const { orgId } = popUp?.deleteOrganization?.data as { orgId: string };
await deleteOrganization(orgId);
createNotification({
type: "success",
text: "Successfully deleted organization"
});
handlePopUpClose("deleteOrganization");
};
const handleDeleteOrganizationMembership = async () => {
const { orgId, membershipId } = popUp?.deleteOrganizationMembership?.data as {
orgId: string;
membershipId: string;
};
if (!orgId || !membershipId) {
return;
}
await deleteOrganizationMembership({ organizationId: orgId, membershipId });
createNotification({
type: "success",
text: "Successfully removed user from organization"
});
handlePopUpClose("viewMembers");
handlePopUpClose("deleteOrganizationMembership");
};
const handleDeleteUser = async () => {
const { userId } = popUp?.deleteUser?.data as { userId: string };
if (!userId) {
return;
}
await deleteUser(userId);
createNotification({
type: "success",
text: "Successfully deleted user"
});
handlePopUpClose("viewMembers");
handlePopUpClose("deleteUser");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<OrganizationsPanelTable
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteOrganization.isOpen}
deleteKey="delete"
title={`Are you sure you want to delete organization ${
(popUp?.deleteOrganization?.data as { orgName: string })?.orgName || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrganization", isOpen)}
onDeleteApproved={handleDeleteOrganization}
/>
<DeleteActionModal
isOpen={popUp.deleteOrganizationMembership.isOpen}
deleteKey="delete"
title={`Are you sure you want to remove ${
(popUp?.deleteOrganizationMembership?.data as { username: string })?.username || ""
} from organization ${
(popUp?.deleteOrganizationMembership?.data as { orgName: string })?.orgName || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrganizationMembership", isOpen)}
onDeleteApproved={handleDeleteOrganizationMembership}
/>
<DeleteActionModal
isOpen={popUp.deleteUser.isOpen}
deleteKey="delete"
title={`Are you sure you want to delete user ${
(popUp?.deleteUser?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteUser", isOpen)}
onDeleteApproved={handleDeleteUser}
/>
</div>
);
};
@@ -1 +0,0 @@
export { OrganizationsTable } from "./OrganizationsTable";
@@ -0,0 +1,42 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { MachineIdentitiesTable, OrganizationsTable, UserIdentitiesTable } from "./components";
export const ResourceOverviewPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Resource Overview" })}</title>
</Helmet>
<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">
<PageHeader
title="Resource Overview"
description="Manage resources within your Infisical instance."
/>
<Tabs defaultValue="tab-organizations">
<TabList>
<Tab value="tab-organizations">Organizations</Tab>
<Tab value="tab-users">Users</Tab>
<Tab value="tab-identities">Identities</Tab>
</TabList>
<TabPanel value="tab-organizations">
<OrganizationsTable />
</TabPanel>
<TabPanel value="tab-users">
<UserIdentitiesTable />
</TabPanel>
<TabPanel value="tab-identities">
<MachineIdentitiesTable />
</TabPanel>
</Tabs>
</div>
</div>
</div>
);
};
@@ -0,0 +1,203 @@
import { useMemo, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { CreatableSelect } from "@app/components/v2/CreatableSelect";
import { useDebounce } from "@app/hooks";
import { useAdminGetUsers, useServerAdminCreateOrganization } from "@app/hooks/api";
import { User } from "@app/hooks/api/users/types";
import { GenericResourceNameSchema } from "@app/lib/schemas";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
};
type ContentProps = {
onClose: () => void;
};
type Invitee = Pick<User, "email" | "firstName" | "lastName" | "username" | "id">;
type NewOption = { label: string; value: string };
const getUserLabel = (user: Invitee | NewOption) => {
if (Object.prototype.hasOwnProperty.call(user, "value")) {
return (user as NewOption).label;
}
const { firstName, lastName, username, email } = user as Invitee;
const name = `${firstName ?? ""} ${lastName ?? ""}`.trim();
const userEmail = email || username;
if (!name) return userEmail;
return `${name}${userEmail ? ` (${userEmail})` : ""}`;
};
const AddOrgSchema = z.object({
name: GenericResourceNameSchema.nonempty("Organization name required"),
invitees: z
.object({
id: z.string(),
firstName: z.string().nullish(),
lastName: z.string().nullish(),
email: z.string().nullish(),
username: z.string().nullish()
})
.array()
.min(1, "At least one admin is required")
});
type FormData = z.infer<typeof AddOrgSchema>;
const Content = ({ onClose }: ContentProps) => {
const createOrg = useServerAdminCreateOrganization();
const {
handleSubmit,
control,
formState: { isSubmitting }
} = useForm({
defaultValues: {
name: "",
invitees: []
},
resolver: zodResolver(AddOrgSchema)
});
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce(searchUserFilter, 500);
const { data, isFetching } = useAdminGetUsers(
{
limit: 20,
searchTerm: debouncedSearchTerm,
adminsOnly: false
},
{
placeholderData: (prev) => prev
}
);
const users = useMemo(() => data?.pages.flat() ?? [], [data]);
const onSubmit = async ({ name, invitees }: FormData) => {
try {
await createOrg.mutateAsync({
name,
inviteAdminEmails: invitees
.filter((user) => Boolean(user.email))
.map((user) => user.email) as string[]
});
createNotification({
type: "success",
text: "Successfully created organization"
});
onClose();
} catch {
createNotification({
text: "Failed to create organization",
type: "error"
});
}
};
const { append } = useFieldArray<FormData>({ control, name: "invitees" });
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Name">
<Input autoFocus value={value} onChange={onChange} placeholder="My Organization" />
</FormControl>
)}
control={control}
name="name"
/>
<Controller
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Assign Organization Admins"
>
<CreatableSelect
/* eslint-disable-next-line react/no-unstable-nested-components */
noOptionsMessage={() => (
<p>Invite new users to this organization by typing out their email address.</p>
)}
onCreateOption={(inputValue) =>
append({ id: `${inputValue}_${Math.random()}`, email: inputValue })
}
formatCreateLabel={(inputValue) => `Invite "${inputValue}"`}
isValidNewOption={(input) =>
Boolean(input) &&
z.string().email().safeParse(input).success &&
!users
?.flatMap((user) => {
const emails: string[] = [];
if (user.email) {
emails.push(user.email);
}
if (user.username) {
emails.push(user.username);
}
return emails;
})
.includes(input)
}
isLoading={searchUserFilter !== debouncedSearchTerm || isFetching}
className="w-full"
placeholder="Search users or invite new ones..."
isMulti
name="members"
options={users}
getOptionLabel={(user) => getUserLabel(user)}
getOptionValue={(user) => user.id}
value={field.value}
onChange={field.onChange}
onInputChange={(value) => {
setSearchUserFilter(value);
if (!value) setDebouncedSearchTerm("");
}}
/>
</FormControl>
)}
control={control}
name="invitees"
/>
<div className="flex w-full gap-4 pt-4">
<Button
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
colorSchema="secondary"
>
Add Organization
</Button>
<Button onClick={() => onClose()} variant="plain" colorSchema="secondary">
Cancel
</Button>
</div>
</form>
);
};
export const AddOrganizationModal = ({ isOpen, onOpenChange }: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent bodyClassName="overflow-visible" title="Add Organization">
<Content onClose={() => onOpenChange(false)} />
</ModalContent>
</Modal>
);
};
@@ -2,8 +2,8 @@ import { useState } from "react";
import {
faEllipsisV,
faMagnifyingGlass,
faServer,
faShieldHalved,
faWrench,
faXmark
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -136,7 +136,7 @@ const IdentityPanelTable = ({
)}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faServer} />}
{!isPending && isEmpty && <EmptyState title="No identities found" icon={faWrench} />}
</TableContainer>
{!isEmpty && (
<Button
@@ -183,6 +183,12 @@ export const MachineIdentitiesTable = () => {
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-mineshaft-100">Machine Identities</p>
<p className="text-sm text-bunker-300">Manage machine identities across your instance.</p>
</div>
</div>
<IdentityPanelTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal
isOpen={popUp.removeServerAdmin.isOpen}
@@ -0,0 +1,752 @@
import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faBuilding,
faCircleQuestion,
faEllipsisV,
faEnvelope,
faEye,
faMagnifyingGlass,
faPlus,
faTrash,
faUserCheck,
faUserMinus,
faUserPlus,
faUsers,
faUserXmark,
faWarning
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import {
Badge,
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Modal,
ModalContent,
Pagination,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useUser } from "@app/context";
import { OrgMembershipRole } from "@app/helpers/roles";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useAdminDeleteOrganization,
useAdminDeleteOrganizationMembership,
useAdminDeleteUser,
useAdminGetOrganizations,
useServerAdminAccessOrg,
useServerAdminResendOrgInvite
} from "@app/hooks/api";
import { OrganizationWithProjects } from "@app/hooks/api/admin/types";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { OrgMembershipStatus } from "@app/hooks/api/organization/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { AddOrganizationModal } from "@app/pages/admin/ResourceOverviewPage/components/AddOrganizationModal";
enum MembersOrderBy {
Name = "firstName",
Email = "email"
}
const ORG_MEMBERS_TABLE_LIMIT = 15;
const ViewMembersModalContent = ({
popUp,
handlePopUpOpen
}: {
popUp: UsePopUpState<["viewMembers"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>,
data?: {
username?: string;
membershipId?: string;
userId?: string;
orgName?: string;
orgId?: string;
organization?: OrganizationWithProjects;
}
) => void;
}) => {
const organization = popUp.viewMembers?.data?.organization as OrganizationWithProjects;
const [resendInviteId, setResendInviteId] = useState<string | null>(null);
const members = organization?.members ?? [];
const {
search,
setSearch,
setPage,
page,
perPage,
setPerPage,
offset,
orderDirection,
orderBy,
setOrderBy,
setOrderDirection,
toggleOrderDirection
} = usePagination<MembersOrderBy>(MembersOrderBy.Name, {
initPerPage: ORG_MEMBERS_TABLE_LIMIT
});
const filteredMembers = useMemo(
() =>
members
?.filter(
({ user: u }) =>
u?.firstName?.toLowerCase().includes(search.toLowerCase()) ||
u?.lastName?.toLowerCase().includes(search.toLowerCase()) ||
u?.username?.toLowerCase().includes(search.toLowerCase()) ||
u?.email?.toLowerCase().includes(search.toLowerCase())
)
.sort((a, b) => {
const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a];
let valueOne: string | null;
let valueTwo: string | null;
switch (orderBy) {
case MembersOrderBy.Email:
valueOne = memberOne.user.email || memberOne.user.username;
valueTwo = memberTwo.user.email || memberTwo.user.username;
break;
case MembersOrderBy.Name:
default:
valueOne = memberOne.user.firstName ?? memberOne.user.lastName;
valueTwo = memberTwo.user.firstName ?? memberTwo.user.lastName;
}
if (!valueOne) return 1;
if (!valueTwo) return -1;
return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase());
}),
[members, search, orderBy, orderDirection]
);
const handleSort = (column: MembersOrderBy) => {
if (column === orderBy) {
toggleOrderDirection();
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
useResetPageHelper({
totalCount: filteredMembers.length,
offset,
setPage
});
const resendOrgInvite = useServerAdminResendOrgInvite();
const onResendInvite = async (membershipId: string) => {
setResendInviteId(membershipId);
try {
await resendOrgInvite.mutateAsync({
membershipId,
organizationId: organization.id
});
createNotification({
text: "Successfully resent org invitation",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to resend org invitation",
type: "error"
});
} finally {
setResendInviteId(null);
}
};
return (
<>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search members..."
/>
<TableContainer
className={twMerge(
"mt-4 flex flex-1 flex-col border border-mineshaft-500 bg-mineshaft-700",
Boolean(filteredMembers.length) && "rounded-b-none"
)}
>
<Table className="overflow-y-auto bg-mineshaft-700">
<THead className="sticky top-0 z-50">
<Tr>
<Th className="w-1/3 border-none bg-mineshaft-700 p-0">
<div className="flex h-12 w-full items-center border-b-2 border-mineshaft-500 px-3 py-2.5">
Name
<IconButton
variant="plain"
className={`ml-2 ${orderBy === MembersOrderBy.Name ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(MembersOrderBy.Name)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === MembersOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-1/3 border-none bg-mineshaft-700 p-0">
<div className="flex h-12 w-full items-center border-b-2 border-mineshaft-500 px-3 py-2.5">
Email
<IconButton
variant="plain"
className={`ml-2 ${orderBy === MembersOrderBy.Email ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(MembersOrderBy.Email)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC && orderBy === MembersOrderBy.Email
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-1/4 border-none bg-mineshaft-700 p-0">
<div className="flex h-12 w-full items-center border-b-2 border-mineshaft-500 px-3 py-2.5">
Role
</div>
</Th>
<Th className="w-5 border-none bg-mineshaft-700 p-0">
<div className="flex h-12 w-full items-center border-b-2 border-mineshaft-500 px-3 py-2.5" />
</Th>
</Tr>
</THead>
<TBody>
{filteredMembers.slice(offset, perPage * page).map((member) => {
const { username, email, firstName, lastName, id } = member.user;
const { role, status } = member;
const name = firstName || lastName ? `${firstName} ${lastName}` : null;
return (
<Tr key={`user-${id}`} className="w-full">
<Td className="max-w-0">
<div className="flex items-center">
<p className="truncate">
{name ?? <span className="text-mineshaft-400">Not Set</span>}
</p>
</div>
</Td>
<Td className="max-w-0">
<div className="flex items-center">
<p className="truncate">{username || email}</p>
{role === OrgMembershipRole.Admin &&
status !== OrgMembershipStatus.Accepted && (
<Button
isDisabled={resendOrgInvite.isPending}
className="ml-2 h-7 border-mineshaft-600 bg-mineshaft-800/50 font-normal"
colorSchema="primary"
variant="outline_bg"
size="xs"
isLoading={
resendOrgInvite.isPending && resendInviteId === member.membershipId
}
leftIcon={<FontAwesomeIcon icon={faEnvelope} />}
onClick={(e) => {
onResendInvite(member.membershipId);
e.stopPropagation();
}}
>
Resend Invite
</Button>
)}
</div>
</Td>
<Td className="max-w-0">
<Badge className="flex w-fit max-w-full items-center gap-x-1 whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
<p className="truncate capitalize">{member.role.replace("-", " ")}</p>
{Boolean(member.roleId) && (
<Tooltip content="This member has a custom role assigned.">
<FontAwesomeIcon icon={faCircleQuestion} className="w-3" />
</Tooltip>
)}
</Badge>
</Td>
<Td>
<div className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserMinus} />}
onClick={() =>
handlePopUpOpen("deleteOrganizationMembership", {
membershipId: member.membershipId,
orgId: organization.id,
username: member.user.username,
orgName: organization.name
})
}
>
Remove From Organization
</DropdownMenuItem>
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faUserXmark} />}
onClick={() =>
handlePopUpOpen("deleteUser", { userId: member.user.id })
}
>
Delete User
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!filteredMembers.length && (
<EmptyState
className="my-auto bg-mineshaft-700"
title={
members.length
? "No organization members match search..."
: "No organization members found"
}
icon={faUsers}
/>
)}
</TableContainer>
{Boolean(filteredMembers.length) && (
<Pagination
className="rounded-b-md border border-t-0 bg-mineshaft-700"
count={filteredMembers.length}
page={page}
perPage={perPage}
onChangePage={setPage}
onChangePerPage={setPerPage}
perPageList={[ORG_MEMBERS_TABLE_LIMIT]}
/>
)}
</>
);
};
const ViewMembersModal = ({
isOpen,
onOpenChange,
popUp,
handlePopUpOpen
}: {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
popUp: UsePopUpState<["viewMembers", "deleteOrganizationMembership", "deleteUser"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteOrganizationMembership", "deleteUser"]>
) => void;
}) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
onOpenAutoFocus={(event) => {
event.preventDefault();
}}
title="Organization Members"
subTitle="View the members of the organization."
className="h-full max-w-4xl"
bodyClassName="flex flex-col h-full"
>
<ViewMembersModalContent popUp={popUp} handlePopUpOpen={handlePopUpOpen} />
</ModalContent>
</Modal>
);
};
const OrganizationsPanelTable = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: {
popUp: UsePopUpState<
[
"deleteOrganization",
"viewMembers",
"deleteOrganizationMembership",
"deleteUser",
"createOrganization"
]
>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
[
"deleteOrganization",
"viewMembers",
"deleteOrganizationMembership",
"deleteUser",
"createOrganization"
]
>,
data?: {
orgName?: string;
orgId?: string;
message?: string;
organization?: OrganizationWithProjects;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["deleteOrganization", "viewMembers"]>,
isOpen?: boolean
) => void;
}) => {
const [searchOrganizationsFilter, setSearchOrganizationsFilter] = useState("");
const [debouncedSearchTerm] = useDebounce(searchOrganizationsFilter, 500);
const { user } = useUser();
const navigate = useNavigate();
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } =
useAdminGetOrganizations({
limit: 20,
searchTerm: debouncedSearchTerm
});
const isEmpty = !isPending && !data?.pages?.[0].length;
const { mutateAsync: accessOrganization } = useServerAdminAccessOrg();
const handleAccessOrg = async (orgId: string) => {
try {
await accessOrganization(orgId);
navigate({
to: "/login/select-organization",
search: {
org_id: orgId
}
});
createNotification({
text: "Successfully joined organization",
type: "success"
});
} catch {
createNotification({
text: "Failed to join organization",
type: "error"
});
}
};
return (
<>
<Input
value={searchOrganizationsFilter}
onChange={(e) => setSearchOrganizationsFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search organizations..."
className="flex-1"
/>
<div className="mt-4">
<TableContainer>
<Table>
<THead>
<Tr>
<Th className="w-1/2">Name</Th>
<Th className="w-1/3">Members</Th>
<Th className="w-1/3">Projects</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="organizations" />}
{!isPending &&
data?.pages?.map((orgs) =>
orgs.map((org) => {
const isMember = org.members.find((member) => member.user.id === user.id);
return (
<Tr key={`org-${org.id}`} className="w-full">
<Td className="w-1/2 max-w-0">
<div className="flex items-center gap-x-1.5">
{org.name ? (
<p className="truncate">{org.name}</p>
) : (
<span className="text-mineshaft-400">Not Set</span>
)}
</div>
</Td>
<Td className="w-1/3">
<button
type="button"
onClick={() => handlePopUpOpen("viewMembers", { organization: org })}
className="flex items-center hover:underline"
>
<Tooltip className="text-center" content="View Members">
<FontAwesomeIcon
icon={faEye}
className="mr-1.5 text-mineshaft-300"
size="sm"
/>
</Tooltip>
{org.members.length} {org.members.length === 1 ? "Member" : "Members"}
{!org.members.some(
(member) =>
member.role === OrgMembershipRole.Admin &&
member.status === OrgMembershipStatus.Accepted
) && (
<Tooltip content="No admins have accepted their invitations.">
<div className="ml-1.5">
<FontAwesomeIcon className="text-yellow" icon={faWarning} />
</div>
</Tooltip>
)}
</button>
</Td>
<Td className="w-1/3">
{org.projects.length} {org.projects.length === 1 ? "Project" : "Projects"}
</Td>
<Td>
<div className="flex justify-end gap-x-1">
{isMember && (
<Tooltip
className="text-center"
content="You are a member of this organization"
>
<div>
<FontAwesomeIcon
className="text-mineshaft-400"
icon={faUserCheck}
size="sm"
/>
</div>
</Tooltip>
)}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
{!isMember && (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleAccessOrg(org.id);
}}
icon={<FontAwesomeIcon icon={faUserPlus} />}
>
Join Organization
</DropdownMenuItem>
)}
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteOrganization", {
orgId: org.id,
orgName: org.name
});
}}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete Organization
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
);
})
)}
</TBody>
</Table>
{!isPending && isEmpty && <EmptyState title="No organizations found" icon={faBuilding} />}
</TableContainer>
{!isEmpty && (
<Button
className="mt-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={() => fetchNextPage()}
>
{hasNextPage ? "Load More" : "End of list"}
</Button>
)}
</div>
<ViewMembersModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
isOpen={popUp.viewMembers.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("viewMembers", isOpen)}
/>
</>
);
};
export const OrganizationsTable = () => {
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteOrganization",
"deleteOrganizationMembership",
"deleteUser",
"viewMembers",
"createOrganization"
] as const);
const { mutateAsync: deleteOrganization } = useAdminDeleteOrganization();
const { mutateAsync: deleteOrganizationMembership } = useAdminDeleteOrganizationMembership();
const { mutateAsync: deleteUser } = useAdminDeleteUser();
const handleDeleteOrganization = async () => {
const { orgId } = popUp?.deleteOrganization?.data as { orgId: string };
await deleteOrganization(orgId);
createNotification({
type: "success",
text: "Successfully deleted organization"
});
handlePopUpClose("deleteOrganization");
};
const handleDeleteOrganizationMembership = async () => {
const { orgId, membershipId } = popUp?.deleteOrganizationMembership?.data as {
orgId: string;
membershipId: string;
};
if (!orgId || !membershipId) {
return;
}
await deleteOrganizationMembership({ organizationId: orgId, membershipId });
createNotification({
type: "success",
text: "Successfully removed user from organization"
});
handlePopUpClose("viewMembers");
handlePopUpClose("deleteOrganizationMembership");
};
const handleDeleteUser = async () => {
const { userId } = popUp?.deleteUser?.data as { userId: string };
if (!userId) {
return;
}
await deleteUser(userId);
createNotification({
type: "success",
text: "Successfully deleted user"
});
handlePopUpClose("viewMembers");
handlePopUpClose("deleteUser");
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-mineshaft-100">Organizations</p>
<p className="text-sm text-bunker-300">
Manage, join and view organizations across your instance.
</p>
</div>
<Button
colorSchema="secondary"
onClick={() => handlePopUpOpen("createOrganization")}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Add Organization
</Button>
</div>
<OrganizationsPanelTable
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp.deleteOrganization.isOpen}
deleteKey="delete"
title={`Are you sure you want to delete organization ${
(popUp?.deleteOrganization?.data as { orgName: string })?.orgName || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrganization", isOpen)}
onDeleteApproved={handleDeleteOrganization}
/>
<DeleteActionModal
isOpen={popUp.deleteOrganizationMembership.isOpen}
deleteKey="delete"
title={`Are you sure you want to remove ${
(popUp?.deleteOrganizationMembership?.data as { username: string })?.username || ""
} from organization ${
(popUp?.deleteOrganizationMembership?.data as { orgName: string })?.orgName || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteOrganizationMembership", isOpen)}
onDeleteApproved={handleDeleteOrganizationMembership}
/>
<DeleteActionModal
isOpen={popUp.deleteUser.isOpen}
deleteKey="delete"
title={`Are you sure you want to delete user ${
(popUp?.deleteUser?.data as { username: string })?.username || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteUser", isOpen)}
onDeleteApproved={handleDeleteUser}
/>
<AddOrganizationModal
isOpen={popUp.createOrganization.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("createOrganization", isOpen)}
/>
</div>
);
};
@@ -218,7 +218,7 @@ const UserPanelTable = ({
</div>
</Td>
<Td className="w-5/12 max-w-0">
<p className="truncate">{email}</p>
<p className="truncate">{username || email}</p>
</Td>
<Td>
<div className="flex justify-end">
@@ -463,6 +463,12 @@ export const UserIdentitiesTable = () => {
</div>
</div>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<p className="text-xl font-semibold text-mineshaft-100">User Identities</p>
<p className="text-sm text-bunker-300">Manage user identities across your instance.</p>
</div>
</div>
<UserPanelTable
handlePopUpOpen={handlePopUpOpen}
users={users}
@@ -515,14 +521,14 @@ export const UserIdentitiesTable = () => {
/>
<DeleteActionModal
isOpen={popUp.removeUsers.isOpen}
title="Are you sure you want to remove the following users?"
title="Are you sure you want to delete the following users?"
onChange={(isOpen) => handlePopUpToggle("removeUsers", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleRemoveUsers()}
buttonText="Remove"
buttonText="Delete"
>
<div className="mt-4 text-sm text-mineshaft-400">
The following members will be removed:
The following users will be deleted:
</div>
<div className="mt-2 max-h-[20rem] overflow-y-auto rounded border border-mineshaft-600 bg-red/10 p-4 pl-8 text-sm text-red-200">
<ul className="list-disc">
@@ -549,7 +555,7 @@ export const UserIdentitiesTable = () => {
className="ml-1 mt-[0.05rem] inline-flex w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faWarning} />
<span>Removing Yourself</span>
<span>Deleting Yourself</span>
</Badge>
</div>
</Tooltip>
@@ -0,0 +1,3 @@
export * from "./MachineIdentitiesTable";
export * from "./OrganizationsTable";
export * from "./UserIdentitiesTable";
@@ -1,11 +1,11 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { OrganizationResourcesPage } from "./OrganizationResourcesPage";
import { ResourceOverviewPage } from "./ResourceOverviewPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/organizations"
"/_authenticate/_inject-org-details/admin/_admin-layout/resources/overview"
)({
component: OrganizationResourcesPage,
component: ResourceOverviewPage,
beforeLoad: async () => {
return {
breadcrumbs: [
@@ -14,9 +14,9 @@ export const Route = createFileRoute(
link: linkOptions({ to: "/admin" })
},
{
label: "Organizations",
label: "Resource Overview",
link: linkOptions({
to: "/admin/resources/organizations"
to: "/admin/resources/overview"
})
}
]
@@ -1,27 +0,0 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { UserIdentitiesTable } from "./components";
export const UserIdentitiesResourcesPage = () => {
const { t } = useTranslation();
return (
<div className="h-full bg-bunker-800">
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<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">
<PageHeader
title="User Identities"
description="Manage all user identities within your Infisical instance."
/>
<UserIdentitiesTable />
</div>
</div>
</div>
);
};
@@ -1 +0,0 @@
export { UserIdentitiesTable } from "./UserIdentitiesTable";
@@ -88,7 +88,15 @@ export const SelectOrganizationSection = () => {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
let url = "";
if (organization.orgAuthMethod === AuthMethod.OIDC) {
if (organization.googleSsoAuthEnforced) {
if (authToken.authMethod !== AuthMethod.GOOGLE) {
url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`;
if (callbackPort) {
url += `&callback_port=${callbackPort}`;
}
}
} else if (organization.orgAuthMethod === AuthMethod.OIDC) {
url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${
callbackPort ? `&callbackPort=${callbackPort}` : ""
}`;
@@ -98,15 +106,6 @@ export const SelectOrganizationSection = () => {
if (callbackPort) {
url += `?callback_port=${callbackPort}`;
}
} else if (
organization.googleSsoAuthEnforced &&
authToken.authMethod !== AuthMethod.GOOGLE
) {
url = `/api/v1/sso/redirect/google?org_slug=${organization.slug}`;
if (callbackPort) {
url += `&callback_port=${callbackPort}`;
}
}
// we are conditionally checking if the url is set because it may not be set if google SSO is enforced, but the user is already logged in with google SSO
@@ -13,7 +13,7 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
useCreatePkiAlert,
useGetPkiAlertById,
@@ -60,15 +60,15 @@ type Props = {
};
export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { data: alert } = useGetPkiAlertById(
(popUp?.pkiAlert?.data as { alertId: string })?.alertId || ""
);
const { data: pkiCollections } = useListWorkspacePkiCollections({
workspaceId: projectId
projectId
});
const { mutateAsync: createPkiAlert } = useCreatePkiAlert();
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { useDeletePkiAlert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -12,8 +12,8 @@ import { PkiAlertModal } from "./PkiAlertModal";
import { PkiAlertsTable } from "./PkiAlertsTable";
export const PkiAlertsSection = () => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { mutateAsync: deletePkiAlert } = useDeletePkiAlert();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -10,7 +10,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useListWorkspacePkiAlerts } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -24,11 +24,11 @@ type Props = {
};
export const PkiAlertsTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { data, isPending } = useListWorkspacePkiAlerts({
workspaceId: projectId
projectId
});
return (
@@ -6,7 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
useCreatePkiCollection,
useGetPkiCollectionById,
@@ -28,8 +28,8 @@ type Props = {
export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { data: pkiCollection } = useGetPkiCollectionById(
(popUp?.pkiCollection?.data as { collectionId: string })?.collectionId || ""
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { useDeletePkiCollection } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -12,8 +12,8 @@ import { PkiCollectionModal } from "./PkiCollectionModal";
import { PkiCollectionTable } from "./PkiCollectionTable";
export const PkiCollectionSection = () => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { mutateAsync: deletePkiCollection } = useDeletePkiCollection();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -19,7 +19,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { useListWorkspacePkiCollections } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -32,11 +32,11 @@ type Props = {
export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { data, isPending } = useListWorkspacePkiCollections({
workspaceId: projectId
projectId
});
return (
@@ -15,7 +15,7 @@ import {
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api";
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -30,7 +30,7 @@ import {
} from "./components";
const Page = () => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const navigate = useNavigate();
const params = useParams({
from: ROUTE_PATHS.CertManager.CertAuthDetailsByIDPage.id
@@ -38,11 +38,11 @@ const Page = () => {
const { caName } = params as { caName: string };
const { data } = useGetCa({
caName,
projectId: currentWorkspace?.id || "",
projectId: currentProject?.id || "",
type: CaType.INTERNAL
}) as { data: TInternalCertificateAuthority };
const projectId = currentWorkspace?.id || "";
const projectId = currentProject?.id || "";
const { mutateAsync: deleteCa } = useDeleteCa();
@@ -55,11 +55,11 @@ const Page = () => {
const onRemoveCaSubmit = async () => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
await deleteCa({
caName,
projectId: currentWorkspace.id,
projectId: currentProject.id,
type: CaType.INTERNAL
});
@@ -4,7 +4,7 @@ import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { CaStatus, CaType, InternalCaType, useGetCa } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
@@ -21,7 +21,7 @@ type Props = {
};
export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
@@ -31,7 +31,7 @@ export const CaDetailsSection = ({ caName, handlePopUpOpen }: Props) => {
const { data } = useGetCa({
caName,
projectId: currentWorkspace.id,
projectId: currentProject.id,
type: CaType.INTERNAL
});
@@ -13,7 +13,7 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
CaRenewalType,
useRenewCa
@@ -45,8 +45,8 @@ type Props = {
};
export const CaRenewalModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectSlug = currentWorkspace?.slug || "";
const { currentProject } = useProject();
const projectSlug = currentProject?.slug || "";
const popUpData = popUp?.renewCa?.data as {
caId: string;
@@ -8,7 +8,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, TextArea, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetCaCsr, useImportCaCertificate } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -26,7 +26,7 @@ type Props = {
};
export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const [copyTextCaCsr, isCopyingCaCsr, setCopyTextCaCsr] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
@@ -41,7 +41,7 @@ export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
});
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentWorkspace.id);
const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentProject.id);
useEffect(() => {
reset();
@@ -49,11 +49,11 @@ export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const onFormSubmit = async ({ certificate, certificateChain }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
if (!csr || !caId || !currentProject?.slug) return;
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
projectSlug: currentProject?.slug,
certificate,
certificateChain
});
@@ -6,7 +6,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
CaStatus,
useGetCaById,
@@ -46,16 +46,16 @@ type Props = {
};
export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
projectId: currentProject.id,
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentWorkspace.id);
const { mutateAsync: importCaCertificate } = useImportCaCertificate(currentProject.id);
const {
control,
@@ -102,7 +102,7 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
if (!csr || !caId || !currentProject?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
@@ -114,7 +114,7 @@ export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
projectSlug: currentProject?.slug,
certificate,
certificateChain
});
@@ -16,7 +16,7 @@ import {
Switch
// DatePicker
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
CaStatus,
CaType,
@@ -84,10 +84,10 @@ const caTypes = [
];
export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: ca } = useGetCa({
caName: (popUp?.ca?.data as { name: string })?.name || "",
projectId: currentWorkspace?.id || "",
projectId: currentProject?.id || "",
type: CaType.INTERNAL
});
@@ -178,13 +178,13 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
configuration
}: FormData) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
if (ca) {
// update
await updateMutateAsync({
caName: ca.name,
projectId: currentWorkspace.id,
projectId: currentProject.id,
name,
type: CaType.INTERNAL,
status,
@@ -193,7 +193,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
} else {
// create
await createMutateAsync({
projectId: currentWorkspace.id,
projectId: currentProject.id,
name,
type,
status,
@@ -5,7 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -15,7 +15,7 @@ import { CaModal } from "./CaModal";
import { CaTable } from "./CaTable";
export const CaSection = () => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { mutateAsync: deleteCa } = useDeleteCa();
const { mutateAsync: updateCa } = useUpdateCa();
@@ -30,9 +30,9 @@ export const CaSection = () => {
const onRemoveCaSubmit = async (caName: string) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
await deleteCa({ caName, projectId: currentWorkspace.id, type: CaType.INTERNAL });
await deleteCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL });
createNotification({
text: "Successfully deleted CA",
@@ -50,9 +50,9 @@ export const CaSection = () => {
const onUpdateCaStatus = async ({ caName, status }: { caName: string; status: CaStatus }) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
await updateCa({ caName, projectId: currentWorkspace.id, type: CaType.INTERNAL, status });
await updateCa({ caName, projectId: currentProject.id, type: CaType.INTERNAL, status });
createNotification({
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
@@ -22,7 +22,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListCasByTypeAndProjectId } from "@app/hooks/api";
import {
caStatusToNameMap,
@@ -49,8 +49,8 @@ type Props = {
export const CaTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentWorkspace.id);
const { currentProject } = useProject();
const { data, isPending } = useListCasByTypeAndProjectId(CaType.INTERNAL, currentProject.id);
const cas = data as TInternalCertificateAuthority[];
return (
@@ -80,7 +80,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
navigate({
to: "/projects/cert-management/$projectId/ca/$caName",
params: {
projectId: currentWorkspace.id,
projectId: currentProject.id,
caName: ca.name
}
})
@@ -4,6 +4,7 @@ import { SingleValue } from "react-select";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { AppConnectionOption } from "@app/components/app-connections";
import { createNotification } from "@app/components/notifications";
import {
Button,
@@ -16,7 +17,7 @@ import {
SelectItem,
Switch
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import {
TAvailableAppConnection,
@@ -127,11 +128,11 @@ const caTypes = [
];
export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: ca, isLoading: isCaLoading } = useGetCa({
caName: (popUp?.ca?.data as { name: string })?.name || "",
projectId: currentWorkspace?.id || "",
projectId: currentProject?.id || "",
type: (popUp?.ca?.data as { type: CaType })?.type || ""
});
@@ -201,17 +202,17 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
}, [popUp?.ca?.isOpen, popUp?.ca?.data, reset, ca]);
const { data: availableRoute53Connections, isPending: isRoute53Pending } =
useListAvailableAppConnections(AppConnection.AWS, {
useListAvailableAppConnections(AppConnection.AWS, currentProject.id, {
enabled: caType === CaType.ACME
});
const { data: availableCloudflareConnections, isPending: isCloudflarePending } =
useListAvailableAppConnections(AppConnection.Cloudflare, {
useListAvailableAppConnections(AppConnection.Cloudflare, currentProject.id, {
enabled: caType === CaType.ACME
});
const { data: availableAzureConnections, isPending: isAzurePending } =
useListAvailableAppConnections(AppConnection.AzureADCS, {
useListAvailableAppConnections(AppConnection.AzureADCS, currentProject.id, {
enabled: caType === CaType.AZURE_AD_CS
});
@@ -297,7 +298,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
configuration: formConfiguration
}: FormData) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
let configPayload: any;
@@ -321,7 +322,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
if (ca) {
await updateMutateAsync({
caName: ca.name,
projectId: currentWorkspace.id,
projectId: currentProject.id,
name,
type,
status,
@@ -330,7 +331,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
});
} else {
await createMutateAsync({
projectId: currentWorkspace.id,
projectId: currentProject.id,
name,
type,
status,
@@ -457,6 +458,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
@@ -599,6 +601,7 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
placeholder="Select connection..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: AppConnectionOption }}
/>
</FormControl>
)}
@@ -5,7 +5,7 @@ import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -13,7 +13,7 @@ import { ExternalCaModal } from "./ExternalCaModal";
import { ExternalCaTable } from "./ExternalCaTable";
export const ExternalCaSection = () => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { mutateAsync: deleteCa } = useDeleteCa();
const { mutateAsync: updateCa } = useUpdateCa();
@@ -26,9 +26,9 @@ export const ExternalCaSection = () => {
const onRemoveCaSubmit = async (caName: string, type: CaType) => {
try {
if (!currentWorkspace?.id) return;
if (!currentProject?.id) return;
await deleteCa({ caName, type, projectId: currentWorkspace.id });
await deleteCa({ caName, type, projectId: currentProject.id });
createNotification({
text: "Successfully deleted CA",
@@ -54,9 +54,9 @@ export const ExternalCaSection = () => {
status: CaStatus;
}) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
await updateCa({ caName: name, type, status, projectId: currentWorkspace.id });
await updateCa({ caName: name, type, status, projectId: currentProject.id });
createNotification({
text: `Successfully ${status === CaStatus.ACTIVE ? "enabled" : "disabled"} CA`,
@@ -26,7 +26,7 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaStatus, CaType, useListExternalCasByProjectId } from "@app/hooks/api";
import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -44,8 +44,8 @@ type Props = {
};
export const ExternalCaTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListExternalCasByProjectId(currentWorkspace.id);
const { currentProject } = useProject();
const { data, isPending } = useListExternalCasByProjectId(currentProject.id);
return (
<div>
@@ -14,7 +14,7 @@ import {
SelectItem,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useGetCert, useImportCertificate, useListWorkspacePkiCollections } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -48,13 +48,13 @@ type TCertificateDetails = {
export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: cert } = useGetCert(
(popUp?.certificateImport?.data as { serialNumber: string })?.serialNumber || ""
);
const { data } = useListWorkspacePkiCollections({
workspaceId: currentWorkspace?.id || ""
projectId: currentProject?.id || ""
});
const { mutateAsync: importCertificate } = useImportCertificate();
@@ -76,10 +76,10 @@ export const CertificateImportModal = ({ popUp, handlePopUpToggle }: Props) => {
collectionId
}: FormData) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
const { serialNumber, certificate, certificateChain, privateKey } = await importCertificate({
projectSlug: currentWorkspace.slug,
projectSlug: currentProject.slug,
certificatePem,
privateKeyPem,
@@ -22,7 +22,7 @@ import {
SelectItem,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
CaStatus,
useCreateCertificate,
@@ -89,22 +89,22 @@ const CERT_TEMPLATE_NONE_VALUE = "none";
export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: cert } = useGetCert(
(popUp?.certificate?.data as { serialNumber: string })?.serialNumber || ""
);
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
projectId: currentProject.id,
status: CaStatus.ACTIVE
});
const { data } = useListWorkspacePkiCollections({
workspaceId: currentWorkspace?.id || ""
projectId: currentProject?.id || ""
});
const { data: templatesData } = useListWorkspaceCertificateTemplates({
workspaceId: currentWorkspace?.id || ""
projectId: currentProject?.id || ""
});
const { mutateAsync: createCertificate } = useCreateCertificate();
@@ -191,12 +191,12 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
extendedKeyUsages
}: FormData) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
caId: !selectedCertTemplate ? caId : undefined,
certificateTemplateId: selectedCertTemplate ? selectedCertTemplateId : undefined,
projectSlug: currentWorkspace.slug,
projectSlug: currentProject.slug,
pkiCollectionId: collectionId,
friendlyName,
commonName,
@@ -4,7 +4,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useRevokeCert } from "@app/hooks/api";
import { crlReasons } from "@app/hooks/api/certificates/constants";
import { CrlReason } from "@app/hooks/api/certificates/enums";
@@ -35,7 +35,7 @@ type Props = {
};
export const CertificateRevocationModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { mutateAsync: revokeCertificate } = useRevokeCert();
const {
@@ -49,12 +49,12 @@ export const CertificateRevocationModal = ({ popUp, handlePopUpToggle }: Props)
const onFormSubmit = async ({ revocationReason }: FormData) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
const { serialNumber } = popUp.revokeCertificate.data as { serialNumber: string };
await revokeCertificate({
projectSlug: currentWorkspace.slug,
projectSlug: currentProject.slug,
serialNumber,
revocationReason
});
@@ -22,7 +22,7 @@ import {
SelectItem,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
CaStatus,
useCreateCertTemplate,
@@ -82,7 +82,7 @@ type Props = {
};
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: ca } = useGetCaById(caId);
@@ -91,12 +91,12 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
);
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
projectId: currentProject?.id,
status: CaStatus.ACTIVE
});
const { data: collectionsData } = useListWorkspacePkiCollections({
workspaceId: currentWorkspace?.id || ""
projectId: currentProject?.id || ""
});
const { mutateAsync: createCertTemplate } = useCreateCertTemplate();
@@ -155,7 +155,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
keyUsages,
extendedKeyUsages
}: FormData) => {
if (!currentWorkspace?.id) {
if (!currentProject?.id) {
return;
}
@@ -163,7 +163,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
if (certTemplate) {
await updateCertTemplate({
id: certTemplate.id,
projectId: currentWorkspace.id,
projectId: currentProject.id,
pkiCollectionId: collectionId,
caId,
name,
@@ -184,7 +184,7 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
});
} else {
await createCertTemplate({
projectId: currentWorkspace.id,
projectId: currentProject.id,
pkiCollectionId: collectionId,
caId,
name,
@@ -12,7 +12,7 @@ import { DeleteActionModal, IconButton } from "@app/components/v2";
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useWorkspace
useProject
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplate } from "@app/hooks/api";
@@ -33,18 +33,18 @@ export const CertificateTemplatesSection = ({ caId }: Props) => {
"upgradePlan"
] as const);
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { mutateAsync: deleteCertTemplate } = useDeleteCertTemplate();
const onRemoveCertificateTemplateSubmit = async (id: string) => {
if (!currentWorkspace?.id) {
if (!currentProject?.id) {
return;
}
try {
await deleteCertTemplate({
id,
projectId: currentWorkspace.id
projectId: currentProject.id
});
createNotification({
@@ -7,7 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useWorkspace
useProject
} from "@app/context";
import { useDeleteCert } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -19,7 +19,7 @@ import { CertificateRevocationModal } from "./CertificateRevocationModal";
import { CertificatesTable } from "./CertificatesTable";
export const CertificatesSection = () => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { mutateAsync: deleteCert } = useDeleteCert();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -32,9 +32,9 @@ export const CertificatesSection = () => {
const onRemoveCertificateSubmit = async (serialNumber: string) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
await deleteCert({ serialNumber, projectSlug: currentWorkspace.slug });
await deleteCert({ serialNumber, projectSlug: currentProject.slug });
createNotification({
text: "Successfully deleted certificate",
@@ -33,7 +33,7 @@ import {
import {
ProjectPermissionCertificateActions,
ProjectPermissionSub,
useWorkspace
useProject
} from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
@@ -62,15 +62,15 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data, isPending } = useListWorkspaceCertificates({
projectSlug: currentWorkspace?.slug ?? "",
projectId: currentProject?.slug ?? "",
offset: (page - 1) * perPage,
limit: perPage
});
// Fetch CA data to determine capabilities
const { data: caData } = useListCasByProjectId(currentWorkspace?.id ?? "");
const { data: caData } = useListCasByProjectId(currentProject?.id ?? "");
// Create mapping from caId to CA type for capability checking
const caCapabilityMap = useMemo(() => {
@@ -16,7 +16,7 @@ import {
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { useDeletePkiCollection, useGetPkiCollectionById } from "@app/hooks/api";
import { PkiItemType } from "@app/hooks/api/pkiCollections/constants";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -30,8 +30,8 @@ export const PkiCollectionPage = () => {
from: ROUTE_PATHS.CertManager.PkiCollectionDetailsByIDPage.id
});
const collectionId = params.collectionId as string;
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { currentProject } = useProject();
const projectId = currentProject?.id || "";
const { data } = useGetPkiCollectionById(collectionId);
const { mutateAsync: deletePkiCollection } = useDeletePkiCollection();
@@ -4,7 +4,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
CaStatus,
useAddItemToPkiCollection,
@@ -41,15 +41,15 @@ export const AddPkiCollectionItemModal = ({
popUp,
handlePopUpToggle
}: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug || "",
projectId: currentProject?.id || "",
status: CaStatus.ACTIVE
});
const { data } = useListWorkspaceCertificates({
projectSlug: currentWorkspace?.slug || "",
projectId: currentProject?.slug || "",
offset: 0,
limit: 25
});
@@ -19,7 +19,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
useWorkspace
useProject
} from "@app/context";
import { useDeletePkiSubscriber, useGetPkiSubscriber } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
@@ -29,8 +29,8 @@ import { PkiSubscriberCertificatesSection, PkiSubscriberDetailsSection } from ".
const Page = () => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { currentProject } = useProject();
const projectId = currentProject.id;
const subscriberName = useParams({
from: ROUTE_PATHS.CertManager.PkiSubscriberDetailsByIDPage.id,
select: (el) => el.subscriberName
@@ -27,8 +27,8 @@ import {
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
useProject,
useProjectPermission
} from "@app/context";
import { useGetPkiSubscriberCertificates } from "@app/hooks/api";
import { caSupportsCapability } from "@app/hooks/api/ca/constants";
@@ -45,8 +45,8 @@ type Props = {
const PER_PAGE_INIT = 25;
export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { currentProject } = useProject();
const projectId = currentProject.id;
const { permission } = useProjectPermission();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
@@ -64,7 +64,7 @@ export const PkiSubscriberCertificatesTable = ({ subscriberName, handlePopUpOpen
);
// Fetch CA data to determine capabilities
const { data: caData } = useListCasByProjectId(currentWorkspace.id);
const { data: caData } = useListCasByProjectId(currentProject.id);
// Create mapping from caId to CA type for capability checking
const caCapabilityMap = useMemo(() => {
@@ -16,8 +16,8 @@ import {
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
useProject,
useProjectPermission
} from "@app/context";
import { useTimedReset } from "@app/hooks";
import {
@@ -44,8 +44,8 @@ type TCertificateDetails = {
};
export const PkiSubscriberDetailsSection = ({ subscriberName, handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { currentProject } = useProject();
const projectId = currentProject.id;
const { permission } = useProjectPermission();
const [certificateDetails, setCertificateDetails] = useState<TCertificateDetails | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -22,7 +22,7 @@ import {
TabPanel,
Tabs
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
CaType,
useCreatePkiSubscriber,
@@ -158,8 +158,8 @@ const schema = z
export type FormData = z.infer<typeof schema>;
export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { currentProject } = useProject();
const projectId = currentProject.id;
const { data: subscribers } = useListWorkspacePkiSubscribers(projectId);
const { data: cas } = useListCasByProjectId(projectId);
const [tabValue, setTabValue] = useState<FormTab>(FormTab.Configuration);
@@ -7,7 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
useWorkspace
useProject
} from "@app/context";
import { useDeletePkiSubscriber, useUpdatePkiSubscriber } from "@app/hooks/api";
import { PkiSubscriberStatus } from "@app/hooks/api/pkiSubscriber/types";
@@ -17,8 +17,8 @@ import { PkiSubscriberModal } from "./PkiSubscriberModal";
import { PkiSubscribersTable } from "./PkiSubscribersTable";
export const PkiSubscriberSection = () => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { currentProject } = useProject();
const projectId = currentProject.id;
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
@@ -55,7 +55,7 @@ export const PkiSubscriberSection = () => {
status: PkiSubscriberStatus;
}) => {
try {
if (!currentWorkspace?.slug) return;
if (!currentProject?.slug) return;
await updatePkiSubscriber({ subscriberName, projectId, status });
@@ -30,7 +30,7 @@ import {
import {
ProjectPermissionPkiSubscriberActions,
ProjectPermissionSub,
useWorkspace
useProject
} from "@app/context";
import { useListWorkspacePkiSubscribers } from "@app/hooks/api";
import {
@@ -49,8 +49,8 @@ type Props = {
export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListWorkspacePkiSubscribers(currentWorkspace?.id || "");
const { currentProject } = useProject();
const { data, isPending } = useListWorkspacePkiSubscribers(currentProject?.id || "");
return (
<div>
<TableContainer>
@@ -77,7 +77,7 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => {
navigate({
to: "/projects/cert-management/$projectId/subscribers/$subscriberName",
params: {
projectId: currentWorkspace.id,
projectId: currentProject.id,
subscriberName: subscriber.name
}
})
@@ -42,8 +42,8 @@ import {
import {
ProjectPermissionPkiTemplateActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
useProject,
useSubscription
} from "@app/context";
import { usePopUp } from "@app/hooks";
import { useDeleteCertTemplateV2 } from "@app/hooks/api";
@@ -55,7 +55,7 @@ import { PkiTemplateForm } from "./components/PkiTemplateForm";
const PER_PAGE_INIT = 25;
export const PkiTemplateListPage = () => {
const { t } = useTranslation();
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -68,7 +68,7 @@ export const PkiTemplateListPage = () => {
const { subscription } = useSubscription();
const { data, isPending } = useListCertificateTemplates({
projectId: currentWorkspace.id,
projectId: currentProject.id,
offset: (page - 1) * perPage,
limit: perPage
});
@@ -78,7 +78,7 @@ export const PkiTemplateListPage = () => {
const onRemovePkiSubscriberSubmit = async () => {
try {
const pkiTemplate = await deleteCertTemplate.mutateAsync({
projectId: currentWorkspace.id,
projectId: currentProject.id,
templateName: popUp?.deleteTemplate?.data?.name
});
@@ -18,7 +18,7 @@ import {
Input,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import {
useCreateCertTemplateV2,
useListCasByProjectId,
@@ -72,9 +72,9 @@ type Props = {
};
export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { data: cas, isPending: isCaLoading } = useListCasByProjectId(currentWorkspace.id);
const { data: cas, isPending: isCaLoading } = useListCasByProjectId(currentProject.id);
const { mutateAsync: createCertTemplate } = useCreateCertTemplateV2();
const { mutateAsync: updateCertTemplate } = useUpdateCertTemplateV2();
@@ -124,7 +124,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
extendedKeyUsages,
ca
}: FormData) => {
if (!currentWorkspace?.id) {
if (!currentProject?.id) {
return;
}
@@ -132,7 +132,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
if (certTemplate) {
await updateCertTemplate({
templateName: certTemplate.name,
projectId: currentWorkspace.id,
projectId: currentProject.id,
caName: ca.name,
name,
commonName,
@@ -152,7 +152,7 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
});
} else {
await createCertTemplate({
projectId: currentWorkspace.id,
projectId: currentProject.id,
caName: ca.name,
name,
commonName,
+6 -6
View File
@@ -1,9 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from "@app/components/v2";
import { workspaceKeys } from "@app/hooks/api";
import { projectKeys } from "@app/hooks/api";
import { fetchProjectById } from "@app/hooks/api/projects/queries";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
@@ -13,15 +13,15 @@ export const Route = createFileRoute(
component: PkiManagerLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
queryKey: projectKeys.getProjectById(params.projectId),
queryFn: () => fetchProjectById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
projectId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId })
});
return {
@@ -13,7 +13,7 @@ import {
ModalContent,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip";
import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types";
@@ -60,8 +60,8 @@ type FormProps = Pick<Props, "kmipClient"> & {
const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
const createKmipClient = useCreateKmipClient();
const updateKmipClient = useUpdateKmipClient();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { currentProject } = useProject();
const projectId = currentProject.id;
const isUpdate = !!kmipClient;
const {
@@ -39,9 +39,9 @@ import {
import {
ProjectPermissionKmipActions,
ProjectPermissionSub,
useProject,
useProjectPermission,
useSubscription,
useWorkspace
useSubscription
} from "@app/context";
import {
getUserTablePreference,
@@ -59,9 +59,9 @@ import { KmipClientCertificateModal } from "./KmipClientCertificateModal";
import { KmipClientModal } from "./KmipClientModal";
export const KmipClientTable = () => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const projectId = currentWorkspace?.id ?? "";
const projectId = currentProject?.id ?? "";
const {
offset,
@@ -14,7 +14,7 @@ import {
SelectItem,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useProject } from "@app/context";
import { keyUsageDefaultOption, kmsKeyUsageOptions } from "@app/helpers/kms";
import {
AllowedEncryptionKeyAlgorithms,
@@ -49,8 +49,8 @@ type FormProps = Pick<Props, "cmek"> & {
const CmekForm = ({ onComplete, cmek }: FormProps) => {
const createCmek = useCreateCmek();
const updateCmek = useUpdateCmek();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace.id;
const { currentProject } = useProject();
const projectId = currentProject.id;
const isUpdate = !!cmek;
const {
@@ -49,8 +49,8 @@ import {
ProjectPermissionActions,
ProjectPermissionCmekActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
useProject,
useProjectPermission
} from "@app/context";
import { kmsKeyUsageOptions } from "@app/helpers/kms";
import {
@@ -87,10 +87,10 @@ const getStatusBadgeProps = (
};
export const CmekTable = () => {
const { currentWorkspace } = useWorkspace();
const { currentProject } = useProject();
const { permission } = useProjectPermission();
const projectId = currentWorkspace?.id ?? "";
const projectId = currentProject?.id ?? "";
const {
offset,
+6 -6
View File
@@ -1,9 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { BreadcrumbTypes } from "@app/components/v2";
import { workspaceKeys } from "@app/hooks/api";
import { projectKeys } from "@app/hooks/api";
import { fetchProjectById } from "@app/hooks/api/projects/queries";
import { fetchUserProjectPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries";
import { fetchWorkspaceById } from "@app/hooks/api/workspace/queries";
import { KmsLayout } from "@app/layouts/KmsLayout";
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
@@ -13,15 +13,15 @@ export const Route = createFileRoute(
component: KmsLayout,
beforeLoad: async ({ params, context }) => {
const project = await context.queryClient.ensureQueryData({
queryKey: workspaceKeys.getWorkspaceById(params.projectId),
queryFn: () => fetchWorkspaceById(params.projectId)
queryKey: projectKeys.getProjectById(params.projectId),
queryFn: () => fetchProjectById(params.projectId)
});
await context.queryClient.ensureQueryData({
queryKey: roleQueryKeys.getUserProjectPermissions({
workspaceId: params.projectId
projectId: params.projectId
}),
queryFn: () => fetchUserProjectPermissions({ workspaceId: params.projectId })
queryFn: () => fetchUserProjectPermissions({ projectId: params.projectId })
});
return {
@@ -15,7 +15,8 @@ import { setAuthToken } from "@app/hooks/api/reactQuery";
const QueryParamsSchema = z.object({
callback_port: z.coerce.number().optional().catch(undefined),
force: z.boolean().optional()
force: z.boolean().optional(),
org_id: z.string().optional().catch(undefined)
});
export const AuthConsentWrapper = () => {
@@ -102,6 +103,12 @@ export const Route = createFileRoute("/_restrict-login-signup")({
return;
}
if (search.org_id) {
if (location.pathname.endsWith("select-organization")) return;
throw redirect({ to: "/login/select-organization", search: { org_id: search.org_id } });
}
if (!data.organizationId) {
if (
location.pathname.endsWith("select-organization") ||
@@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faQuestionCircle, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
@@ -25,6 +26,7 @@ import {
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { getObjectFromSeconds } from "@app/helpers/datetime";
import {
MachineIdentityAuthMethod,
useAddIdentityLdapAuth,
@@ -35,6 +37,8 @@ import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { LockoutTab } from "./lockout/LockoutTab";
import { superRefineLockout } from "./lockout/super-refine";
import { IdentityFormTab } from "./types";
const schema = z
@@ -74,9 +78,28 @@ const schema = z
ipAddress: z.string().max(50)
})
)
.min(1)
.min(1),
lockoutEnabled: z.boolean().default(true),
lockoutThreshold: z
.string()
.refine(
(value) => Number(value) <= 30 && Number(value) >= 1,
"Lockout threshold must be between 1 and 30"
),
lockoutDurationValue: z.string(),
lockoutDurationUnit: z.enum(["s", "m", "h", "d"], {
invalid_type_error: "Please select a valid time unit"
}),
lockoutCounterResetValue: z.string(),
lockoutCounterResetUnit: z.enum(["s", "m", "h"], {
invalid_type_error: "Please select a valid time unit"
})
})
.required()
.superRefine((data, ctx) => {
superRefineLockout(data, ctx);
// Validation based on scope
if (data.scope === "template") {
if (!data.templateId) {
@@ -178,12 +201,25 @@ export const IdentityLdapAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
lockoutEnabled: true,
lockoutThreshold: "3",
lockoutDurationValue: "5",
lockoutDurationUnit: "m",
lockoutCounterResetValue: "30",
lockoutCounterResetUnit: "s"
}
});
const scope = watch("scope");
const lockoutEnabledWatch = watch("lockoutEnabled");
const lockoutThresholdWatch = watch("lockoutThreshold");
const lockoutDurationValueWatch = watch("lockoutDurationValue");
const lockoutDurationUnitWatch = watch("lockoutDurationUnit");
const lockoutCounterResetValueWatch = watch("lockoutCounterResetValue");
const lockoutCounterResetUnitWatch = watch("lockoutCounterResetUnit");
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
@@ -210,6 +246,9 @@ export const IdentityLdapAuthForm = ({
if (data) {
const detectedScope = determineScope(data);
const lockoutDurationObj = getObjectFromSeconds(data.lockoutDurationSeconds);
const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterResetSeconds);
reset({
scope: detectedScope,
templateId: data.templateId || "",
@@ -229,7 +268,13 @@ export const IdentityLdapAuthForm = ({
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
};
}
)
),
lockoutEnabled: data.lockoutEnabled,
lockoutThreshold: String(data.lockoutThreshold),
lockoutDurationValue: String(lockoutDurationObj.value),
lockoutDurationUnit: lockoutDurationObj.unit as "s" | "m" | "h" | "d",
lockoutCounterResetValue: String(lockoutCounterResetObj.value),
lockoutCounterResetUnit: lockoutCounterResetObj.unit as "s" | "m" | "h"
});
return;
}
@@ -247,7 +292,13 @@ export const IdentityLdapAuthForm = ({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
lockoutEnabled: true,
lockoutThreshold: "3",
lockoutDurationValue: "5",
lockoutDurationUnit: "m",
lockoutCounterResetValue: "30",
lockoutCounterResetUnit: "s"
});
}, [data, reset]);
@@ -275,9 +326,19 @@ export const IdentityLdapAuthForm = ({
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
accessTokenTrustedIps,
lockoutEnabled,
lockoutThreshold,
lockoutDurationValue,
lockoutDurationUnit,
lockoutCounterResetValue,
lockoutCounterResetUnit
} = formData;
const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000;
const lockoutCounterResetSeconds =
ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000;
const basePayload = {
organizationId: orgId,
identityId,
@@ -287,7 +348,11 @@ export const IdentityLdapAuthForm = ({
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
accessTokenTrustedIps,
lockoutEnabled,
lockoutThreshold: Number(lockoutThreshold),
lockoutDurationSeconds,
lockoutCounterResetSeconds
};
// Add scope-specific fields
@@ -327,7 +392,10 @@ export const IdentityLdapAuthForm = ({
return (
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
const firstErrorField = Object.keys(fields)[0];
let tab = IdentityFormTab.Advanced;
if (
[
"scope",
"templateId",
@@ -340,15 +408,29 @@ export const IdentityLdapAuthForm = ({
"allowedFields",
"accessTokenMaxTTL",
"accessTokenNumUsesLimit"
].includes(Object.keys(fields)[0])
? IdentityFormTab.Configuration
: IdentityFormTab.Advanced
);
].includes(firstErrorField)
) {
tab = IdentityFormTab.Configuration;
} else if (
[
"lockoutEnabled",
"lockoutThreshold",
"lockoutDurationValue",
"lockoutDurationUnit",
"lockoutCounterResetValue",
"lockoutCounterResetUnit"
].includes(firstErrorField)
) {
tab = IdentityFormTab.Lockout;
}
setTabValue(tab);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Lockout}>Lockout</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
@@ -691,6 +773,15 @@ export const IdentityLdapAuthForm = ({
)}
/>
</TabPanel>
<LockoutTab
control={control}
lockoutEnabled={lockoutEnabledWatch}
lockoutThreshold={lockoutThresholdWatch}
lockoutDurationValue={lockoutDurationValueWatch}
lockoutDurationUnit={lockoutDurationUnitWatch}
lockoutCounterResetValue={lockoutCounterResetValueWatch}
lockoutCounterResetUnit={lockoutCounterResetUnitWatch}
/>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
@@ -12,9 +12,6 @@ import {
FormControl,
IconButton,
Input,
Select,
SelectItem,
Switch,
Tab,
TabList,
TabPanel,
@@ -30,6 +27,8 @@ import {
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { LockoutTab } from "./lockout/LockoutTab";
import { superRefineLockout } from "./lockout/super-refine";
import { IdentityFormTab } from "./types";
const schema = z
@@ -83,61 +82,7 @@ const schema = z
})
})
.required()
.superRefine((data, ctx) => {
const {
lockoutDurationValue,
lockoutCounterResetValue,
lockoutDurationUnit,
lockoutCounterResetUnit,
lockoutEnabled
} = data;
if (!lockoutEnabled) return;
let isAnyParseError = false;
const parsedLockoutDuration = parseInt(lockoutDurationValue, 10);
if (Number.isNaN(parsedLockoutDuration)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout duration must be a number",
path: ["lockoutDurationValue"]
});
isAnyParseError = true;
}
const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10);
if (Number.isNaN(parsedLockoutCounterReset)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout counter reset must be a number",
path: ["lockoutCounterResetValue"]
});
isAnyParseError = true;
}
if (isAnyParseError) return;
const lockoutDurationInSeconds = ms(`${parsedLockoutDuration}${lockoutDurationUnit}`) / 1000;
const lockoutCounterResetInSeconds =
ms(`${parsedLockoutCounterReset}${lockoutCounterResetUnit}`) / 1000;
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout duration must be between 30 seconds and 1 day",
path: ["lockoutDurationValue"]
});
}
if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout counter reset must be between 5 seconds and 1 hour",
path: ["lockoutCounterResetValue"]
});
}
});
.superRefine(superRefineLockout);
export type FormData = z.infer<typeof schema>;
@@ -432,187 +377,15 @@ export const IdentityUniversalAuthForm = ({
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Lockout}>
<div className="mb-3 flex flex-col">
<Controller
control={control}
name="lockoutEnabled"
defaultValue
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl
helperText={`The lockout feature will prevent login attempts for ${lockoutDurationValueWatch}${lockoutDurationUnitWatch} after ${lockoutThresholdWatch} consecutive login failures. If ${lockoutCounterResetValueWatch}${lockoutCounterResetUnitWatch} pass after the most recent failure, the lockout counter resets.`}
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
className="ml-0 mr-3 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
containerClassName="flex-row-reverse w-fit"
id="lockout-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
Lockout
</Switch>
</FormControl>
);
}}
/>
<div className="flex flex-col gap-2">
<Controller
control={control}
name="lockoutThreshold"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
label="Lockout Threshold"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The amount of times login must fail before locking the identity auth method"
>
<Input
{...field}
placeholder="Enter lockout threshold..."
isDisabled={!lockoutEnabledWatch}
/>
</FormControl>
);
}}
/>
<div className="flex items-end gap-2">
<Controller
control={control}
name="lockoutDurationValue"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
label="Lockout Duration"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="How long an identity auth method lockout lasts"
>
<Input
{...field}
placeholder="Enter lockout duration..."
isDisabled={!lockoutEnabledWatch}
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name="lockoutDurationUnit"
render={({ field, fieldState: { error } }) => (
<FormControl
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
isError={Boolean(error)}
errorText={error?.message}
>
<Select
isDisabled={!lockoutEnabledWatch}
value={field.value}
className="min-w-32 pr-2"
onValueChange={field.onChange}
position="popper"
>
<SelectItem
value="s"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Seconds</div>
</SelectItem>
<SelectItem
value="m"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Minutes</div>
</SelectItem>
<SelectItem
value="h"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Hours</div>
</SelectItem>
<SelectItem
value="d"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Days</div>
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex items-end gap-2">
<Controller
control={control}
name="lockoutCounterResetValue"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
label="Lockout Counter Reset"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
>
<Input
{...field}
placeholder="Enter lockout counter reset..."
isDisabled={!lockoutEnabledWatch}
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name="lockoutCounterResetUnit"
render={({ field, fieldState: { error } }) => (
<FormControl
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
isError={Boolean(error)}
errorText={error?.message}
>
<Select
isDisabled={!lockoutEnabledWatch}
value={field.value}
className="min-w-32 pr-2"
onValueChange={field.onChange}
position="popper"
>
<SelectItem
value="s"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Seconds</div>
</SelectItem>
<SelectItem
value="m"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Minutes</div>
</SelectItem>
<SelectItem
value="h"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Hours</div>
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
</div>
</div>
</TabPanel>
<LockoutTab
control={control}
lockoutEnabled={lockoutEnabledWatch}
lockoutThreshold={lockoutThresholdWatch}
lockoutDurationValue={lockoutDurationValueWatch}
lockoutDurationUnit={lockoutDurationUnitWatch}
lockoutCounterResetValue={lockoutCounterResetValueWatch}
lockoutCounterResetUnit={lockoutCounterResetUnitWatch}
/>
<TabPanel value={IdentityFormTab.Advanced}>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
@@ -0,0 +1,205 @@
import { Control, Controller } from "react-hook-form";
import { FormControl, Input, Select, SelectItem, Switch, TabPanel } from "@app/components/v2";
import { IdentityFormTab } from "../types";
export const LockoutTab = ({
control,
lockoutEnabled,
lockoutThreshold,
lockoutDurationValue,
lockoutDurationUnit,
lockoutCounterResetValue,
lockoutCounterResetUnit
}: {
control: Control<any>;
lockoutEnabled: boolean;
lockoutThreshold: string;
lockoutDurationValue: string;
lockoutDurationUnit: "s" | "m" | "h" | "d";
lockoutCounterResetValue: string;
lockoutCounterResetUnit: "s" | "m" | "h";
}) => {
return (
<TabPanel value={IdentityFormTab.Lockout}>
<div className="mb-3 flex flex-col">
<Controller
control={control}
name="lockoutEnabled"
render={({ field: { value, onChange }, fieldState: { error } }) => {
return (
<FormControl
helperText={`The lockout feature will prevent login attempts for ${lockoutDurationValue}${lockoutDurationUnit} after ${lockoutThreshold} consecutive login failures. If ${lockoutCounterResetValue}${lockoutCounterResetUnit} pass after the most recent failure, the lockout counter resets.`}
isError={Boolean(error)}
errorText={error?.message}
>
<Switch
className="ml-0 mr-3 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
containerClassName="flex-row-reverse w-fit"
id="lockout-enabled"
thumbClassName="bg-mineshaft-800"
onCheckedChange={onChange}
isChecked={value}
>
Lockout
</Switch>
</FormControl>
);
}}
/>
<div className="flex flex-col gap-2">
<Controller
control={control}
name="lockoutThreshold"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
label="Lockout Threshold"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="The amount of times login must fail before locking the identity auth method"
>
<Input
{...field}
placeholder="Enter lockout threshold..."
isDisabled={!lockoutEnabled}
/>
</FormControl>
);
}}
/>
<div className="flex items-end gap-2">
<Controller
control={control}
name="lockoutDurationValue"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
label="Lockout Duration"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="How long an identity auth method lockout lasts"
>
<Input
{...field}
placeholder="Enter lockout duration..."
isDisabled={!lockoutEnabled}
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name="lockoutDurationUnit"
render={({ field, fieldState: { error } }) => (
<FormControl
className={`mb-0 ${lockoutEnabled ? "" : "opacity-70"}`}
isError={Boolean(error)}
errorText={error?.message}
>
<Select
isDisabled={!lockoutEnabled}
value={field.value}
className="min-w-32 pr-2"
onValueChange={field.onChange}
position="popper"
>
<SelectItem
value="s"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Seconds</div>
</SelectItem>
<SelectItem
value="m"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Minutes</div>
</SelectItem>
<SelectItem
value="h"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Hours</div>
</SelectItem>
<SelectItem
value="d"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Days</div>
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex items-end gap-2">
<Controller
control={control}
name="lockoutCounterResetValue"
render={({ field, fieldState: { error } }) => {
return (
<FormControl
className={`mb-0 flex-grow ${lockoutEnabled ? "" : "opacity-70"}`}
label="Lockout Counter Reset"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
>
<Input
{...field}
placeholder="Enter lockout counter reset..."
isDisabled={!lockoutEnabled}
/>
</FormControl>
);
}}
/>
<Controller
control={control}
name="lockoutCounterResetUnit"
render={({ field, fieldState: { error } }) => (
<FormControl
className={`mb-0 ${lockoutEnabled ? "" : "opacity-70"}`}
isError={Boolean(error)}
errorText={error?.message}
>
<Select
isDisabled={!lockoutEnabled}
value={field.value}
className="min-w-32 pr-2"
onValueChange={field.onChange}
position="popper"
>
<SelectItem
value="s"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Seconds</div>
</SelectItem>
<SelectItem
value="m"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Minutes</div>
</SelectItem>
<SelectItem
value="h"
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">Hours</div>
</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
</div>
</div>
</TabPanel>
);
};
@@ -0,0 +1,67 @@
import ms from "ms";
import { z } from "zod";
export function superRefineLockout(
data: {
lockoutDurationValue: string;
lockoutCounterResetValue: string;
lockoutDurationUnit: "s" | "m" | "h" | "d";
lockoutCounterResetUnit: "s" | "m" | "h";
lockoutEnabled: boolean;
},
ctx: z.RefinementCtx
) {
const {
lockoutDurationValue,
lockoutCounterResetValue,
lockoutDurationUnit,
lockoutCounterResetUnit,
lockoutEnabled
} = data;
if (lockoutEnabled) {
let isAnyParseError = false;
const parsedLockoutDuration = parseInt(lockoutDurationValue, 10);
if (Number.isNaN(parsedLockoutDuration)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout duration must be a number",
path: ["lockoutDurationValue"]
});
isAnyParseError = true;
}
const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10);
if (Number.isNaN(parsedLockoutCounterReset)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout counter reset must be a number",
path: ["lockoutCounterResetValue"]
});
isAnyParseError = true;
}
if (!isAnyParseError) {
const lockoutDurationInSeconds = ms(`${parsedLockoutDuration}${lockoutDurationUnit}`) / 1000;
const lockoutCounterResetInSeconds =
ms(`${parsedLockoutCounterReset}${lockoutCounterResetUnit}`) / 1000;
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout duration must be between 30 seconds and 1 day",
path: ["lockoutDurationValue"]
});
}
if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Lockout counter reset must be between 5 seconds and 1 hour",
path: ["lockoutCounterResetValue"]
});
}
}
}
}
@@ -21,10 +21,10 @@ import {
useAddUsersToOrg,
useFetchServerStatus,
useGetOrgRoles,
useGetUserWorkspaces
useGetUserProjects
} from "@app/hooks/api";
import { ProjectType, ProjectVersion } from "@app/hooks/api/projects/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { OrgInviteLink } from "./OrgInviteLink";
@@ -76,7 +76,7 @@ export const AddOrgMemberModal = ({
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { data: projects, isPending: isProjectsLoading } = useGetUserWorkspaces({
const { data: projects, isPending: isProjectsLoading } = useGetUserProjects({
includeRoles: true
});
@@ -1,24 +1,17 @@
import { Helmet } from "react-helmet";
import { faArrowUpRightFromSquare, faBookOpen, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, PageHeader } from "@app/components/v2";
import { PageHeader } from "@app/components/v2";
import {
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import {
AddAppConnectionModal,
AppConnectionsTable
} from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
import { AppConnectionsTable } from "@app/pages/organization/AppConnections/AppConnectionsPage/components";
export const AppConnectionsPage = withPermission(
() => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["addConnection"] as const);
return (
<div className="bg-bunker-800">
<Helmet>
@@ -30,54 +23,18 @@ export const AppConnectionsPage = withPermission(
<div className="w-full max-w-7xl">
<PageHeader
className="w-full"
title={
<div className="flex w-full items-center">
<span>App Connections</span>
<a
className="-mt-1.5"
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Create}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
handlePopUpOpen("addConnection");
}}
isDisabled={!isAllowed}
className="ml-auto"
>
Add Connection
</Button>
)}
</OrgPermissionCan>
</div>
}
description="Create and configure connections with third-party apps for re-use across Infisical projects"
title="App Connections"
description="Manage organization App Connections"
/>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<AppConnectionsTable />
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
/>
<div className="mb-4 flex w-full flex-col rounded-md border border-blue-500/50 bg-blue-500/30 px-4 py-2 text-sm text-blue-200">
<div className="flex items-center">
<FontAwesomeIcon icon={faInfoCircle} className="mb-0.5 mr-2 text-sm" />
<span className="text-sm text-blue-200">
App connections can also be created and managed independently in projects now.
</span>
</div>
</div>
<AppConnectionsTable />
</div>
</div>
</div>
@@ -3,6 +3,7 @@ import { useState } from "react";
import { Modal, ModalContent } from "@app/components/v2";
import { TAppConnection } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { ProjectType } from "@app/hooks/api/projects/types";
import { AppConnectionForm } from "./AppConnectionForm";
import { AppConnectionsSelect } from "./AppConnectionList";
@@ -10,29 +11,44 @@ import { AppConnectionsSelect } from "./AppConnectionList";
type Props = {
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
projectId?: string;
projectType?: ProjectType;
app?: AppConnection;
onComplete?: (appConnection: TAppConnection) => void;
};
type ContentProps = {
onComplete: (appConnection: TAppConnection) => void;
projectId?: string;
projectType?: ProjectType;
app?: AppConnection;
};
const Content = ({ onComplete }: ContentProps) => {
const Content = ({ onComplete, projectId, projectType, app }: ContentProps) => {
const [selectedApp, setSelectedApp] = useState<AppConnection | null>(null);
if (selectedApp) {
if (app ?? selectedApp) {
return (
<AppConnectionForm
onComplete={onComplete}
onBack={() => setSelectedApp(null)}
app={selectedApp}
onBack={app ? undefined : () => setSelectedApp(null)}
app={(app ?? selectedApp)!}
projectId={projectId}
/>
);
}
return <AppConnectionsSelect onSelect={setSelectedApp} />;
return <AppConnectionsSelect onSelect={setSelectedApp} projectType={projectType} />;
};
export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
export const AddAppConnectionModal = ({
isOpen,
onOpenChange,
projectId,
projectType,
app,
onComplete
}: Props) => {
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
@@ -40,7 +56,15 @@ export const AddAppConnectionModal = ({ isOpen, onOpenChange }: Props) => {
title="Add Connection"
subTitle="Select a third-party app to connect to."
>
<Content onComplete={() => onOpenChange(false)} />
<Content
projectId={projectId}
projectType={projectType}
app={app}
onComplete={(appConnection) => {
if (onComplete) onComplete(appConnection);
onOpenChange(false);
}}
/>
</ModalContent>
</Modal>
);
@@ -52,12 +52,15 @@ type FormProps = {
onComplete: (appConnection: TAppConnection) => void;
} & ({ appConnection: TAppConnection } | { app: AppConnection });
type CreateFormProps = FormProps & { app: AppConnection };
type CreateFormProps = FormProps & {
app: AppConnection;
projectId?: string;
};
type UpdateFormProps = FormProps & {
appConnection: TAppConnection;
};
const CreateForm = ({ app, onComplete }: CreateFormProps) => {
const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => {
const createAppConnection = useCreateAppConnection();
const { name: appName } = APP_CONNECTION_MAP[app];
@@ -68,7 +71,10 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
>
) => {
try {
const connection = await createAppConnection.mutateAsync(formData);
const connection = await createAppConnection.mutateAsync({
...formData,
projectId
});
createNotification({
text: `Successfully added ${appName} Connection`,
type: "success"
@@ -88,15 +94,15 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm />;
return <GitHubConnectionForm projectId={projectId} />;
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm />;
return <GitHubRadarConnectionForm projectId={projectId} />;
case AppConnection.GCP:
return <GcpConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} />;
return <AzureKeyVaultConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureAppConfiguration:
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} />;
return <AzureAppConfigurationConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm onSubmit={onSubmit} />;
case AppConnection.Databricks:
@@ -118,9 +124,9 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} />;
return <AzureClientSecretsConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm onSubmit={onSubmit} />;
return <AzureDevOpsConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} />;
case AppConnection.Auth0:
@@ -142,7 +148,7 @@ const CreateForm = ({ app, onComplete }: CreateFormProps) => {
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} />;
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} />;
return <GitLabConnectionForm onSubmit={onSubmit} projectId={projectId} />;
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} />;
case AppConnection.Bitbucket:
@@ -200,16 +206,33 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.AWS:
return <AwsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.GitHub:
return <GitHubConnectionForm appConnection={appConnection} />;
return (
<GitHubConnectionForm appConnection={appConnection} projectId={appConnection.projectId} />
);
case AppConnection.GitHubRadar:
return <GitHubRadarConnectionForm appConnection={appConnection} />;
return (
<GitHubRadarConnectionForm
appConnection={appConnection}
projectId={appConnection.projectId}
/>
);
case AppConnection.GCP:
return <GcpConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
case AppConnection.AzureKeyVault:
return <AzureKeyVaultConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureKeyVaultConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureAppConfiguration:
return (
<AzureAppConfigurationConnectionForm appConnection={appConnection} onSubmit={onSubmit} />
<AzureAppConfigurationConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureADCS:
return <AzureADCSConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
@@ -232,9 +255,21 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Camunda:
return <CamundaConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.AzureClientSecrets:
return <AzureClientSecretsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureClientSecretsConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.AzureDevOps:
return <AzureDevOpsConnectionForm appConnection={appConnection} onSubmit={onSubmit} />;
return (
<AzureDevOpsConnectionForm
appConnection={appConnection}
onSubmit={onSubmit}
projectId={appConnection.projectId}
/>
);
case AppConnection.Windmill:
return <WindmillConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Auth0:
@@ -256,7 +291,13 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
case AppConnection.Flyio:
return <FlyioConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.GitLab:
return <GitLabConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
return (
<GitLabConnectionForm
onSubmit={onSubmit}
appConnection={appConnection}
projectId={appConnection.projectId}
/>
);
case AppConnection.Cloudflare:
return <CloudflareConnectionForm onSubmit={onSubmit} appConnection={appConnection} />;
case AppConnection.Bitbucket:
@@ -278,12 +319,12 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => {
}
};
type Props = { onBack?: () => void } & Pick<FormProps, "onComplete"> &
type Props = { onBack?: () => void; projectId?: string } & Pick<FormProps, "onComplete"> &
(
| { app: AppConnection; appConnection?: undefined }
| { app?: undefined; appConnection: TAppConnection }
);
export const AppConnectionForm = ({ onBack, ...props }: Props) => {
export const AppConnectionForm = ({ onBack, projectId, ...props }: Props) => {
const { app, appConnection } = props;
return (
@@ -296,7 +337,7 @@ export const AppConnectionForm = ({ onBack, ...props }: Props) => {
{appConnection ? (
<UpdateForm {...props} appConnection={appConnection} />
) : (
<CreateForm {...props} app={app} />
<CreateForm {...props} app={app} projectId={projectId} />
)}
</div>
);
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureAppConfigurationConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureAppConfigurationFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureAppConfigurationConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,11 @@ const getDefaultValues = (appConnection?: TAzureAppConfigurationConnection): Par
return base;
};
export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureAppConfigurationConnectionForm = ({
appConnection,
onSubmit,
projectId
}: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +120,8 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -128,7 +140,12 @@ export const AzureAppConfigurationConnectionForm = ({ appConnection, onSubmit }:
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureAppConfigurationConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureAppConfigurationFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://azconfig.io/.default%20openid%20offline_access&state=${state}<:>azure-app-configuration`
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureClientSecretsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureClientSecretsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -26,6 +31,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureClientSecretsConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -97,7 +103,7 @@ const getDefaultValues = (appConnection?: TAzureClientSecretsConnection): Partia
return base;
};
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -111,6 +117,8 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureClientSecretsConnectionForm = ({ appConnection, onSubmit }: Pr
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureClientSecretsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureClientSecretsFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://graph.microsoft.com/.default%20openid%20offline_access&state=${state}<:>azure-client-secrets`
@@ -7,7 +7,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
AzureDevOpsConnectionMethod,
@@ -16,6 +20,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { AzureDevOpsFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -65,6 +70,7 @@ type OnSubmitForm = z.infer<typeof accessTokenSchema> | z.infer<typeof clientSec
type Props = {
appConnection?: TAzureDevOpsConnection;
onSubmit: (formData: OnSubmitForm) => Promise<void>;
projectId: string | undefined | null;
};
const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormData> => {
@@ -132,7 +138,7 @@ const getDefaultValues = (appConnection?: TAzureDevOpsConnection): Partial<FormD
return base;
};
export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -146,6 +152,8 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -164,7 +172,12 @@ export const AzureDevOpsConnectionForm = ({ appConnection, onSubmit }: Props) =>
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureDevOpsConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureDevOpsFormData)
);
window.location.assign(
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -15,6 +19,7 @@ import {
TAzureKeyVaultConnection
} from "@app/hooks/api/appConnections/types/azure-key-vault-connection";
import { AzureKeyVaultFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -25,6 +30,7 @@ type ClientSecretForm = z.infer<typeof clientSecretSchema>;
type Props = {
appConnection?: TAzureKeyVaultConnection;
onSubmit: (formData: ClientSecretForm) => Promise<void>;
projectId: string | undefined | null;
};
const baseSchema = genericAppConnectionFieldsSchema.extend({
@@ -96,7 +102,7 @@ const getDefaultValues = (appConnection?: TAzureKeyVaultConnection): Partial<For
return base;
};
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props) => {
export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,6 +116,8 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
defaultValues: getDefaultValues(appConnection)
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -129,7 +137,12 @@ export const AzureKeyVaultConnectionForm = ({ appConnection, onSubmit }: Props)
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"azureKeyVaultConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as AzureKeyVaultFormData)
);
window.location.assign(
`https://login.microsoftonline.com/${formData.tenantId || "common"}/oauth2/v2.0/authorize?client_id=${oauthClientId}&response_type=code&redirect_uri=${window.location.origin}/organization/app-connections/azure/oauth/callback&response_mode=query&scope=https://vault.azure.net/.default%20openid%20offline_access&state=${state}<:>azure-key-vault`
@@ -25,7 +25,11 @@ import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { gatewaysQueryKeys } from "@app/hooks/api";
import {
@@ -35,6 +39,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { GithubFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -42,6 +47,7 @@ import {
type Props = {
appConnection?: TGitHubConnection;
projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -63,7 +69,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer<typeof formSchema>;
export const GitHubConnectionForm = ({ appConnection }: Props) => {
export const GitHubConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,13 +104,21 @@ export const GitHubConnectionForm = ({ appConnection }: Props) => {
const selectedMethod = watch("method");
const instanceType = watch("credentials.instanceType");
const returnUrl = useGetAppConnectionOauthReturnUrl();
const onSubmit = (formData: FormData) => {
setIsRedirecting(true);
const state = crypto.randomBytes(16).toString("hex");
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
credentials: formData.credentials as TGitHubConnection["credentials"],
connectionId: appConnection?.id,
projectId,
returnUrl
} as GithubFormData)
);
const githubHost =
@@ -6,7 +6,11 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button, FormControl, ModalClose, Select, SelectItem } from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import {
GitHubRadarConnectionMethod,
@@ -15,6 +19,7 @@ import {
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { GithubRadarFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -22,6 +27,7 @@ import {
type Props = {
appConnection?: TGitHubRadarConnection;
projectId: string | undefined | null;
};
const formSchema = genericAppConnectionFieldsSchema.extend({
@@ -31,7 +37,7 @@ const formSchema = genericAppConnectionFieldsSchema.extend({
type FormData = z.infer<typeof formSchema>;
export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
export const GitHubRadarConnectionForm = ({ appConnection, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -48,6 +54,8 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
}
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -63,7 +71,12 @@ export const GitHubRadarConnectionForm = ({ appConnection }: Props) => {
localStorage.setItem("latestCSRFToken", state);
localStorage.setItem(
"githubRadarConnectionFormData",
JSON.stringify({ ...formData, connectionId: appConnection?.id })
JSON.stringify({
...formData,
connectionId: appConnection?.id,
projectId,
returnUrl
} as GithubRadarFormData)
);
switch (formData.method) {
@@ -16,7 +16,11 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
APP_CONNECTION_MAP,
getAppConnectionMethodDetails,
useGetAppConnectionOauthReturnUrl
} from "@app/helpers/appConnections";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useGetAppConnectionOption } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
@@ -26,6 +30,7 @@ import {
TGitLabConnection
} from "@app/hooks/api/appConnections/types/gitlab-connection";
import { GitLabFormData } from "../../../OauthCallbackPage/OauthCallbackPage.types";
import {
genericAppConnectionFieldsSchema,
GenericAppConnectionsFields
@@ -34,6 +39,7 @@ import {
type Props = {
appConnection?: TGitLabConnection;
onSubmit: (formData: FormData) => Promise<void>;
projectId: string | undefined | null;
};
const formSchema = z.discriminatedUnion("method", [
@@ -72,7 +78,7 @@ const formSchema = z.discriminatedUnion("method", [
type FormData = z.infer<typeof formSchema>;
export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Props) => {
export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit, projectId }: Props) => {
const isUpdate = Boolean(appConnection);
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -98,6 +104,8 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
} as FormData))
});
const returnUrl = useGetAppConnectionOauthReturnUrl();
const {
handleSubmit,
control,
@@ -132,8 +140,10 @@ export const GitLabConnectionForm = ({ appConnection, onSubmit: formSubmit }: Pr
JSON.stringify({
...formData,
connectionId: appConnection?.id,
isUpdate
})
isUpdate,
projectId,
returnUrl
} as GitLabFormData)
);
// Redirect to Gitlab OAuth
@@ -9,14 +9,16 @@ import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { ProjectType } from "@app/hooks/api/projects/types";
type Props = {
onSelect: (app: AppConnection) => void;
projectType?: ProjectType;
};
export const AppConnectionsSelect = ({ onSelect }: Props) => {
export const AppConnectionsSelect = ({ onSelect, projectType }: Props) => {
const { subscription } = useSubscription();
const { isPending, data: appConnectionOptions } = useAppConnectionOptions();
const { isPending, data: appConnectionOptions } = useAppConnectionOptions(projectType);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -1,19 +1,23 @@
import { useCallback } from "react";
import { subject } from "@casl/ability";
import {
faAsterisk,
faBuilding,
faCheck,
faCopy,
faEdit,
faEllipsisV,
faInfoCircle,
faServer,
faTable,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { VariablePermissionCan } from "@app/components/permissions";
import {
Badge,
DropdownMenu,
@@ -25,9 +29,11 @@ import {
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects } from "@app/context";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { getProjectBaseURL } from "@app/helpers/project";
import { useToggle } from "@app/hooks";
import { TAppConnection } from "@app/hooks/api/appConnections";
@@ -36,15 +42,18 @@ type Props = {
onDelete: (appConnection: TAppConnection) => void;
onEditCredentials: (appConnection: TAppConnection) => void;
onEditDetails: (appConnection: TAppConnection) => void;
isProjectView: boolean;
};
export const AppConnectionRow = ({
appConnection,
onDelete,
onEditCredentials,
onEditDetails
onEditDetails,
isProjectView
}: Props) => {
const { id, name, method, app, description, isPlatformManagedCredentials } = appConnection;
const { id, name, method, app, description, isPlatformManagedCredentials, project } =
appConnection;
const [isIdCopied, setIsIdCopied] = useToggle(false);
@@ -111,7 +120,38 @@ export const AppConnectionRow = ({
{methodDetails.name}
</p>
</Td>
{!isProjectView && (
<Td className="!min-w-[8rem] max-w-0">
{project ? (
<Link
// @ts-expect-error app-connections aren't in kms/ssh
to={`${getProjectBaseURL(project.type)}/app-connections`}
params={{
projectId: project.id
}}
className="underline"
>
<p className="truncate">
<FontAwesomeIcon
size="sm"
className="mr-1.5 text-mineshaft-300/75"
icon={faTable}
/>
{project.name}
</p>
</Link>
) : (
<p className="truncate">
<FontAwesomeIcon
size="sm"
className="mr-1.5 text-mineshaft-300/75"
icon={faBuilding}
/>
Organization
</p>
)}
</Td>
)}
<Td>
<div className="flex items-center justify-end gap-2">
{isPlatformManagedCredentials && (
@@ -143,48 +183,91 @@ export const AppConnectionRow = ({
>
Copy Connection ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
{(isProjectView || !project) && (
<>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Edit
: OrgPermissionAppConnectionActions.Edit
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Edit}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => onEditDetails(appConnection)}
>
Edit Details
</DropdownMenuItem>
)}
</VariablePermissionCan>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Edit
: OrgPermissionAppConnectionActions.Edit
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
{isPlatformManagedCredentials ? "View" : "Edit"} Credentials
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faAsterisk} />}
onClick={() => onEditCredentials(appConnection)}
>
{isPlatformManagedCredentials ? "View" : "Edit"} Credentials
</DropdownMenuItem>
)}
</VariablePermissionCan>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Delete
: OrgPermissionAppConnectionActions.Delete
}
a={
isProjectView
? subject(ProjectPermissionSub.AppConnections, {
connectionId: id
})
: subject(OrgPermissionSubjects.AppConnections, {
connectionId: id
})
}
>
Delete Connection
</DropdownMenuItem>
)}
</OrgPermissionCan>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
onClick={() => onDelete(appConnection)}
>
Delete Connection
</DropdownMenuItem>
)}
</VariablePermissionCan>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
@@ -2,16 +2,21 @@ import { useMemo, useState } from "react";
import {
faArrowDown,
faArrowUp,
faArrowUpRightFromSquare,
faBookOpen,
faCheckCircle,
faFilter,
faMagnifyingGlass,
faPlug,
faPlus,
faSearch
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { VariablePermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -29,6 +34,9 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects, ProjectPermissionSub } from "@app/context";
import { OrgPermissionAppConnectionActions } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionAppConnectionActions } from "@app/context/ProjectPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import {
getUserTablePreference,
@@ -39,7 +47,9 @@ import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import { TAppConnection, useListAppConnections } from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { AddAppConnectionModal } from "./AddAppConnectionModal";
import { AppConnectionRow } from "./AppConnectionRow";
import { DeleteAppConnectionModal } from "./DeleteAppConnectionModal";
import { EditAppConnectionCredentialsModal } from "./EditAppConnectionCredentialsModal";
@@ -48,22 +58,45 @@ import { EditAppConnectionDetailsModal } from "./EditAppConnectionDetailsModal";
enum AppConnectionsOrderBy {
App = "app",
Name = "name",
Method = "method"
Method = "method",
ManagedBy = "managed-by"
}
type AppConnectionFilters = {
apps: AppConnection[];
};
export const AppConnectionsTable = () => {
const { isPending, data: appConnections = [] } = useListAppConnections();
enum View {
All = "all",
Scope = "scope"
}
const APP_CONNECTION_VIEW_STORAGE_KEY = "app-connection-view";
type Props = {
projectId?: string;
projectType?: ProjectType;
};
export const AppConnectionsTable = ({ projectId, projectType }: Props) => {
const isProjectView = Boolean(projectId);
const { isPending, data: appConnections = [] } = useListAppConnections(projectId);
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"addConnection",
"deleteConnection",
"editCredentials",
"editDetails"
] as const);
const [view, setView] = useState<View>(() => {
const storedView = localStorage.getItem(APP_CONNECTION_VIEW_STORAGE_KEY) as View | null;
if (storedView && Object.values(View).includes(storedView)) return storedView;
return View.Scope;
});
const [filters, setFilters] = useState<AppConnectionFilters>({
apps: []
});
@@ -96,6 +129,10 @@ export const AppConnectionsTable = () => {
.filter((appConnection) => {
const { app, method, name } = appConnection;
if (view === View.Scope && !isProjectView && appConnection.projectId) {
return false;
}
if (filters.apps.length && !filters.apps.includes(app)) return false;
const searchValue = search.trim().toLowerCase();
@@ -121,6 +158,13 @@ export const AppConnectionsTable = () => {
.localeCompare(
getAppConnectionMethodDetails(connectionTwo.method).name.toLowerCase()
);
case AppConnectionsOrderBy.ManagedBy:
if (!connectionOne.project) return 1;
if (!connectionTwo.project) return -1;
return connectionOne.project.name
.toLowerCase()
.localeCompare(connectionTwo.project.name.toLowerCase());
case AppConnectionsOrderBy.App:
default:
return APP_CONNECTION_MAP[connectionOne.app].name
@@ -128,7 +172,7 @@ export const AppConnectionsTable = () => {
.localeCompare(APP_CONNECTION_MAP[connectionTwo.app].name.toLowerCase());
}
}),
[appConnections, orderDirection, search, orderBy, filters]
[appConnections, orderDirection, search, orderBy, filters, view]
);
useResetPageHelper({
@@ -165,8 +209,87 @@ export const AppConnectionsTable = () => {
handlePopUpOpen("editDetails", appConnection);
return (
<div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">App Connections</p>
<a
href="https://infisical.com/docs/integrations/app-connections/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.12rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<p className="text-sm text-bunker-300">
Create and configure connections with third-party apps for re-use across your project
{isProjectView ? "" : "s"}.
</p>
</div>
<VariablePermissionCan
type={isProjectView ? "project" : "org"}
I={
isProjectView
? ProjectPermissionAppConnectionActions.Create
: OrgPermissionAppConnectionActions.Create
}
a={
isProjectView
? ProjectPermissionSub.AppConnections
: OrgPermissionSubjects.AppConnections
}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("addConnection")}
isDisabled={!isAllowed}
>
Add Connection
</Button>
)}
</VariablePermissionCan>
</div>
<div className="flex gap-2">
{!isProjectView && (
<div className="flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
<Button
variant="outline_bg"
onClick={() => {
setView(View.Scope);
localStorage.setItem(APP_CONNECTION_VIEW_STORAGE_KEY, View.Scope);
}}
size="xs"
className={`${
view === View.Scope ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[5rem] rounded border-none hover:bg-mineshaft-600`}
>
Organization
</Button>
<Button
variant="outline_bg"
onClick={() => {
setView(View.All);
localStorage.setItem(APP_CONNECTION_VIEW_STORAGE_KEY, View.All);
}}
size="xs"
className={`${
view === View.All ? "bg-mineshaft-500" : "bg-transparent"
} min-w-[5rem] rounded border-none hover:bg-mineshaft-600`}
>
All
</Button>
</div>
)}
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
@@ -188,38 +311,46 @@ export const AppConnectionsTable = () => {
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuContent
sideOffset={2}
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
align="end"
>
<DropdownMenuLabel>Filter by Apps</DropdownMenuLabel>
{appConnections.length ? (
[...new Set(appConnections.map(({ app }) => app))].map((app) => (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
apps: prev.apps.includes(app)
? prev.apps.filter((a) => a !== app)
: [...prev.apps, app]
}));
}}
key={app}
icon={
filters.apps.includes(app) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${APP_CONNECTION_MAP[app].name} integration`}
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
className="h-4 w-4"
/>
<span>{APP_CONNECTION_MAP[app].name}</span>
</div>
</DropdownMenuItem>
))
[...new Set(appConnections.map(({ app }) => app))]
.sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
})
.map((app) => (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setFilters((prev) => ({
...prev,
apps: prev.apps.includes(app)
? prev.apps.filter((a) => a !== app)
: [...prev.apps, app]
}));
}}
key={app}
icon={
filters.apps.includes(app) && (
<FontAwesomeIcon className="text-primary" icon={faCheckCircle} />
)
}
iconPos="right"
>
<div className="flex items-center gap-2">
<img
alt={`${APP_CONNECTION_MAP[app].name} integration`}
src={`/images/integrations/${APP_CONNECTION_MAP[app].image}`}
className="h-4 w-4"
/>
<span>{APP_CONNECTION_MAP[app].name}</span>
</div>
</DropdownMenuItem>
))
) : (
<DropdownMenuItem isDisabled>No Connections Configured</DropdownMenuItem>
)}
@@ -269,7 +400,21 @@ export const AppConnectionsTable = () => {
</IconButton>
</div>
</Th>
{!isProjectView && (
<Th>
<div className="flex items-center">
Managed By
<IconButton
variant="plain"
className={getClassName(AppConnectionsOrderBy.ManagedBy)}
ariaLabel="sort"
onClick={() => handleSort(AppConnectionsOrderBy.ManagedBy)}
>
<FontAwesomeIcon icon={getColSortIcon(AppConnectionsOrderBy.ManagedBy)} />
</IconButton>
</div>
</Th>
)}
<Th className="w-5" />
</Tr>
</THead>
@@ -284,6 +429,7 @@ export const AppConnectionsTable = () => {
onDelete={handleDelete}
onEditCredentials={handleEditCredentials}
onEditDetails={handleEditDetails}
isProjectView={isProjectView}
/>
))}
</TBody>
@@ -323,6 +469,12 @@ export const AppConnectionsTable = () => {
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
appConnection={popUp.editDetails.data}
/>
<AddAppConnectionModal
isOpen={popUp.addConnection.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addConnection", isOpen)}
projectId={projectId}
projectType={projectType}
/>
</div>
);
};
@@ -12,73 +12,14 @@ import {
AzureKeyVaultConnectionMethod,
GitHubConnectionMethod,
GitLabConnectionMethod,
TAzureAppConfigurationConnection,
TAzureClientSecretsConnection,
TAzureDevOpsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
TGitLabConnection,
TAppConnection,
useCreateAppConnection,
useUpdateAppConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
import { IntegrationsListPageTabs } from "@app/types/integrations";
type BaseFormData = {
returnUrl?: string;
connectionId?: string;
isUpdate?: boolean;
};
type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
type GitLabFormData = BaseFormData &
Pick<TGitLabConnection, "name" | "method" | "description" | "credentials">;
type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
type AzureAppConfigurationFormData = BaseFormData &
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
type AzureClientSecretsFormData = BaseFormData &
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
type OAuthCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.OAuth }
>["credentials"];
type AccessTokenCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.AccessToken }
>["credentials"];
type AzureDevOpsFormData = BaseFormData &
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<AccessTokenCredentials, "orgName">);
type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
};
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
app: AppConnection.AzureClientSecrets;
};
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
app: AppConnection.AzureDevOps;
};
};
import { FormDataMap } from "./OauthCallbackPage.types";
const formDataStorageFieldMap: Partial<Record<AppConnection, string>> = {
[AppConnection.GitHub]: "githubConnectionFormData",
@@ -148,11 +89,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitLab);
const { connectionId, name, description, returnUrl, isUpdate, credentials } = formData;
const { connectionId, name, description, returnUrl, isUpdate, projectId, credentials } =
formData;
let connection: TAppConnection;
try {
if (isUpdate && connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitLab,
connectionId,
credentials: {
@@ -161,10 +105,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitLab,
name,
description,
projectId,
method: GitLabConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -173,14 +118,12 @@ export const OAuthCallbackPage = () => {
});
}
navigate({
to: returnUrl ?? "/organization/app-connections"
});
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
} catch (err: any) {
createNotification({
@@ -189,7 +132,10 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
@@ -201,11 +147,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureKeyVault);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
connectionId,
credentials: {
@@ -214,10 +162,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureKeyVault,
name,
description,
projectId,
method: AzureKeyVaultConnectionMethod.OAuth,
credentials: {
tenantId: formData.tenantId,
@@ -232,14 +181,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -249,11 +204,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureAppConfiguration);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
connectionId,
credentials: {
@@ -262,10 +219,11 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureAppConfiguration,
name,
description,
projectId,
method: AzureAppConfigurationConnectionMethod.OAuth,
credentials: {
code: code as string,
@@ -280,14 +238,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -297,11 +261,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureClientSecrets);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
connectionId,
credentials: {
@@ -310,11 +276,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureClientSecrets,
name,
description,
method: AzureClientSecretsConnectionMethod.OAuth,
projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId
@@ -328,14 +295,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -345,7 +318,9 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.AzureDevOps);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (!("tenantId" in formData)) {
@@ -353,7 +328,7 @@ export const OAuthCallbackPage = () => {
}
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
connectionId,
credentials: {
@@ -363,11 +338,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.AzureDevOps,
name,
description,
method: AzureDevOpsConnectionMethod.OAuth,
projectId,
credentials: {
code: code as string,
tenantId: formData.tenantId as string,
@@ -382,14 +358,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -399,11 +381,14 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHub);
const { connectionId, name, description, returnUrl, gatewayId, credentials } = formData;
const { connectionId, name, description, returnUrl, gatewayId, credentials, projectId } =
formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHub,
...(installationId
? {
@@ -427,10 +412,11 @@ export const OAuthCallbackPage = () => {
})
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHub,
name,
description,
projectId,
...(installationId
? {
method: GitHubConnectionMethod.App,
@@ -460,14 +446,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -477,11 +469,13 @@ export const OAuthCallbackPage = () => {
clearState(AppConnection.GitHubRadar);
const { connectionId, name, description, returnUrl } = formData;
const { connectionId, name, description, returnUrl, projectId } = formData;
let connection: TAppConnection;
try {
if (connectionId) {
await updateAppConnection.mutateAsync({
connection = await updateAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
connectionId,
credentials: {
@@ -490,11 +484,12 @@ export const OAuthCallbackPage = () => {
}
});
} else {
await createAppConnection.mutateAsync({
connection = await createAppConnection.mutateAsync({
app: AppConnection.GitHubRadar,
name,
description,
method: GitHubConnectionMethod.App,
projectId,
credentials: {
code: code as string,
installationId: installationId as string
@@ -508,14 +503,20 @@ export const OAuthCallbackPage = () => {
type: "error"
});
navigate({
to: returnUrl ?? "/organization/app-connections"
to: returnUrl,
params: {
projectId
}
});
return null;
}
return {
connectionId,
returnUrl,
appConnectionName: formData.app
appConnectionName: formData.app,
projectId,
connection
};
}, []);
@@ -530,8 +531,13 @@ export const OAuthCallbackPage = () => {
if (!isReady) return;
(async () => {
let data: { connectionId?: string; returnUrl?: string; appConnectionName?: string } | null =
null;
let data: {
returnUrl: string;
appConnectionName: string;
connectionId?: string;
projectId?: string;
connection: TAppConnection;
} | null = null;
if (appConnection === AppConnection.GitHub) {
data = await handleGithub();
@@ -554,16 +560,26 @@ export const OAuthCallbackPage = () => {
text: `Successfully ${data.connectionId ? "updated" : "added"} ${data.appConnectionName ? APP_CONNECTION_MAP[data.appConnectionName as AppConnection].name : ""} Connection`,
type: "success"
});
} else {
createNotification({
text: "Failed to add connection",
type: "error"
await navigate({
to: data.returnUrl,
params: {
projectId: data.projectId ?? undefined
},
// scott: if it's not an app connection page we need to pass connection details as it's an inline creation
search: data.returnUrl.includes("app-connections")
? undefined
: {
connectionId: data.connection.id,
connectionName: data.connection.name,
...(data.returnUrl.includes("integrations")
? {
selectedTab: IntegrationsListPageTabs.SecretSyncs
}
: {})
}
});
}
await navigate({
to: data?.returnUrl ?? "/organization/app-connections"
});
})();
}, [isReady]);
@@ -0,0 +1,68 @@
import {
AzureDevOpsConnectionMethod,
TAzureAppConfigurationConnection,
TAzureClientSecretsConnection,
TAzureDevOpsConnection,
TAzureKeyVaultConnection,
TGitHubConnection,
TGitHubRadarConnection,
TGitLabConnection
} from "@app/hooks/api/appConnections";
import { AppConnection } from "@app/hooks/api/appConnections/enums";
type BaseFormData = {
returnUrl: string;
connectionId?: string;
isUpdate?: boolean;
projectId: string;
};
export type GithubFormData = BaseFormData &
Pick<TGitHubConnection, "name" | "method" | "description" | "gatewayId" | "credentials">;
export type GithubRadarFormData = BaseFormData &
Pick<TGitHubRadarConnection, "name" | "method" | "description">;
export type GitLabFormData = BaseFormData &
Pick<TGitLabConnection, "name" | "method" | "description" | "credentials">;
export type AzureKeyVaultFormData = BaseFormData &
Pick<TAzureKeyVaultConnection, "name" | "method" | "description"> &
Pick<TAzureKeyVaultConnection["credentials"], "tenantId">;
export type AzureAppConfigurationFormData = BaseFormData &
Pick<TAzureAppConfigurationConnection, "name" | "method" | "description"> &
Pick<TAzureAppConfigurationConnection["credentials"], "tenantId">;
export type AzureClientSecretsFormData = BaseFormData &
Pick<TAzureClientSecretsConnection, "name" | "method" | "description"> &
Pick<TAzureClientSecretsConnection["credentials"], "tenantId">;
type OAuthCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.OAuth }
>["credentials"];
type AccessTokenCredentials = Extract<
TAzureDevOpsConnection,
{ method: AzureDevOpsConnectionMethod.AccessToken }
>["credentials"];
export type AzureDevOpsFormData = BaseFormData &
Pick<TAzureDevOpsConnection, "name" | "method" | "description"> &
(Pick<OAuthCredentials, "tenantId" | "orgName"> | Pick<AccessTokenCredentials, "orgName">);
export type FormDataMap = {
[AppConnection.GitHub]: GithubFormData & { app: AppConnection.GitHub };
[AppConnection.GitHubRadar]: GithubRadarFormData & { app: AppConnection.GitHubRadar };
[AppConnection.GitLab]: GitLabFormData & { app: AppConnection.GitLab };
[AppConnection.AzureKeyVault]: AzureKeyVaultFormData & { app: AppConnection.AzureKeyVault };
[AppConnection.AzureAppConfiguration]: AzureAppConfigurationFormData & {
app: AppConnection.AzureAppConfiguration;
};
[AppConnection.AzureClientSecrets]: AzureClientSecretsFormData & {
app: AppConnection.AzureClientSecrets;
};
[AppConnection.AzureDevOps]: AzureDevOpsFormData & {
app: AppConnection.AzureDevOps;
};
};
@@ -21,7 +21,7 @@ import {
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useGetUserProjects } from "@app/hooks/api";
import {
eventToNameMap,
secretEvents,
@@ -29,7 +29,7 @@ import {
} from "@app/hooks/api/auditLogs/constants";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { UserAgentType } from "@app/hooks/api/auth/types";
import { Workspace } from "@app/hooks/api/workspace/types";
import { Project } from "@app/hooks/api/projects/types";
import { LogFilterItem } from "./LogFilterItem";
import { auditLogFilterFormSchema, Presets, TAuditLogFilterFormData } from "./types";
@@ -44,7 +44,7 @@ type Props = {
presets?: Presets;
setFilter: (data: TAuditLogFilterFormData) => void;
filter: TAuditLogFilterFormData;
project?: Workspace;
project?: Project;
};
const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
@@ -72,7 +72,7 @@ const getActiveFilterCount = (filter: TAuditLogFilterFormData) => {
};
export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
const { data: workspaces = [] } = useGetUserWorkspaces();
const { data: workspaces = [] } = useGetUserProjects();
const { currentOrg } = useOrganization();
const workspacesInOrg = workspaces.filter((ws) => ws.orgId === currentOrg?.id);
@@ -13,7 +13,7 @@ import {
} from "@app/context";
import { Timezone } from "@app/helpers/datetime";
import { withPermission, withProjectPermission } from "@app/hoc";
import { Workspace } from "@app/hooks/api/workspace/types";
import { Project } from "@app/hooks/api/projects/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { LogsDateFilter } from "./LogsDateFilter";
@@ -31,7 +31,7 @@ type Props = {
refetchInterval?: number;
showFilters?: boolean;
pageView?: boolean;
project?: Workspace;
project?: Project;
};
const LogsSectionComponent = ({
@@ -1,7 +1,7 @@
import { z } from "zod";
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectType } from "@app/hooks/api/projects/types";
export enum AuditLogDateFilterType {
Relative = "relative",
@@ -1,5 +1,7 @@
import { useEffect } from "react";
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
@@ -15,13 +17,15 @@ import {
useGetOrgPlanBillingInfo,
useGetOrgTrialUrl
} from "@app/hooks/api";
import { subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries";
import { usePopUp } from "@app/hooks/usePopUp";
import { ManagePlansModal } from "./ManagePlansModal";
export const PreviewSection = () => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { subscription } = useSubscription(true);
const queryClient = useQueryClient();
const { data, isPending } = useGetOrgPlanBillingInfo(currentOrg?.id ?? "");
const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession();
@@ -37,6 +41,12 @@ export const PreviewSection = () => {
return formattedTotal;
};
useEffect(() => {
queryClient.invalidateQueries({
queryKey: subscriptionQueryKeys.getOrgSubsription(currentOrg?.id ?? "")
});
}, []);
const formatDate = (date: number) => {
const createdDate = new Date(date * 1000);
const day: number = createdDate.getDate();
@@ -4,17 +4,17 @@ import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
faDoorClosed,
faEdit,
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
faPlug,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import { format, formatRelative } from "date-fns";
import { formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
@@ -48,13 +48,14 @@ import {
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
import { useDeleteGatewayV2ById } from "@app/hooks/api/gateways-v2";
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
export const GatewayListPage = withPermission(
() => {
const [search, setSearch] = useState("");
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteGateway",
@@ -62,16 +63,20 @@ export const GatewayListPage = withPermission(
] as const);
const deleteGatewayById = useDeleteGatewayById();
const deleteGatewayV2ById = useDeleteGatewayV2ById();
const handleDeleteGateway = async () => {
await deleteGatewayById.mutateAsync((popUp.deleteGateway.data as { id: string }).id, {
onSuccess: () => {
handlePopUpToggle("deleteGateway");
createNotification({
type: "success",
text: "Successfully delete gateway"
});
}
const data = popUp.deleteGateway.data as { id: string; isV1: boolean };
if (data.isV1) {
await deleteGatewayById.mutateAsync(data.id);
} else {
await deleteGatewayV2ById.mutateAsync(data.id);
}
handlePopUpToggle("deleteGateway");
createNotification({
type: "success",
text: "Successfully deleted gateway"
});
};
@@ -127,7 +132,6 @@ export const GatewayListPage = withPermission(
<THead>
<Tr>
<Th className="w-1/3">Name</Th>
<Th>Cert Issued At</Th>
<Th>Identity</Th>
<Th>
Health Check
@@ -143,13 +147,19 @@ export const GatewayListPage = withPermission(
</Tr>
</THead>
<TBody>
{isGatewayLoading && (
{isGatewaysLoading && (
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
)}
{filteredGateway?.map((el) => (
<Tr key={el.id}>
<Td>{el.name}</Td>
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
<Td>
<div className="flex items-center gap-2">
<span>{el.name}</span>
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
Gateway v{el.isV1 ? "1" : "2"}
</span>
</div>
</Td>
<Td>{el.identity.name}</Td>
<Td>
{el.heartbeat
@@ -176,20 +186,22 @@ export const GatewayListPage = withPermission(
>
Copy ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgGatewayPermissionActions.EditGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => handlePopUpOpen("editDetails", el)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
{el.isV1 && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.EditGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => handlePopUpOpen("editDetails", el)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
)}
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
@@ -224,14 +236,14 @@ export const GatewayListPage = withPermission(
/>
</ModalContent>
</Modal>
{!isGatewayLoading && !filteredGateway?.length && (
{!isGatewaysLoading && !filteredGateway?.length && (
<EmptyState
title={
gateways?.length
? "No Gateways match search..."
: "No Gateways have been configured"
}
icon={gateways?.length ? faSearch : faPlug}
icon={gateways?.length ? faSearch : faDoorClosed}
/>
)}
<DeleteActionModal
@@ -251,8 +263,5 @@ export const GatewayListPage = withPermission(
</div>
);
},
{
action: OrgPermissionAppConnectionActions.Read,
subject: OrgPermissionSubjects.AppConnections
}
{ action: OrgGatewayPermissionActions.ListGateways, subject: OrgPermissionSubjects.Gateway }
);
@@ -17,7 +17,7 @@ import {
useAddIdentityToWorkspace,
useGetIdentityProjectMemberships,
useGetProjectRoles,
useGetUserWorkspaces,
useGetUserProjects,
useGetWorkspaceById
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -44,7 +44,7 @@ type Props = {
const Content = ({ identityId, handlePopUpToggle }: Omit<Props, "popUp">) => {
const { currentOrg } = useOrganization();
const { data: workspaces = [] } = useGetUserWorkspaces();
const { data: workspaces = [] } = useGetUserProjects();
const { mutateAsync: addIdentityToWorkspace } = useAddIdentityToWorkspace();
const {
@@ -77,7 +77,7 @@ const Content = ({ identityId, handlePopUpToggle }: Omit<Props, "popUp">) => {
const onFormSubmit = async ({ project: selectedProject, role }: FormData) => {
try {
await addIdentityToWorkspace({
workspaceId: selectedProject.id,
projectId: selectedProject.id,
identityId,
role: role.slug || undefined
});
@@ -8,7 +8,7 @@ import { createNotification } from "@app/components/notifications";
import { IconButton, Td, Tooltip, Tr } from "@app/components/v2";
import { getProjectBaseURL } from "@app/helpers/project";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useGetUserProjects } from "@app/hooks/api";
import { IdentityMembership } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
@@ -31,7 +31,7 @@ export const IdentityProjectRow = ({
membership: { id, createdAt, identity, project, roles },
handlePopUpOpen
}: Props) => {
const { data: workspaces } = useGetUserWorkspaces();
const { data: workspaces } = useGetUserProjects();
const navigate = useNavigate();
const isAccessible = useMemo(() => {
@@ -25,7 +25,7 @@ export const IdentityProjectsSection = ({ identityId }: Props) => {
try {
await deleteMutateAsync({
identityId: id,
workspaceId: projectId
projectId
});
createNotification({
@@ -0,0 +1,81 @@
import { useState } from "react";
import { UseMutationResult } from "@tanstack/react-query";
import ms from "ms";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button } from "@app/components/v2";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
export const LockoutFields = ({
clearLockoutsResult,
lockedOut,
identityId,
data,
onResetAllLockouts
}: {
clearLockoutsResult: UseMutationResult<number, object, { identityId: string }, unknown>;
lockedOut: boolean;
identityId: string;
data: {
lockoutEnabled: boolean;
lockoutThreshold: number;
lockoutDurationSeconds: number;
lockoutCounterResetSeconds: number;
};
onResetAllLockouts: () => void;
}) => {
const { mutateAsync, isPending } = clearLockoutsResult;
const [lockedOutState, setLockedOutState] = useState(lockedOut);
async function clearLockouts() {
try {
const deleted = await mutateAsync({ identityId });
createNotification({
text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`,
type: "success"
});
setLockedOutState(false);
onResetAllLockouts();
} catch (error) {
console.error(error);
createNotification({
text: "Failed to clear lockouts. Please try again.",
type: "error"
});
}
}
return (
<>
<div className="col-span-2 mt-3 flex justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Lockout Options</span>
<OrgPermissionCan I={OrgPermissionIdentityActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed || !lockedOutState || isPending}
size="xs"
onClick={() => clearLockouts()}
isLoading={isPending}
colorSchema="secondary"
>
Reset All Lockouts
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityAuthFieldDisplay label="Lockout Threshold">
{data.lockoutThreshold}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Lockout Duration">
{ms(data.lockoutDurationSeconds * 1000, { long: true })}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Lockout Counter Reset">
{ms(data.lockoutCounterResetSeconds * 1000, { long: true })}
</IdentityAuthFieldDisplay>
</>
);
};
@@ -2,11 +2,12 @@ import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityLdapAuth } from "@app/hooks/api";
import { useClearIdentityLdapAuthLockouts, useGetIdentityLdapAuth } from "@app/hooks/api";
import { IdentityLdapAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityLdapAuthForm";
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { LockoutFields } from "./IdentityAuthLockoutFields";
import { ViewAuthMethodProps } from "./types";
export const ViewIdentityLdapAuthContent = ({
@@ -14,9 +15,12 @@ export const ViewIdentityLdapAuthContent = ({
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
popUp,
lockedOut,
onResetAllLockouts
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityLdapAuth(identityId);
const clearLockoutsResult = useClearIdentityLdapAuthLockouts();
if (isPending) {
return (
@@ -98,6 +102,18 @@ export const ViewIdentityLdapAuthContent = ({
</Tooltip>
)}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Lockout">
{data.lockoutEnabled ? "Enabled" : "Disabled"}
</IdentityAuthFieldDisplay>
{data.lockoutEnabled && (
<LockoutFields
identityId={identityId}
lockedOut={lockedOut}
clearLockoutsResult={clearLockoutsResult}
data={data}
onResetAllLockouts={onResetAllLockouts}
/>
)}
</ViewIdentityContentWrapper>
);
};
@@ -1,12 +1,7 @@
import { useState } from "react";
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import ms from "ms";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import {
useClearIdentityUniversalAuthLockouts,
@@ -16,6 +11,7 @@ import {
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { LockoutFields } from "./IdentityAuthLockoutFields";
import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
@@ -32,33 +28,12 @@ export const ViewIdentityUniversalAuthContent = ({
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
const { data: clientSecrets = [], isPending: clientSecretsPending } =
useGetIdentityUniversalAuthClientSecrets(identityId);
const { mutateAsync: clearLockoutsFn, isPending: isClearLockoutsPending } =
useClearIdentityUniversalAuthLockouts();
const [lockedOutState, setLockedOutState] = useState(lockedOut);
const clearLockoutsResult = useClearIdentityUniversalAuthLockouts();
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
initialState: "Copy Client ID to clipboard"
});
async function clearLockouts() {
try {
const deleted = await clearLockoutsFn({ identityId });
createNotification({
text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`,
type: "success"
});
setLockedOutState(false);
onResetAllLockouts();
} catch (error) {
console.error(error);
createNotification({
text: "Failed to clear lockouts. Please try again.",
type: "error"
});
}
}
if (isPending || clientSecretsPending) {
return (
<div className="flex w-full items-center justify-center">
@@ -119,36 +94,13 @@ export const ViewIdentityUniversalAuthContent = ({
{data.lockoutEnabled ? "Enabled" : "Disabled"}
</IdentityAuthFieldDisplay>
{data.lockoutEnabled && (
<>
<div className="col-span-2 mt-3 flex justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Lockout Options</span>
<OrgPermissionCan
I={OrgPermissionIdentityActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed || !lockedOutState || isClearLockoutsPending}
size="xs"
onClick={() => clearLockouts()}
isLoading={isClearLockoutsPending}
colorSchema="secondary"
>
Reset All Lockouts
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityAuthFieldDisplay label="Lockout Threshold">
{data.lockoutThreshold}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Lockout Duration">
{ms(data.lockoutDurationSeconds * 1000, { long: true })}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Lockout Counter Reset">
{ms(data.lockoutCounterResetSeconds * 1000, { long: true })}
</IdentityAuthFieldDisplay>
</>
<LockoutFields
identityId={identityId}
lockedOut={lockedOut}
clearLockoutsResult={clearLockoutsResult}
data={data}
onResetAllLockouts={onResetAllLockouts}
/>
)}
<div className="col-span-2 my-3">
<div className="mb-3 border-b border-mineshaft-500 pb-2">
@@ -1,5 +1,4 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import {
faArrowDownAZ,
faBorderAll,
@@ -16,6 +15,7 @@ import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { RequestProjectAccessModal } from "@app/components/projects/RequestProjectAccessModal";
import {
Badge,
Button,
@@ -24,12 +24,9 @@ import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
IconButton,
Input,
Lottie,
Modal,
ModalContent,
Pagination,
Skeleton,
Tooltip
@@ -43,12 +40,8 @@ import {
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
import {
useOrgAdminAccessProject,
useRequestProjectAccess,
useSearchProjects
} from "@app/hooks/api";
import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types";
import { useOrgAdminAccessProject, useSearchProjects } from "@app/hooks/api";
import { Project, ProjectEnv, ProjectType } from "@app/hooks/api/projects/types";
import {
ProjectListToggle,
ProjectListView
@@ -62,53 +55,6 @@ type Props = {
onProjectListViewChange: (value: ProjectListView) => void;
};
type RequestAccessModalProps = {
projectId: string;
onPopUpToggle: () => void;
};
const RequestAccessModal = ({ projectId, onPopUpToggle }: RequestAccessModalProps) => {
const form = useForm<{ note: string }>();
const requestProjectAccess = useRequestProjectAccess();
const onFormSubmit = ({ note }: { note: string }) => {
if (requestProjectAccess.isPending) return;
requestProjectAccess.mutate(
{
comment: note,
projectId
},
{
onSuccess: () => {
createNotification({
type: "success",
title: "Project Access Request Sent",
text: "Project admins will receive an email of your request"
});
onPopUpToggle();
}
}
);
};
return (
<form onSubmit={form.handleSubmit(onFormSubmit)}>
<FormControl label="Note">
<Input {...form.register("note")} />
</FormControl>
<div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={form.formState.isSubmitting}>
Submit Request
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => onPopUpToggle()}>
Cancel
</Button>
</div>
</form>
);
};
export const AllProjectView = ({
onAddNewProject,
onUpgradePlan,
@@ -155,7 +101,7 @@ export const AllProjectView = ({
const handleAccessProject = async (
type: ProjectType,
projectId: string,
environments: WorkspaceEnv[]
environments: ProjectEnv[]
) => {
try {
await orgAdminAccessProject.mutateAsync({
@@ -180,7 +126,7 @@ export const AllProjectView = ({
offset,
totalCount: searchedProjects?.totalCount || 0
});
const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Workspace;
const requestedWorkspaceDetails = (popUp.requestAccessConfirmation.data || {}) as Project;
const handleToggleFilterByProjectType = (el: ProjectType) =>
setProjectTypeFilter((state) => (state === el ? undefined : el));
@@ -278,22 +224,26 @@ export const AllProjectView = ({
</IconButton>
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
{(isOldProjectPermissionAllowed) => (
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Project}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed && !isOldProjectPermissionAllowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
)}
</OrgPermissionCan>
)}
</OrgPermissionCan>
</div>
@@ -419,20 +369,11 @@ export const AllProjectView = ({
<div className="text-center font-light">No Projects Found</div>
</div>
)}
<Modal
<RequestProjectAccessModal
isOpen={popUp.requestAccessConfirmation.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("requestAccessConfirmation", isOpen)}
>
<ModalContent
title="Confirm Access Request"
subTitle={`Requesting access to project ${requestedWorkspaceDetails?.name}. You may include an optional note for project admins to review your request.`}
>
<RequestAccessModal
onPopUpToggle={() => handlePopUpToggle("requestAccessConfirmation")}
projectId={requestedWorkspaceDetails?.id}
/>
</ModalContent>
</Modal>
project={requestedWorkspaceDetails}
/>
</div>
);
};
@@ -39,11 +39,11 @@ import {
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { useGetUserProjects } from "@app/hooks/api";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { Project, ProjectType } from "@app/hooks/api/projects/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
import {
ProjectListToggle,
ProjectListView
@@ -79,7 +79,7 @@ export const MyProjectView = ({
{}
);
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserWorkspaces();
const { data: workspaces = [], isPending: isWorkspaceLoading } = useGetUserProjects();
const {
setPage,
perPage,
@@ -136,7 +136,7 @@ export const MyProjectView = ({
const { workspacesWithFaveProp } = useMemo(() => {
const workspacesWithFav = filteredWorkspaces
.map((w): Workspace & { isFavorite: boolean } => ({
.map((w): Project & { isFavorite: boolean } => ({
...w,
isFavorite: Boolean(projectFavorites?.includes(w.id))
}))
@@ -188,7 +188,7 @@ export const MyProjectView = ({
}
};
const renderProjectGridItem = (workspace: Workspace, isFavorite: boolean) => (
const renderProjectGridItem = (workspace: Project, isFavorite: boolean) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
@@ -242,7 +242,7 @@ export const MyProjectView = ({
</p>
</div>
);
const renderProjectListItem = (workspace: Workspace, isFavorite: boolean, index: number) => (
const renderProjectListItem = (workspace: Project, isFavorite: boolean, index: number) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
onClick={() => {
@@ -480,22 +480,26 @@ export const MyProjectView = ({
</IconButton>
</div>
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Workspace}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
{(isOldProjectV1Allowed) => (
<OrgPermissionCan I={OrgPermissionActions.Create} an={OrgPermissionSubjects.Project}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed && !isOldProjectV1Allowed}
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (isAddingProjectsAllowed) {
onAddNewProject();
} else {
onUpgradePlan();
}
}}
className="ml-2"
>
Add New Project
</Button>
)}
</OrgPermissionCan>
)}
</OrgPermissionCan>
</div>
@@ -122,12 +122,11 @@ export const formSchema = z.object({
.refine((val) => val !== "custom", { message: "Cannot use custom as its a keyword" }),
permissions: z
.object({
workspace: z
project: z
.object({
create: z.boolean().optional()
})
.optional(),
"audit-logs": auditLogsPermissionSchema,
member: generalPermissionSchema,
groups: groupPermissionSchema,
@@ -162,7 +161,11 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => {
// i would have to write a if loop with both conditions same
const formVal: Record<string, any> = {};
permissions.forEach((permission) => {
const { subject, action } = permission;
const { action } = permission;
let { subject } = permission;
if (subject === OrgPermissionSubjects.Workspace) {
subject = OrgPermissionSubjects.Project;
}
if (!formVal?.[subject]) formVal[subject] = {};
formVal[subject][action] = true;
});
@@ -28,7 +28,7 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
const rule = useWatch({
control,
name: "permissions.workspace"
name: "permissions.project"
});
const selectedPermissionCategory = useMemo(() => {
@@ -60,7 +60,7 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
setIsCustom.off();
if (val === Permission.NoAccess) {
setValue("permissions.workspace", { create: false }, { shouldDirty: true });
setValue("permissions.project", { create: false }, { shouldDirty: true });
}
};
@@ -95,8 +95,8 @@ export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) =>
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
name={`permissions.project.${action}`}
key={`permissions.project.${action}`}
control={control}
render={({ field }) => (
<Checkbox

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