mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 08:27:53 +00:00
feat: completed identity project detail screen
This commit is contained in:
+7
-2
@@ -158,7 +158,11 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
|
const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime;
|
||||||
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
|
const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange;
|
||||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
||||||
...data,
|
slug: data.slug,
|
||||||
|
permissions: data.permissions,
|
||||||
|
isTemporary: data.isTemporary,
|
||||||
|
temporaryRange: data.temporaryRange,
|
||||||
|
temporaryMode: data.temporaryMode,
|
||||||
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""),
|
||||||
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || ""))
|
||||||
});
|
});
|
||||||
@@ -169,7 +173,8 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, {
|
||||||
...data,
|
slug: data.slug,
|
||||||
|
permissions: data.permissions,
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
temporaryAccessStartTime: null,
|
temporaryAccessStartTime: null,
|
||||||
temporaryAccessEndTime: null,
|
temporaryAccessEndTime: null,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TOrgRole } from "../roles/types";
|
import { TOrgRole } from "../roles/types";
|
||||||
import { Workspace } from "../workspace/types";
|
import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types";
|
||||||
import { IdentityAuthMethod } from "./enums";
|
import { IdentityAuthMethod } from "./enums";
|
||||||
|
|
||||||
export type IdentityTrustedIp = {
|
export type IdentityTrustedIp = {
|
||||||
@@ -66,7 +66,7 @@ export type IdentityMembership = {
|
|||||||
| {
|
| {
|
||||||
isTemporary: true;
|
isTemporary: true;
|
||||||
temporaryRange: string;
|
temporaryRange: string;
|
||||||
temporaryMode: string;
|
temporaryMode: ProjectUserMembershipTemporaryMode;
|
||||||
temporaryAccessEndTime: string;
|
temporaryAccessEndTime: string;
|
||||||
temporaryAccessStartTime: string;
|
temporaryAccessStartTime: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,11 @@ export const useCreateIdentityProjectAdditionalPrivilege = () => {
|
|||||||
|
|
||||||
return useMutation<TIdentityProjectPrivilege, {}, TCreateIdentityProjectPrivilegeDTO>({
|
return useMutation<TIdentityProjectPrivilege, {}, TCreateIdentityProjectPrivilegeDTO>({
|
||||||
mutationFn: async (dto) => {
|
mutationFn: async (dto) => {
|
||||||
const { data } = await apiRequest.post(
|
const { data } = await apiRequest.post("/api/v2/identity-project-additional-privilege", dto);
|
||||||
"/api/v1/additional-privilege/identity/permanent",
|
|
||||||
dto
|
|
||||||
);
|
|
||||||
return data.privilege;
|
return data.privilege;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectSlug, identityId }) => {
|
onSuccess: (_, { projectId, identityId }) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId }));
|
||||||
identitiyProjectPrivilegeKeys.list({ projectSlug, identityId })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -33,19 +28,22 @@ export const useUpdateIdentityProjectAdditionalPrivilege = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<TIdentityProjectPrivilege, {}, TUpdateIdentityProjectPrivlegeDTO>({
|
return useMutation<TIdentityProjectPrivilege, {}, TUpdateIdentityProjectPrivlegeDTO>({
|
||||||
mutationFn: async ({ privilegeSlug, projectSlug, identityId, privilegeDetails }) => {
|
mutationFn: async ({ projectId, privilegeId, identityId, permissions, slug, type }) => {
|
||||||
const { data: res } = await apiRequest.patch("/api/v1/additional-privilege/identity", {
|
const { data: res } = await apiRequest.patch(
|
||||||
privilegeSlug,
|
`/api/v2/identity-project-additional-privilege/${privilegeId}`,
|
||||||
projectSlug,
|
{
|
||||||
identityId,
|
privilegeId,
|
||||||
privilegeDetails
|
projectId,
|
||||||
});
|
identityId,
|
||||||
|
permissions,
|
||||||
|
slug,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
);
|
||||||
return res.privilege;
|
return res.privilege;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectSlug, identityId }) => {
|
onSuccess: (_, { projectId, identityId }) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId }));
|
||||||
identitiyProjectPrivilegeKeys.list({ projectSlug, identityId })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -54,20 +52,21 @@ export const useDeleteIdentityProjectAdditionalPrivilege = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<TIdentityProjectPrivilege, {}, TDeleteIdentityProjectPrivilegeDTO>({
|
return useMutation<TIdentityProjectPrivilege, {}, TDeleteIdentityProjectPrivilegeDTO>({
|
||||||
mutationFn: async ({ identityId, projectSlug, privilegeSlug }) => {
|
mutationFn: async ({ identityId, projectId, privilegeId }) => {
|
||||||
const { data } = await apiRequest.delete("/api/v1/additional-privilege/identity", {
|
const { data } = await apiRequest.delete(
|
||||||
data: {
|
`/api/v2/identity-project-additional-privilege/${privilegeId}`,
|
||||||
identityId,
|
{
|
||||||
projectSlug,
|
data: {
|
||||||
privilegeSlug
|
identityId,
|
||||||
|
privilegeId,
|
||||||
|
projectId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
return data.privilege;
|
return data.privilege;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { projectSlug, identityId }) => {
|
onSuccess: (_, { projectId, identityId }) => {
|
||||||
queryClient.invalidateQueries(
|
queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId }));
|
||||||
identitiyProjectPrivilegeKeys.list({ projectSlug, identityId })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,36 +9,36 @@ import {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const identitiyProjectPrivilegeKeys = {
|
export const identitiyProjectPrivilegeKeys = {
|
||||||
details: ({ identityId, privilegeSlug, projectSlug }: TGetIdentityProjectPrivilegeDetails) =>
|
details: ({ identityId, privilegeId, projectId }: TGetIdentityProjectPrivilegeDetails) =>
|
||||||
[
|
[
|
||||||
"identity-user-privilege",
|
"identity-user-privilege",
|
||||||
{
|
{
|
||||||
identityId,
|
identityId,
|
||||||
projectSlug,
|
projectId,
|
||||||
privilegeSlug
|
privilegeId
|
||||||
}
|
}
|
||||||
] as const,
|
] as const,
|
||||||
list: ({ projectSlug, identityId }: TListIdentityProjectPrivileges) =>
|
list: ({ projectId, identityId }: TListIdentityProjectPrivileges) =>
|
||||||
["identity-user-privileges", { identityId, projectSlug }] as const
|
["identity-user-privileges", { identityId, projectId }] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetIdentityProjectPrivilegeDetails = ({
|
export const useGetIdentityProjectPrivilegeDetails = ({
|
||||||
projectSlug,
|
projectId,
|
||||||
identityId,
|
identityId,
|
||||||
privilegeSlug
|
privilegeId
|
||||||
}: TGetIdentityProjectPrivilegeDetails) => {
|
}: TGetIdentityProjectPrivilegeDetails) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: Boolean(projectSlug && identityId && privilegeSlug),
|
enabled: Boolean(projectId && identityId && privilegeId),
|
||||||
queryKey: identitiyProjectPrivilegeKeys.details({ projectSlug, privilegeSlug, identityId }),
|
queryKey: identitiyProjectPrivilegeKeys.details({ projectId, privilegeId, identityId }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const {
|
const {
|
||||||
data: { privilege }
|
data: { privilege }
|
||||||
} = await apiRequest.get<{
|
} = await apiRequest.get<{
|
||||||
privilege: TIdentityProjectPrivilege;
|
privilege: TIdentityProjectPrivilege;
|
||||||
}>(`/api/v1/additional-privilege/identity/${privilegeSlug}`, {
|
}>(`/api/v2/identity-project-additional-privilege/${privilegeId}`, {
|
||||||
params: {
|
params: {
|
||||||
identityId,
|
identityId,
|
||||||
projectSlug
|
projectId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return privilege;
|
return privilege;
|
||||||
@@ -47,19 +47,19 @@ export const useGetIdentityProjectPrivilegeDetails = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useListIdentityProjectPrivileges = ({
|
export const useListIdentityProjectPrivileges = ({
|
||||||
projectSlug,
|
projectId,
|
||||||
identityId
|
identityId
|
||||||
}: TListIdentityProjectPrivileges) => {
|
}: TListIdentityProjectPrivileges) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
enabled: Boolean(projectSlug && identityId),
|
enabled: Boolean(projectId && identityId),
|
||||||
queryKey: identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }),
|
queryKey: identitiyProjectPrivilegeKeys.list({ projectId, identityId }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const {
|
const {
|
||||||
data: { privileges }
|
data: { privileges }
|
||||||
} = await apiRequest.get<{
|
} = await apiRequest.get<{
|
||||||
privileges: Array<TIdentityProjectPrivilege>;
|
privileges: Array<TIdentityProjectPrivilege>;
|
||||||
}>("/api/v1/additional-privilege/identity", {
|
}>("/api/v2/identity-project-additional-privilege", {
|
||||||
params: { identityId, projectSlug }
|
params: { identityId, projectId }
|
||||||
});
|
});
|
||||||
return privileges;
|
return privileges;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,48 +28,42 @@ export type TIdentityProjectPrivilege = {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type TProjectSpecificPrivilegePermission = {
|
|
||||||
conditions: {
|
|
||||||
environment: string;
|
|
||||||
secretPath?: { $glob: string };
|
|
||||||
};
|
|
||||||
actions: string[];
|
|
||||||
subject: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TCreateIdentityProjectPrivilegeDTO = {
|
export type TCreateIdentityProjectPrivilegeDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
projectSlug: string;
|
projectId: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
isTemporary?: boolean;
|
type:
|
||||||
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
|
| {
|
||||||
temporaryRange?: string;
|
isTemporary: true;
|
||||||
temporaryAccessStartTime?: string;
|
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
|
||||||
privilegePermission: TProjectSpecificPrivilegePermission;
|
temporaryRange?: string;
|
||||||
|
temporaryAccessStartTime?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isTemporary: false;
|
||||||
|
};
|
||||||
|
permissions: TProjectPermission[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateIdentityProjectPrivlegeDTO = {
|
export type TUpdateIdentityProjectPrivlegeDTO = {
|
||||||
projectSlug: string;
|
projectId: string;
|
||||||
identityId: string;
|
identityId: string;
|
||||||
privilegeSlug: string;
|
privilegeId: string;
|
||||||
privilegeDetails: Partial<
|
} & Partial<Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">>;
|
||||||
Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TDeleteIdentityProjectPrivilegeDTO = {
|
export type TDeleteIdentityProjectPrivilegeDTO = {
|
||||||
projectSlug: string;
|
projectId: string;
|
||||||
identityId: string;
|
identityId: string;
|
||||||
privilegeSlug: string;
|
privilegeId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TListIdentityUserPrivileges = {
|
export type TListIdentityUserPrivileges = {
|
||||||
projectSlug: string;
|
projectId: string;
|
||||||
identityId: string;
|
identityId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetIdentityProejctPrivilegeDetails = {
|
export type TGetIdentityProejctPrivilegeDetails = {
|
||||||
projectSlug: string;
|
projectId: string;
|
||||||
identityId: string;
|
identityId: string;
|
||||||
privilegeSlug: string;
|
privilegeId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export {
|
|||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
useGetWorkspaceAuthorizations,
|
useGetWorkspaceAuthorizations,
|
||||||
useGetWorkspaceById,
|
useGetWorkspaceById,
|
||||||
|
useGetWorkspaceIdentityMembershipDetails,
|
||||||
useGetWorkspaceIdentityMemberships,
|
useGetWorkspaceIdentityMemberships,
|
||||||
useGetWorkspaceIndexStatus,
|
useGetWorkspaceIndexStatus,
|
||||||
useGetWorkspaceIntegrations,
|
useGetWorkspaceIntegrations,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { TCertificate } from "../certificates/types";
|
|||||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||||
import { TGroupMembership } from "../groups/types";
|
import { TGroupMembership } from "../groups/types";
|
||||||
import { identitiesKeys } from "../identities/queries";
|
import { identitiesKeys } from "../identities/queries";
|
||||||
import { TProjectIdentitiesList } from "../identities/types";
|
import { IdentityMembership, TProjectIdentitiesList } from "../identities/types";
|
||||||
import { IntegrationAuth } from "../integrationAuth/types";
|
import { IntegrationAuth } from "../integrationAuth/types";
|
||||||
import { TIntegration } from "../integrations/types";
|
import { TIntegration } from "../integrations/types";
|
||||||
import { TPkiAlert } from "../pkiAlerts/types";
|
import { TPkiAlert } from "../pkiAlerts/types";
|
||||||
@@ -477,6 +477,9 @@ export const useUpdateIdentityWorkspaceRole = () => {
|
|||||||
onSuccess: (_, { identityId, workspaceId }) => {
|
onSuccess: (_, { identityId, workspaceId }) => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId));
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIdentityMemberships(workspaceId));
|
||||||
queryClient.invalidateQueries(identitiesKeys.getIdentityProjectMemberships(identityId));
|
queryClient.invalidateQueries(identitiesKeys.getIdentityProjectMemberships(identityId));
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
workspaceKeys.getWorkspaceIdentityMembershipDetails(workspaceId, identityId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -553,6 +556,21 @@ export const useGetWorkspaceIdentityMemberships = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetWorkspaceIdentityMembershipDetails = (projectId: string, identityId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
enabled: Boolean(projectId && identityId),
|
||||||
|
queryKey: workspaceKeys.getWorkspaceIdentityMembershipDetails(projectId, identityId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { identityMembership }
|
||||||
|
} = await apiRequest.get<{ identityMembership: IdentityMembership }>(
|
||||||
|
`/api/v2/workspace/${projectId}/identity-memberships/${identityId}`
|
||||||
|
);
|
||||||
|
return identityMembership;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useListWorkspaceGroups = (projectId: string) => {
|
export const useListWorkspaceGroups = (projectId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId),
|
queryKey: workspaceKeys.getWorkspaceGroupMemberships(projectId),
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export const workspaceKeys = {
|
|||||||
[{ workspaceId, membershipId }, "workspace-user-details"] as const,
|
[{ workspaceId, membershipId }, "workspace-user-details"] as const,
|
||||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
||||||
|
getWorkspaceIdentityMembershipDetails: (workspaceId: string, identityId: string) =>
|
||||||
|
[{ workspaceId, identityId }, "workspace-identity-membership-details"] as const,
|
||||||
// allows invalidation using above key without knowing params
|
// allows invalidation using above key without knowing params
|
||||||
getWorkspaceIdentityMembershipsWithParams: ({
|
getWorkspaceIdentityMembershipsWithParams: ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { IdentityDetailsPage } from "@app/views/Project/IdentityDetailsPage";
|
||||||
|
|
||||||
|
export default function ProjectIdentityDetailsPage() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
</Head>
|
||||||
|
<IdentityDetailsPage />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProjectIdentityDetailsPage.requireAuth = true;
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, EmptyState, Spinner } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { withProjectPermission } from "@app/hoc";
|
||||||
|
import { useGetWorkspaceIdentityMembershipDetails } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { IdentityProjectAdditionalPrivilegeSection } from "./components/IdentityProjectAdditionalPrivilegeSection";
|
||||||
|
import { IdentityRoleDetailsSection } from "./components/IdentityRoleDetailsSectiona";
|
||||||
|
|
||||||
|
export const IdentityDetailsPage = withProjectPermission(
|
||||||
|
() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
const identityId = router.query.identityId as string;
|
||||||
|
|
||||||
|
const { data: identityMembershipDetails, isLoading: isMembershipDetailsLoading } =
|
||||||
|
useGetWorkspaceIdentityMembershipDetails(workspaceId, identityId);
|
||||||
|
|
||||||
|
if (isMembershipDetailsLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center p-24">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex max-w-7xl flex-col justify-between bg-bunker-800 p-6 text-white">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`/project/${workspaceId}/members?selectedTab=identities`);
|
||||||
|
}}
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
Project Access Control
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{identityMembershipDetails ? (
|
||||||
|
<>
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between ">
|
||||||
|
<h3 className="text-xl font-semibold text-mineshaft-100">
|
||||||
|
Project Identity Access
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Identity}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Edit role"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Remove Identity
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-12">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-semibold text-gray-400">Name</span>
|
||||||
|
{identityMembershipDetails && (
|
||||||
|
<p className="text-lg capitalize">
|
||||||
|
{identityMembershipDetails?.identity?.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 text-sm text-gray-400">
|
||||||
|
Joined on{" "}
|
||||||
|
{identityMembershipDetails?.createdAt &&
|
||||||
|
format(new Date(identityMembershipDetails?.createdAt || ""), "yyyy-MM-dd")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<IdentityRoleDetailsSection
|
||||||
|
identityMembershipDetails={identityMembershipDetails}
|
||||||
|
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||||
|
/>
|
||||||
|
<IdentityProjectAdditionalPrivilegeSection
|
||||||
|
identityMembershipDetails={identityMembershipDetails}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<EmptyState title="Error: Unable to find the identity." className="py-12" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: ProjectPermissionActions.Read,
|
||||||
|
subject: ProjectPermissionSub.Identity
|
||||||
|
}
|
||||||
|
);
|
||||||
+391
@@ -0,0 +1,391 @@
|
|||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
faCaretDown,
|
||||||
|
faChevronLeft,
|
||||||
|
faClock,
|
||||||
|
faPlus,
|
||||||
|
faSave
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { format, formatDistance } from "date-fns";
|
||||||
|
import ms from "ms";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalTrigger,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import {
|
||||||
|
useCreateIdentityProjectAdditionalPrivilege,
|
||||||
|
useGetIdentityProjectPrivilegeDetails,
|
||||||
|
useUpdateIdentityProjectAdditionalPrivilege
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/hooks/api/identityProjectAdditionalPrivilege/types";
|
||||||
|
import { GeneralPermissionPolicies } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/GeneralPermissionPolicies";
|
||||||
|
import { NewPermissionRule } from "@app/views/Project/RolePage/components/RolePermissionsSection/components/NewPermissionRule";
|
||||||
|
import { PermissionEmptyState } from "@app/views/Project/RolePage/components/RolePermissionsSection/PermissionEmptyState";
|
||||||
|
import {
|
||||||
|
formRolePermission2API,
|
||||||
|
PROJECT_PERMISSION_OBJECT,
|
||||||
|
projectRoleFormSchema,
|
||||||
|
rolePermission2Form
|
||||||
|
} from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils";
|
||||||
|
import { renderConditionalComponents } from "@app/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
privilegeId?: string;
|
||||||
|
identityId: string;
|
||||||
|
onGoBack: () => void;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formSchema = z.object({
|
||||||
|
slug: z.string().optional(),
|
||||||
|
temporaryAccess: z
|
||||||
|
.discriminatedUnion("isTemporary", [
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(true),
|
||||||
|
temporaryRange: z.string().min(1),
|
||||||
|
temporaryAccessStartTime: z.string().datetime(),
|
||||||
|
temporaryAccessEndTime: z.string().datetime().nullable().optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(false)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.default({ isTemporary: false }),
|
||||||
|
permissions: projectRoleFormSchema.shape.permissions
|
||||||
|
});
|
||||||
|
|
||||||
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||||
|
privilegeId = "",
|
||||||
|
onGoBack,
|
||||||
|
identityId,
|
||||||
|
isDisabled
|
||||||
|
}: Props) => {
|
||||||
|
const isCreate = !privilegeId;
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
||||||
|
const projectId = currentWorkspace?.id || "";
|
||||||
|
const { data: privilegeDetails, isLoading } = useGetIdentityProjectPrivilegeDetails({
|
||||||
|
identityId,
|
||||||
|
projectId,
|
||||||
|
privilegeId
|
||||||
|
});
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
const isIdentityEditDisabled = permission.cannot(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<TFormSchema>({
|
||||||
|
values: privilegeDetails
|
||||||
|
? {
|
||||||
|
...privilegeDetails,
|
||||||
|
permissions: rolePermission2Form(privilegeDetails.permissions),
|
||||||
|
temporaryAccess: privilegeDetails.isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryRange: privilegeDetails.temporaryRange || "",
|
||||||
|
temporaryAccessEndTime: privilegeDetails.temporaryAccessEndTime || "",
|
||||||
|
temporaryAccessStartTime: privilegeDetails.temporaryAccessStartTime || ""
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isTemporary: privilegeDetails.isTemporary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isDirty, isSubmitting }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const { mutateAsync: updateIdentityProjectAdditionalPrivilege } =
|
||||||
|
useUpdateIdentityProjectAdditionalPrivilege();
|
||||||
|
const { mutateAsync: createIdentityProjectAdditionalPrivilege } =
|
||||||
|
useCreateIdentityProjectAdditionalPrivilege();
|
||||||
|
|
||||||
|
const onSubmit = async (el: TFormSchema) => {
|
||||||
|
const accessType = !el.temporaryAccess.isTemporary
|
||||||
|
? { role: el.slug, isTemporary: false as const }
|
||||||
|
: {
|
||||||
|
role: el.slug,
|
||||||
|
isTemporary: true as const,
|
||||||
|
temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative,
|
||||||
|
temporaryRange: el.temporaryAccess.temporaryRange,
|
||||||
|
temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isCreate) {
|
||||||
|
await createIdentityProjectAdditionalPrivilege({
|
||||||
|
permissions: formRolePermission2API(el.permissions),
|
||||||
|
identityId,
|
||||||
|
projectId,
|
||||||
|
slug: el.slug || undefined,
|
||||||
|
type: accessType
|
||||||
|
});
|
||||||
|
createNotification({ type: "success", text: "Successfully created privilege" });
|
||||||
|
} else {
|
||||||
|
if (!projectId || !privilegeDetails?.id) return;
|
||||||
|
await updateIdentityProjectAdditionalPrivilege({
|
||||||
|
privilegeId: privilegeDetails.id,
|
||||||
|
permissions: formRolePermission2API(el.permissions),
|
||||||
|
projectId,
|
||||||
|
identityId,
|
||||||
|
slug: el.slug || undefined,
|
||||||
|
type: accessType
|
||||||
|
});
|
||||||
|
createNotification({ type: "success", text: "Successfully updated privilege" });
|
||||||
|
}
|
||||||
|
onGoBack();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({ type: "error", text: "Failed to update role" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const privilegeTemporaryAccess = form.watch("temporaryAccess");
|
||||||
|
const isTemporary = privilegeTemporaryAccess?.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
privilegeTemporaryAccess?.isTemporary &&
|
||||||
|
new Date() > new Date(privilegeTemporaryAccess.temporaryAccessEndTime || "");
|
||||||
|
let text = "Permanent";
|
||||||
|
let toolTipText = "Non expiry access";
|
||||||
|
|
||||||
|
if (isTemporary) {
|
||||||
|
if (isExpired) {
|
||||||
|
text = "Access Expired";
|
||||||
|
toolTipText = "Timed Access Expired";
|
||||||
|
} else {
|
||||||
|
text = formatDistance(
|
||||||
|
new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""),
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
toolTipText = `Until ${format(
|
||||||
|
new Date(privilegeTemporaryAccess.temporaryAccessEndTime || ""),
|
||||||
|
"yyyy-MM-dd hh:mm:ss aaa"
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<IconButton ariaLabel="go-back" variant="plain" onClick={onGoBack}>
|
||||||
|
<FontAwesomeIcon icon={faChevronLeft} />
|
||||||
|
</IconButton>
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">
|
||||||
|
Modify Additional Privilege
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{isDirty && (
|
||||||
|
<Button
|
||||||
|
className="mr-4 text-mineshaft-300"
|
||||||
|
variant="link"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
onClick={onGoBack}
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
type="submit"
|
||||||
|
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
|
||||||
|
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.createPolicy.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="h-10 rounded-l-none"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
New policy
|
||||||
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent title="New Policy" subTitle="Policies grant additional permissions.">
|
||||||
|
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 border-b border-gray-800 p-4 first:rounded-t-md last:rounded-b-md">
|
||||||
|
<div className="mb-2 text-lg">Overview</div>
|
||||||
|
<div className=" flex items-end space-x-6">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="slug"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl label="Privilege Name" isOptional className="mb-0">
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger disabled={isIdentityEditDisabled} asChild>
|
||||||
|
<div className="w-full max-w-md flex-grow">
|
||||||
|
<Tooltip content={toolTipText}>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
|
||||||
|
isDisabled={isIdentityEditDisabled}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
|
||||||
|
isTemporary && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
arrowClassName="fill-gray-600"
|
||||||
|
side="right"
|
||||||
|
sideOffset={12}
|
||||||
|
hideCloseBtn
|
||||||
|
className="border border-gray-600 pt-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||||
|
Configure timed access
|
||||||
|
</div>
|
||||||
|
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
defaultValue="1h"
|
||||||
|
name="temporaryAccess.temporaryRange"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Validity" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
const temporaryRange = form.getValues("temporaryAccess.temporaryRange");
|
||||||
|
if (!temporaryRange) {
|
||||||
|
form.setError(
|
||||||
|
"temporaryAccess.temporaryRange",
|
||||||
|
{ type: "required", message: "Required" },
|
||||||
|
{ shouldFocus: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
form.clearErrors("temporaryAccess.temporaryRange");
|
||||||
|
form.setValue(
|
||||||
|
"temporaryAccess",
|
||||||
|
{
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryAccessStartTime: new Date().toISOString(),
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessEndTime: new Date(
|
||||||
|
new Date().getTime() + ms(temporaryRange)
|
||||||
|
).toISOString()
|
||||||
|
},
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isTemporary ? "Restart" : "Grant"}
|
||||||
|
</Button>
|
||||||
|
{isTemporary && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue(
|
||||||
|
"temporaryAccess",
|
||||||
|
{
|
||||||
|
isTemporary: false
|
||||||
|
},
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revoke Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="mb-2 text-lg">Policies</div>
|
||||||
|
{(isCreate || !isLoading) && <PermissionEmptyState />}
|
||||||
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||||
|
<GeneralPermissionPolicies
|
||||||
|
subject={subject}
|
||||||
|
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||||
|
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||||
|
key={`project-permission-${subject}`}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
>
|
||||||
|
{renderConditionalComponents(subject, isDisabled)}
|
||||||
|
</GeneralPermissionPolicies>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</FormProvider>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+245
@@ -0,0 +1,245 @@
|
|||||||
|
import { faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format, formatDistance } from "date-fns";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
Tag,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteIdentityProjectAdditionalPrivilege } from "@app/hooks/api";
|
||||||
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
|
import { useListIdentityProjectPrivileges } from "@app/hooks/api/identityProjectAdditionalPrivilege/queries";
|
||||||
|
|
||||||
|
import { IdentityProjectAdditionalPrivilegeModifySection } from "./IdentityProjectAdditionalPrivilegeModifySection";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
identityMembershipDetails: IdentityMembership;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDetails }: Props) => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"deletePrivilege",
|
||||||
|
"modifyPrivilege"
|
||||||
|
] as const);
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
const identityId = identityMembershipDetails?.identity?.id;
|
||||||
|
const projectId = identityMembershipDetails?.project?.id;
|
||||||
|
|
||||||
|
const { mutateAsync: deletePrivilege } = useDeleteIdentityProjectAdditionalPrivilege();
|
||||||
|
|
||||||
|
const { data: userProjectPrivileges, isLoading } = useListIdentityProjectPrivileges({
|
||||||
|
identityId: identityMembershipDetails?.identity?.id,
|
||||||
|
projectId: identityMembershipDetails?.project?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePrivilegeDelete = async () => {
|
||||||
|
const { id } = popUp?.deletePrivilege?.data as { id: string };
|
||||||
|
try {
|
||||||
|
await deletePrivilege({
|
||||||
|
privilegeId: id,
|
||||||
|
projectId,
|
||||||
|
identityId
|
||||||
|
});
|
||||||
|
createNotification({ type: "success", text: "Successfully removed the privilege" });
|
||||||
|
handlePopUpClose("deletePrivilege");
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({ type: "error", text: "Failed to delete role" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<AnimatePresence>
|
||||||
|
{popUp?.modifyPrivilege.isOpen ? (
|
||||||
|
<motion.div
|
||||||
|
key="privilege-modify"
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
className="absolute min-h-[10rem] w-full"
|
||||||
|
>
|
||||||
|
<IdentityProjectAdditionalPrivilegeModifySection
|
||||||
|
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||||
|
identityId={identityId}
|
||||||
|
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||||
|
isDisabled={permission.cannot(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="privilege-list"
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
initial={{ opacity: 0, translateX: 0 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
|
className="absolute w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">
|
||||||
|
Project Additional Privilege
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Identity}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="New privilege"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("modifyPrivilege");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
<div className="py-4">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Type</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && (
|
||||||
|
<TableSkeleton columns={3} innerKey="user-project-identity-memberships" />
|
||||||
|
)}
|
||||||
|
{!isLoading &&
|
||||||
|
userProjectPrivileges?.map((privilegeDetails) => {
|
||||||
|
const isTemporary = privilegeDetails?.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
privilegeDetails.isTemporary &&
|
||||||
|
new Date() > new Date(privilegeDetails.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
|
let text = "Permanent";
|
||||||
|
let toolTipText = "Non expiry access";
|
||||||
|
if (privilegeDetails.isTemporary) {
|
||||||
|
if (isExpired) {
|
||||||
|
text = "Access Expired";
|
||||||
|
toolTipText = "Timed Access Expired";
|
||||||
|
} else {
|
||||||
|
text = formatDistance(
|
||||||
|
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
toolTipText = `Until ${format(
|
||||||
|
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||||
|
"yyyy-MM-dd hh:mm:ss aaa"
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr
|
||||||
|
key={`user-project-privilege-${privilegeDetails?.id}`}
|
||||||
|
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)}
|
||||||
|
>
|
||||||
|
<Td className="capitalize">{privilegeDetails.slug}</Td>
|
||||||
|
<Td>
|
||||||
|
<Tooltip asChild={false} content={toolTipText}>
|
||||||
|
<Tag
|
||||||
|
className={twMerge(
|
||||||
|
"capitalize",
|
||||||
|
isTemporary && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Identity}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Remove role"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
handlePopUpOpen("deletePrivilege", {
|
||||||
|
id: privilegeDetails?.id,
|
||||||
|
slug: privilegeDetails?.slug
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && !userProjectPrivileges?.length && (
|
||||||
|
<EmptyState title="This user has no additional privileges" icon={faFolder} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deletePrivilege.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title={`Do you want to remove privilege ${
|
||||||
|
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||||
|
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { IdentityProjectAdditionalPrivilegeSection } from "./IdentityProjectAdditionalPrivilegeSection";
|
||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format, formatDistance } from "date-fns";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
Tag,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useUpdateIdentityWorkspaceRole } from "@app/hooks/api";
|
||||||
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
|
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
|
import { IdentityRoleModify } from "./IdentityRoleModify";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
identityMembershipDetails: IdentityMembership;
|
||||||
|
isMembershipDetailsLoading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityRoleDetailsSection = ({
|
||||||
|
identityMembershipDetails,
|
||||||
|
isMembershipDetailsLoading
|
||||||
|
}: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
||||||
|
"deleteRole",
|
||||||
|
"modifyRole"
|
||||||
|
] as const);
|
||||||
|
const { mutateAsync: updateIdentityWorkspaceRole } = useUpdateIdentityWorkspaceRole();
|
||||||
|
|
||||||
|
const handleRoleDelete = async () => {
|
||||||
|
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||||
|
try {
|
||||||
|
const updatedRole = identityMembershipDetails?.roles?.filter((el) => el.id !== id);
|
||||||
|
await updateIdentityWorkspaceRole({
|
||||||
|
workspaceId: currentWorkspace?.id || "",
|
||||||
|
identityId: identityMembershipDetails.identity.id,
|
||||||
|
roles: updatedRole
|
||||||
|
});
|
||||||
|
createNotification({ type: "success", text: "Successfully removed the role" });
|
||||||
|
handlePopUpClose("deleteRole");
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
createNotification({ type: "error", text: "Failed to delete role" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4 w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||||
|
<h3 className="text-lg font-semibold text-mineshaft-100">Project Roles</h3>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Identity}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Edit role"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("modifyRole");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
<div className="py-4">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Role</Th>
|
||||||
|
<Th>Type</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isMembershipDetailsLoading && (
|
||||||
|
<TableSkeleton columns={3} innerKey="user-project-identities" />
|
||||||
|
)}
|
||||||
|
{!isMembershipDetailsLoading &&
|
||||||
|
identityMembershipDetails?.roles?.map((roleDetails) => {
|
||||||
|
const isTemporary = roleDetails?.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
roleDetails.isTemporary &&
|
||||||
|
new Date() > new Date(roleDetails.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
|
let text = "Permanent";
|
||||||
|
let toolTipText = "Non expiry access";
|
||||||
|
if (roleDetails.isTemporary) {
|
||||||
|
if (isExpired) {
|
||||||
|
text = "Access Expired";
|
||||||
|
toolTipText = "Timed Access Expired";
|
||||||
|
} else {
|
||||||
|
text = formatDistance(
|
||||||
|
new Date(roleDetails.temporaryAccessEndTime || ""),
|
||||||
|
new Date()
|
||||||
|
);
|
||||||
|
toolTipText = `Until ${format(
|
||||||
|
new Date(roleDetails.temporaryAccessEndTime || ""),
|
||||||
|
"yyyy-MM-dd hh:mm:ss aaa"
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr className="group h-10" key={`user-project-identity-${roleDetails?.id}`}>
|
||||||
|
<Td className="capitalize">
|
||||||
|
{roleDetails.role === "custom"
|
||||||
|
? roleDetails.customRoleName
|
||||||
|
: formatProjectRoleName(roleDetails.role)}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Tooltip asChild={false} content={toolTipText}>
|
||||||
|
<Tag
|
||||||
|
className={twMerge(
|
||||||
|
"capitalize",
|
||||||
|
isTemporary && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</Tag>
|
||||||
|
</Tooltip>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={ProjectPermissionSub.Member}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Remove role"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
variant="plain"
|
||||||
|
className="group relative"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("deleteRole", {
|
||||||
|
id: roleDetails?.id,
|
||||||
|
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isMembershipDetailsLoading && !identityMembershipDetails?.roles?.length && (
|
||||||
|
<EmptyState title="This user has no roles" icon={faFolder} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteRole.isOpen}
|
||||||
|
deleteKey="remove"
|
||||||
|
title={`Do you want to remove role ${(popUp?.deleteRole?.data as TProjectRole)?.slug}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteRole", isOpen)}
|
||||||
|
onDeleteApproved={() => handleRoleDelete()}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.modifyRole.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("modifyRole", isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title="Roles" subTitle="Select one of the pre-defined or custom roles.">
|
||||||
|
<IdentityRoleModify identityProjectMembership={identityMembershipDetails} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+332
@@ -0,0 +1,332 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
|
import { faCaretDown, faClock, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { format, formatDistance } from "date-fns";
|
||||||
|
import ms from "ms";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Spinner,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api";
|
||||||
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
const roleFormSchema = z.object({
|
||||||
|
roles: z
|
||||||
|
.object({
|
||||||
|
slug: z.string(),
|
||||||
|
temporaryAccess: z.discriminatedUnion("isTemporary", [
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(true),
|
||||||
|
temporaryRange: z.string().min(1),
|
||||||
|
temporaryAccessStartTime: z.string().datetime(),
|
||||||
|
temporaryAccessEndTime: z.string().datetime().nullable().optional()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
isTemporary: z.literal(false)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
});
|
||||||
|
type TRoleForm = z.infer<typeof roleFormSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
identityProjectMembership: IdentityMembership;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityRoleModify = ({ identityProjectMembership }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
const isIdentityEditDisabled = permission.cannot(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.Identity
|
||||||
|
);
|
||||||
|
|
||||||
|
const roleForm = useForm<TRoleForm>({
|
||||||
|
resolver: zodResolver(roleFormSchema),
|
||||||
|
values: {
|
||||||
|
roles: identityProjectMembership?.roles?.map(({ customRoleSlug, role, ...dto }) => ({
|
||||||
|
slug: customRoleSlug || role,
|
||||||
|
temporaryAccess: dto.isTemporary
|
||||||
|
? {
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryRange: dto.temporaryRange,
|
||||||
|
temporaryAccessEndTime: dto.temporaryAccessEndTime,
|
||||||
|
temporaryAccessStartTime: dto.temporaryAccessStartTime
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
isTemporary: dto.isTemporary
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const selectedRoleList = useFieldArray({
|
||||||
|
name: "roles",
|
||||||
|
control: roleForm.control
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRoleField = roleForm.watch("roles");
|
||||||
|
|
||||||
|
const updateIdentityWorkspaceRole = useUpdateIdentityWorkspaceRole();
|
||||||
|
|
||||||
|
const handleRoleUpdate = async (data: TRoleForm) => {
|
||||||
|
if (updateIdentityWorkspaceRole.isLoading) return;
|
||||||
|
|
||||||
|
const sanitizedRoles = data.roles.map((el) => {
|
||||||
|
const { isTemporary } = el.temporaryAccess;
|
||||||
|
if (!isTemporary) {
|
||||||
|
return { role: el.slug, isTemporary: false as const };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
role: el.slug,
|
||||||
|
isTemporary: true as const,
|
||||||
|
temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
|
||||||
|
temporaryRange: el.temporaryAccess.temporaryRange,
|
||||||
|
temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateIdentityWorkspaceRole.mutateAsync({
|
||||||
|
workspaceId,
|
||||||
|
identityId: identityProjectMembership.identity.id,
|
||||||
|
roles: sanitizedRoles
|
||||||
|
});
|
||||||
|
createNotification({ text: "Successfully updated roles", type: "success" });
|
||||||
|
} catch (err) {
|
||||||
|
createNotification({ text: "Failed to update role", type: "error" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isRolesLoading)
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center p-8">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={roleForm.handleSubmit(handleRoleUpdate)}>
|
||||||
|
<div className="mt-2 flex flex-col space-y-2">
|
||||||
|
{selectedRoleList.fields.map(({ id }, index) => {
|
||||||
|
const { temporaryAccess } = formRoleField[index];
|
||||||
|
const isTemporary = temporaryAccess?.isTemporary;
|
||||||
|
const isExpired =
|
||||||
|
temporaryAccess.isTemporary &&
|
||||||
|
new Date() > new Date(temporaryAccess.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={id} className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={roleForm.control}
|
||||||
|
name={`roles.${index}.slug`}
|
||||||
|
render={({ field: { onChange, ...field } }) => (
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
isDisabled={isIdentityEditDisabled}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full bg-mineshaft-600 duration-200 hover:bg-mineshaft-500"
|
||||||
|
containerClassName="w-1/2"
|
||||||
|
>
|
||||||
|
{projectRoles?.map(({ name, slug, id: projectRoleId }) => (
|
||||||
|
<SelectItem value={slug} key={projectRoleId}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger disabled={isIdentityEditDisabled} asChild>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
temporaryAccess?.isTemporary
|
||||||
|
? isExpired
|
||||||
|
? "Timed Access Expired"
|
||||||
|
: `Until ${format(
|
||||||
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
|
)}`
|
||||||
|
: "Non expiry access"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
|
||||||
|
isDisabled={isIdentityEditDisabled}
|
||||||
|
className={twMerge(
|
||||||
|
"w-full border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
|
||||||
|
isTemporary && "text-primary",
|
||||||
|
isExpired && "text-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{temporaryAccess?.isTemporary
|
||||||
|
? isExpired
|
||||||
|
? "Access Expired"
|
||||||
|
: formatDistance(
|
||||||
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
|
new Date()
|
||||||
|
)
|
||||||
|
: "Permanent"}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
arrowClassName="fill-gray-600"
|
||||||
|
side="right"
|
||||||
|
sideOffset={12}
|
||||||
|
hideCloseBtn
|
||||||
|
className="border border-gray-600 pt-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
||||||
|
Configure timed access
|
||||||
|
</div>
|
||||||
|
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
||||||
|
<Controller
|
||||||
|
control={roleForm.control}
|
||||||
|
defaultValue="1h"
|
||||||
|
name={`roles.${index}.temporaryAccess.temporaryRange`}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={<TtlFormLabel label="Validity" />}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
onClick={() => {
|
||||||
|
const temporaryRange = roleForm.getValues(
|
||||||
|
`roles.${index}.temporaryAccess.temporaryRange`
|
||||||
|
);
|
||||||
|
if (!temporaryRange) {
|
||||||
|
roleForm.setError(
|
||||||
|
`roles.${index}.temporaryAccess.temporaryRange`,
|
||||||
|
{ type: "required", message: "Required" },
|
||||||
|
{ shouldFocus: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
roleForm.clearErrors(`roles.${index}.temporaryAccess.temporaryRange`);
|
||||||
|
roleForm.setValue(
|
||||||
|
`roles.${index}.temporaryAccess`,
|
||||||
|
{
|
||||||
|
isTemporary: true,
|
||||||
|
temporaryAccessStartTime: new Date().toISOString(),
|
||||||
|
temporaryRange,
|
||||||
|
temporaryAccessEndTime: new Date(
|
||||||
|
new Date().getTime() + ms(temporaryRange)
|
||||||
|
).toISOString()
|
||||||
|
},
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{temporaryAccess.isTemporary ? "Restart" : "Grant"}
|
||||||
|
</Button>
|
||||||
|
{temporaryAccess.isTemporary && (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="outline_bg"
|
||||||
|
colorSchema="danger"
|
||||||
|
onClick={() => {
|
||||||
|
roleForm.setValue(`roles.${index}.temporaryAccess`, {
|
||||||
|
isTemporary: false
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Revoke Access
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<IconButton
|
||||||
|
variant="outline_bg"
|
||||||
|
className="border border-mineshaft-500 bg-mineshaft-600 py-3 hover:border-red/70 hover:bg-red/20"
|
||||||
|
ariaLabel="delete-role"
|
||||||
|
isDisabled={isIdentityEditDisabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedRoleList.fields.length > 1) {
|
||||||
|
selectedRoleList.remove(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-between space-x-2">
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Member}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() =>
|
||||||
|
selectedRoleList.append({
|
||||||
|
slug: ProjectMembershipRole.Member,
|
||||||
|
temporaryAccess: { isTemporary: false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add Role
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className={twMerge(
|
||||||
|
"transition-all",
|
||||||
|
"cursor-default opacity-0",
|
||||||
|
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
||||||
|
)}
|
||||||
|
isDisabled={!roleForm.formState.isDirty}
|
||||||
|
isLoading={roleForm.formState.isSubmitting}
|
||||||
|
>
|
||||||
|
Save Roles
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { IdentityRoleDetailsSection } from "./IdentityRoleDetailsSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { IdentityDetailsPage } from "./IdentityDetailPage";
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
@@ -65,6 +66,7 @@ const formatRoleName = (role: string, customRoleName?: string) => {
|
|||||||
export const IdentityTab = withProjectPermission(
|
export const IdentityTab = withProjectPermission(
|
||||||
() => {
|
() => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
offset,
|
offset,
|
||||||
@@ -239,7 +241,18 @@ export const IdentityTab = withProjectPermission(
|
|||||||
createdAt
|
createdAt
|
||||||
} = identityMember;
|
} = identityMember;
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`st-v3-${id}`}>
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
key={`st-v3-${id}`}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(evt) => {
|
||||||
|
if (evt.key === "Enter") {
|
||||||
|
router.push(`/project/${workspaceId}/identities/${id}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => router.push(`/project/${workspaceId}/identities/${id}`)}
|
||||||
|
>
|
||||||
<Td>{name}</Td>
|
<Td>{name}</Td>
|
||||||
|
|
||||||
<Td>
|
<Td>
|
||||||
@@ -340,9 +353,11 @@ export const IdentityTab = withProjectPermission(
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="update-role"
|
ariaLabel="update-role"
|
||||||
onClick={() =>
|
onClick={(evt) => {
|
||||||
handlePopUpOpen("updateRole", { ...identityMember, index })
|
evt.stopPropagation();
|
||||||
}
|
evt.preventDefault();
|
||||||
|
handlePopUpOpen("updateRole", { ...identityMember, index });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -357,7 +372,9 @@ export const IdentityTab = withProjectPermission(
|
|||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={(evt) => {
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.preventDefault();
|
||||||
handlePopUpOpen("deleteIdentity", {
|
handlePopUpOpen("deleteIdentity", {
|
||||||
identityId: id,
|
identityId: id,
|
||||||
name
|
name
|
||||||
|
|||||||
+19
-2
@@ -1,20 +1,37 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { Alert, AlertDescription } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
|
|
||||||
import { IdentityRbacSection } from "./IdentityRbacSection";
|
import { IdentityRbacSection } from "./IdentityRbacSection";
|
||||||
import { SpecificPrivilegeSection } from "./SpecificPrivilegeSection";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
identityProjectMember: IdentityMembership;
|
identityProjectMember: IdentityMembership;
|
||||||
onOpenUpgradeModal: (title: string) => void;
|
onOpenUpgradeModal: (title: string) => void;
|
||||||
};
|
};
|
||||||
export const IdentityRoleForm = ({ identityProjectMember, onOpenUpgradeModal }: Props) => {
|
export const IdentityRoleForm = ({ identityProjectMember, onOpenUpgradeModal }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<IdentityRbacSection
|
<IdentityRbacSection
|
||||||
identityProjectMember={identityProjectMember}
|
identityProjectMember={identityProjectMember}
|
||||||
onOpenUpgradeModal={onOpenUpgradeModal}
|
onOpenUpgradeModal={onOpenUpgradeModal}
|
||||||
/>
|
/>
|
||||||
<SpecificPrivilegeSection identityId={identityProjectMember?.identity?.id} />
|
<Alert className="mt-4">
|
||||||
|
<AlertDescription>
|
||||||
|
Additional privileges now offer full permissions and have been moved to a new screen.
|
||||||
|
<br />
|
||||||
|
<Link
|
||||||
|
href={`/project/${currentWorkspace?.id || ""}/identitiesq/${
|
||||||
|
identityProjectMember?.identity?.id
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="cursor-pointer text-primary">Click here to access them.</span>
|
||||||
|
</Link>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
-535
@@ -1,535 +0,0 @@
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
faArrowRotateLeft,
|
|
||||||
faCaretDown,
|
|
||||||
faCheck,
|
|
||||||
faClock,
|
|
||||||
faPlus,
|
|
||||||
faTrash
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { format, formatDistance } from "date-fns";
|
|
||||||
import ms from "ms";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
DeleteActionModal,
|
|
||||||
FormControl,
|
|
||||||
FormLabel,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Spinner,
|
|
||||||
Tag,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
useProjectPermission,
|
|
||||||
useWorkspace
|
|
||||||
} from "@app/context";
|
|
||||||
import { usePopUp } from "@app/hooks";
|
|
||||||
import {
|
|
||||||
TProjectUserPrivilege,
|
|
||||||
useCreateIdentityProjectAdditionalPrivilege,
|
|
||||||
useDeleteIdentityProjectAdditionalPrivilege,
|
|
||||||
useUpdateIdentityProjectAdditionalPrivilege
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { useListIdentityProjectPrivileges } from "@app/hooks/api/identityProjectAdditionalPrivilege/queries";
|
|
||||||
|
|
||||||
const secretPermissionSchema = z.object({
|
|
||||||
secretPath: z.string().optional(),
|
|
||||||
environmentSlug: z.string(),
|
|
||||||
[ProjectPermissionActions.Edit]: z.boolean().optional(),
|
|
||||||
[ProjectPermissionActions.Read]: z.boolean().optional(),
|
|
||||||
[ProjectPermissionActions.Create]: z.boolean().optional(),
|
|
||||||
[ProjectPermissionActions.Delete]: z.boolean().optional(),
|
|
||||||
temporaryAccess: z.discriminatedUnion("isTemporary", [
|
|
||||||
z.object({
|
|
||||||
isTemporary: z.literal(true),
|
|
||||||
temporaryRange: z.string().min(1),
|
|
||||||
temporaryAccessStartTime: z.string().datetime(),
|
|
||||||
temporaryAccessEndTime: z.string().datetime().nullable().optional()
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
isTemporary: z.literal(false)
|
|
||||||
})
|
|
||||||
])
|
|
||||||
});
|
|
||||||
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
|
|
||||||
const SpecificPrivilegeSecretForm = ({
|
|
||||||
privilege,
|
|
||||||
identityId
|
|
||||||
}: {
|
|
||||||
privilege: TProjectUserPrivilege;
|
|
||||||
identityId: string;
|
|
||||||
}) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
|
|
||||||
"deletePrivilege"
|
|
||||||
] as const);
|
|
||||||
const { permission } = useProjectPermission();
|
|
||||||
const isMemberEditDisabled = permission.cannot(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateIdentityPrivilege = useUpdateIdentityProjectAdditionalPrivilege();
|
|
||||||
const deleteIdentityPrivilege = useDeleteIdentityProjectAdditionalPrivilege();
|
|
||||||
|
|
||||||
const privilegeForm = useForm<TSecretPermissionForm>({
|
|
||||||
resolver: zodResolver(secretPermissionSchema),
|
|
||||||
values: {
|
|
||||||
environmentSlug: privilege.permissions?.[0]?.conditions?.environment,
|
|
||||||
// secret path will be inside $glob operator
|
|
||||||
secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "",
|
|
||||||
read: privilege.permissions?.some(({ action }) =>
|
|
||||||
action.includes(ProjectPermissionActions.Read)
|
|
||||||
),
|
|
||||||
edit: privilege.permissions?.some(({ action }) =>
|
|
||||||
action.includes(ProjectPermissionActions.Edit)
|
|
||||||
),
|
|
||||||
create: privilege.permissions?.some(({ action }) =>
|
|
||||||
action.includes(ProjectPermissionActions.Create)
|
|
||||||
),
|
|
||||||
delete: privilege.permissions?.some(({ action }) =>
|
|
||||||
action.includes(ProjectPermissionActions.Delete)
|
|
||||||
),
|
|
||||||
// zod will pick it
|
|
||||||
temporaryAccess: privilege
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const temporaryAccessField = privilegeForm.watch("temporaryAccess");
|
|
||||||
const selectedEnvironmentSlug = privilegeForm.watch("environmentSlug");
|
|
||||||
const isTemporary = temporaryAccessField?.isTemporary;
|
|
||||||
const isExpired =
|
|
||||||
temporaryAccessField.isTemporary &&
|
|
||||||
new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || "");
|
|
||||||
|
|
||||||
const handleUpdatePrivilege = async (data: TSecretPermissionForm) => {
|
|
||||||
if (updateIdentityPrivilege.isLoading) return;
|
|
||||||
try {
|
|
||||||
const actions = [
|
|
||||||
{ action: ProjectPermissionActions.Read, allowed: data.read },
|
|
||||||
{ action: ProjectPermissionActions.Create, allowed: data.create },
|
|
||||||
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
|
||||||
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
|
||||||
];
|
|
||||||
await updateIdentityPrivilege.mutateAsync({
|
|
||||||
privilegeDetails: {
|
|
||||||
...data.temporaryAccess,
|
|
||||||
privilegePermission: {
|
|
||||||
actions: actions.filter(({ allowed }) => allowed).map(({ action }) => action),
|
|
||||||
subject: ProjectPermissionSub.Secrets,
|
|
||||||
conditions: {
|
|
||||||
environment: data.environmentSlug,
|
|
||||||
...(data.secretPath ? { secretPath: { $glob: data.secretPath } } : {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
privilegeSlug: privilege.slug,
|
|
||||||
identityId,
|
|
||||||
projectSlug
|
|
||||||
});
|
|
||||||
createNotification({
|
|
||||||
type: "success",
|
|
||||||
text: "Successfully updated privilege"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to update privilege"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePrivilege = async () => {
|
|
||||||
if (deleteIdentityPrivilege.isLoading) return;
|
|
||||||
try {
|
|
||||||
await deleteIdentityPrivilege.mutateAsync({
|
|
||||||
identityId,
|
|
||||||
privilegeSlug: privilege.slug,
|
|
||||||
projectSlug
|
|
||||||
});
|
|
||||||
createNotification({
|
|
||||||
type: "success",
|
|
||||||
text: "Successfully deleted privilege"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to delete privilege"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAccessLabel = (exactTime = false) => {
|
|
||||||
if (isExpired) return "Access expired";
|
|
||||||
if (!temporaryAccessField?.isTemporary) return "Permanent";
|
|
||||||
if (exactTime)
|
|
||||||
return `Until ${format(
|
|
||||||
new Date(temporaryAccessField.temporaryAccessEndTime || ""),
|
|
||||||
"yyyy-MM-dd HH:mm:ss"
|
|
||||||
)}`;
|
|
||||||
return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-4">
|
|
||||||
<form onSubmit={privilegeForm.handleSubmit(handleUpdatePrivilege)}>
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<Controller
|
|
||||||
control={privilegeForm.control}
|
|
||||||
name="environmentSlug"
|
|
||||||
render={({ field: { onChange, ...field } }) => (
|
|
||||||
<FormControl label="Env">
|
|
||||||
<Select
|
|
||||||
{...field}
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
className="bg-mineshaft-600 hover:bg-mineshaft-500"
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
>
|
|
||||||
{currentWorkspace?.environments?.map(({ slug, id }) => (
|
|
||||||
<SelectItem value={slug} key={id}>
|
|
||||||
{slug}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={privilegeForm.control}
|
|
||||||
name="secretPath"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormControl label="Secret Path">
|
|
||||||
<SecretPathInput
|
|
||||||
{...field}
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
environment={selectedEnvironmentSlug}
|
|
||||||
containerClassName="w-48"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-grow justify-between">
|
|
||||||
<Controller
|
|
||||||
control={privilegeForm.control}
|
|
||||||
name="read"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<FormLabel label="View" className="mb-4" />
|
|
||||||
<Checkbox
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
id="secret-read"
|
|
||||||
className="h-5 w-5"
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={privilegeForm.control}
|
|
||||||
name="create"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<FormLabel label="Create" className="mb-4" />
|
|
||||||
<Checkbox
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
id="secret-create"
|
|
||||||
className="h-5 w-5"
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={privilegeForm.control}
|
|
||||||
name="edit"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<FormLabel label="Modify" className="mb-4" />
|
|
||||||
<Checkbox
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
id="secret-modify"
|
|
||||||
className="h-5 w-5"
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={privilegeForm.control}
|
|
||||||
name="delete"
|
|
||||||
render={({ field }) => (
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<FormLabel label="Delete" className="mb-4" />
|
|
||||||
<Checkbox
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
id="secret-delete"
|
|
||||||
className="h-5 w-5"
|
|
||||||
isChecked={field.value}
|
|
||||||
onCheckedChange={(isChecked) => field.onChange(isChecked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-7 flex items-center space-x-2">
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger disabled={isMemberEditDisabled}>
|
|
||||||
<div>
|
|
||||||
<Tooltip content={getAccessLabel(true)}>
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
leftIcon={isTemporary ? <FontAwesomeIcon icon={faClock} /> : undefined}
|
|
||||||
rightIcon={<FontAwesomeIcon icon={faCaretDown} className="ml-2" />}
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
className={twMerge(
|
|
||||||
"border-none bg-mineshaft-600 py-2.5 text-xs capitalize hover:bg-mineshaft-500",
|
|
||||||
isTemporary && "text-primary",
|
|
||||||
isExpired && "text-red-600"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{getAccessLabel()}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
arrowClassName="fill-gray-600"
|
|
||||||
side="right"
|
|
||||||
sideOffset={12}
|
|
||||||
hideCloseBtn
|
|
||||||
className="border border-gray-600 pt-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<div className="border-b border-b-gray-700 pb-2 text-sm text-mineshaft-300">
|
|
||||||
Configure timed access
|
|
||||||
</div>
|
|
||||||
{isExpired && <Tag colorSchema="red">Expired</Tag>}
|
|
||||||
<Controller
|
|
||||||
control={privilegeForm.control}
|
|
||||||
defaultValue="1h"
|
|
||||||
name="temporaryAccess.temporaryRange"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label={<TtlFormLabel label="Validity" />}
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
onClick={() => {
|
|
||||||
const temporaryRange = privilegeForm.getValues(
|
|
||||||
"temporaryAccess.temporaryRange"
|
|
||||||
);
|
|
||||||
if (!temporaryRange) {
|
|
||||||
privilegeForm.setError(
|
|
||||||
"temporaryAccess.temporaryRange",
|
|
||||||
{ type: "required", message: "Required" },
|
|
||||||
{ shouldFocus: true }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
privilegeForm.clearErrors("temporaryAccess.temporaryRange");
|
|
||||||
privilegeForm.setValue(
|
|
||||||
"temporaryAccess",
|
|
||||||
{
|
|
||||||
isTemporary: true,
|
|
||||||
temporaryAccessStartTime: new Date().toISOString(),
|
|
||||||
temporaryRange,
|
|
||||||
temporaryAccessEndTime: new Date(
|
|
||||||
new Date().getTime() + ms(temporaryRange)
|
|
||||||
).toISOString()
|
|
||||||
},
|
|
||||||
{ shouldDirty: true }
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{temporaryAccessField.isTemporary ? "Restart" : "Grant"}
|
|
||||||
</Button>
|
|
||||||
{temporaryAccessField.isTemporary && (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="outline_bg"
|
|
||||||
colorSchema="danger"
|
|
||||||
onClick={() => {
|
|
||||||
privilegeForm.setValue("temporaryAccess", {
|
|
||||||
isTemporary: false
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Revoke Access
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
{privilegeForm.formState.isDirty ? (
|
|
||||||
<>
|
|
||||||
<Tooltip content="Cancel" className="mr-4">
|
|
||||||
<IconButton
|
|
||||||
variant="outline_bg"
|
|
||||||
className="border border-mineshaft-500 bg-mineshaft-600 py-2.5 hover:border-red/70 hover:bg-red/20"
|
|
||||||
ariaLabel="delete-privilege"
|
|
||||||
isDisabled={privilegeForm.formState.isSubmitting}
|
|
||||||
onClick={() => privilegeForm.reset()}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faArrowRotateLeft} className="py-0.5" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
content={isMemberEditDisabled ? "Access restricted" : "Save"}
|
|
||||||
className="mr-4"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
className="border-none py-3"
|
|
||||||
ariaLabel="save-privilege"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
{privilegeForm.formState.isSubmitting ? (
|
|
||||||
<Spinner size="xs" className="m-0 h-3 w-3 text-slate-500" />
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon icon={faCheck} className="px-0.5" />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Tooltip
|
|
||||||
content={isMemberEditDisabled ? "Access restricted" : "Delete"}
|
|
||||||
className="mr-4"
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
isDisabled={isMemberEditDisabled}
|
|
||||||
variant="outline_bg"
|
|
||||||
className="border border-mineshaft-500 bg-mineshaft-600 py-3 hover:border-red/70 hover:bg-red/20"
|
|
||||||
ariaLabel="delete-privilege"
|
|
||||||
onClick={() => handlePopUpOpen("deletePrivilege")}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.deletePrivilege.isOpen}
|
|
||||||
title="Remove user additional privilege"
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
|
||||||
deleteKey="delete"
|
|
||||||
onClose={() => handlePopUpClose("deletePrivilege")}
|
|
||||||
onDeleteApproved={handleDeletePrivilege}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
identityId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpecificPrivilegeSection = ({ identityId }: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
|
||||||
const { data: identityPrivileges, isLoading } = useListIdentityProjectPrivileges({
|
|
||||||
identityId,
|
|
||||||
projectSlug
|
|
||||||
});
|
|
||||||
|
|
||||||
const createIdentityPrivilege = useCreateIdentityProjectAdditionalPrivilege();
|
|
||||||
|
|
||||||
const handleCreatePrivilege = async () => {
|
|
||||||
if (createIdentityPrivilege.isLoading) return;
|
|
||||||
try {
|
|
||||||
await createIdentityPrivilege.mutateAsync({
|
|
||||||
privilegePermission: {
|
|
||||||
actions: [ProjectPermissionActions.Read],
|
|
||||||
subject: ProjectPermissionSub.Secrets,
|
|
||||||
conditions: {
|
|
||||||
environment: currentWorkspace?.environments?.[0].slug as string
|
|
||||||
}
|
|
||||||
},
|
|
||||||
identityId,
|
|
||||||
projectSlug
|
|
||||||
});
|
|
||||||
createNotification({
|
|
||||||
type: "success",
|
|
||||||
text: "Successfully created privilege"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to create privilege"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 pt-6">
|
|
||||||
<div className="flex items-center space-x-2 text-lg font-medium">
|
|
||||||
Additional Privileges
|
|
||||||
{isLoading && <Spinner size="xs" />}
|
|
||||||
</div>
|
|
||||||
<p className="mt-0.5 text-sm text-mineshaft-400">
|
|
||||||
Select individual privileges to associate with the identity.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
{identityPrivileges
|
|
||||||
?.filter(({ permissions }) =>
|
|
||||||
permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets)
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
|
||||||
?.map((privilege) => (
|
|
||||||
<SpecificPrivilegeSecretForm
|
|
||||||
privilege={privilege as TProjectUserPrivilege}
|
|
||||||
identityId={identityId}
|
|
||||||
key={privilege?.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Identity}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
className="mt-4"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={handleCreatePrivilege}
|
|
||||||
isLoading={createIdentityPrivilege.isLoading}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Add additional privilege
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+2
-134
@@ -6,7 +6,6 @@ import {
|
|||||||
faCheck,
|
faCheck,
|
||||||
faClock,
|
faClock,
|
||||||
faLockOpen,
|
faLockOpen,
|
||||||
faPlus,
|
|
||||||
faTrash
|
faTrash
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@@ -18,7 +17,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { TtlFormLabel } from "@app/components/features";
|
import { TtlFormLabel } from "@app/components/features";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -48,10 +46,7 @@ import { usePopUp } from "@app/hooks";
|
|||||||
import {
|
import {
|
||||||
TProjectUserPrivilege,
|
TProjectUserPrivilege,
|
||||||
useCreateAccessRequest,
|
useCreateAccessRequest,
|
||||||
useCreateProjectUserAdditionalPrivilege,
|
useDeleteProjectUserAdditionalPrivilege
|
||||||
useDeleteProjectUserAdditionalPrivilege,
|
|
||||||
useListProjectUserPrivileges,
|
|
||||||
useUpdateProjectUserAdditionalPrivilege
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
import { TAccessApprovalPolicy } from "@app/hooks/api/types";
|
||||||
|
|
||||||
@@ -94,7 +89,6 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
const isMemberEditDisabled =
|
const isMemberEditDisabled =
|
||||||
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege;
|
permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Member) && !!privilege;
|
||||||
|
|
||||||
const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege();
|
|
||||||
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege();
|
||||||
const requestAccess = useCreateAccessRequest();
|
const requestAccess = useCreateAccessRequest();
|
||||||
|
|
||||||
@@ -165,53 +159,6 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
temporaryAccessField.isTemporary &&
|
temporaryAccessField.isTemporary &&
|
||||||
new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || "");
|
new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || "");
|
||||||
|
|
||||||
const handleUpdatePrivilege = async (data: TSecretPermissionForm) => {
|
|
||||||
if (!privilege) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "No privilege to update found.",
|
|
||||||
title: "Error"
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateUserPrivilege.isLoading) return;
|
|
||||||
try {
|
|
||||||
const actions = [
|
|
||||||
{ action: ProjectPermissionActions.Read, allowed: data.read },
|
|
||||||
{ action: ProjectPermissionActions.Create, allowed: data.create },
|
|
||||||
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
|
||||||
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
|
||||||
];
|
|
||||||
const conditions: { environment: string; secretPath?: { $glob: string } } = {
|
|
||||||
environment: data.environmentSlug
|
|
||||||
};
|
|
||||||
if (data.secretPath) {
|
|
||||||
conditions.secretPath = { $glob: removeTrailingSlash(data.secretPath) };
|
|
||||||
}
|
|
||||||
await updateUserPrivilege.mutateAsync({
|
|
||||||
privilegeId: privilege.id,
|
|
||||||
...data.temporaryAccess,
|
|
||||||
permissions: {
|
|
||||||
subject: ProjectPermissionSub.Secrets,
|
|
||||||
conditions,
|
|
||||||
actions: actions.filter((i) => i.allowed).map((i) => i.action)
|
|
||||||
},
|
|
||||||
projectMembershipId: privilege.projectMembershipId
|
|
||||||
});
|
|
||||||
createNotification({
|
|
||||||
type: "success",
|
|
||||||
text: "Successfully updated privilege"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to update privilege"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePrivilege = async () => {
|
const handleDeletePrivilege = async () => {
|
||||||
if (!privilege) {
|
if (!privilege) {
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -296,11 +243,7 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (data: TSecretPermissionForm) => {
|
const handleSubmit = async (data: TSecretPermissionForm) => {
|
||||||
if (privilege) {
|
handleRequestAccess(data);
|
||||||
handleUpdatePrivilege(data);
|
|
||||||
} else {
|
|
||||||
handleRequestAccess(data);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAccessLabel = (exactTime = false) => {
|
const getAccessLabel = (exactTime = false) => {
|
||||||
@@ -627,78 +570,3 @@ export const SpecificPrivilegeSecretForm = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
|
||||||
membershipId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SpecificPrivilegeSection = ({ membershipId }: Props) => {
|
|
||||||
const { data: userPrivileges, isLoading } = useListProjectUserPrivileges(membershipId);
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
|
|
||||||
const createUserPrivilege = useCreateProjectUserAdditionalPrivilege();
|
|
||||||
|
|
||||||
const handleCreatePrivilege = async () => {
|
|
||||||
if (createUserPrivilege.isLoading) return;
|
|
||||||
try {
|
|
||||||
await createUserPrivilege.mutateAsync({
|
|
||||||
permissions: {
|
|
||||||
actions: [ProjectPermissionActions.Read],
|
|
||||||
subject: ProjectPermissionSub.Secrets,
|
|
||||||
conditions: {
|
|
||||||
environment: currentWorkspace?.environments?.[0].slug || ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
projectMembershipId: membershipId
|
|
||||||
});
|
|
||||||
createNotification({
|
|
||||||
type: "success",
|
|
||||||
text: "Successfully created privilege"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
type: "error",
|
|
||||||
text: "Failed to create privilege"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-6 border-t border-t-mineshaft-600 pt-6">
|
|
||||||
<div className="flex items-center space-x-2 text-lg font-medium">
|
|
||||||
Additional Privileges
|
|
||||||
{isLoading && <Spinner size="xs" />}
|
|
||||||
</div>
|
|
||||||
<p className="mt-0.5 text-sm text-mineshaft-400">
|
|
||||||
Select individual privileges to associate with the user.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
{userPrivileges
|
|
||||||
?.filter(({ permissions }) =>
|
|
||||||
permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets)
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.id.localeCompare(b.id))
|
|
||||||
?.map((privilege) => (
|
|
||||||
<SpecificPrivilegeSecretForm
|
|
||||||
privilege={privilege as TProjectUserPrivilege}
|
|
||||||
key={privilege?.id}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Member}>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
className="mt-4"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={handleCreatePrivilege}
|
|
||||||
isLoading={createUserPrivilege.isLoading}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Add additional privilege
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
+1
-1
@@ -115,7 +115,7 @@ export const MembersTable = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Prop
|
|||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
key={`membership-${membershipId}`}
|
key={`membership-${membershipId}`}
|
||||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user