mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 07:27:58 +00:00
feat(infisical-pg): removed _id with id for new backend
This commit is contained in:
@@ -2,14 +2,14 @@ import { TRole } from "../roles/types";
|
|||||||
import { IdentityAuthMethod } from "./enums";
|
import { IdentityAuthMethod } from "./enums";
|
||||||
|
|
||||||
export type IdentityTrustedIp = {
|
export type IdentityTrustedIp = {
|
||||||
_id: string;
|
id: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
type: "ipv4" | "ipv6";
|
type: "ipv4" | "ipv6";
|
||||||
prefix?: number;
|
prefix?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Identity = {
|
export type Identity = {
|
||||||
_id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
authMethod?: IdentityAuthMethod;
|
authMethod?: IdentityAuthMethod;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -17,7 +17,7 @@ export type Identity = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type IdentityMembershipOrg = {
|
export type IdentityMembershipOrg = {
|
||||||
_id: string;
|
id: string;
|
||||||
identity: Identity;
|
identity: Identity;
|
||||||
organization: string;
|
organization: string;
|
||||||
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
||||||
@@ -27,7 +27,7 @@ export type IdentityMembershipOrg = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type IdentityMembership = {
|
export type IdentityMembership = {
|
||||||
_id: string;
|
id: string;
|
||||||
identity: Identity;
|
identity: Identity;
|
||||||
organization: string;
|
organization: string;
|
||||||
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
role: "admin" | "member" | "viewer" | "no-access" | "custom";
|
||||||
@@ -100,7 +100,7 @@ export type CreateIdentityUniversalAuthClientSecretDTO = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ClientSecretData = {
|
export type ClientSecretData = {
|
||||||
_id: string;
|
id: string;
|
||||||
identityUniversalAuth: string;
|
identityUniversalAuth: string;
|
||||||
isClientSecretRevoked: boolean;
|
isClientSecretRevoked: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ export const Navbar = () => {
|
|||||||
|
|
||||||
// direct user to start pro trial
|
// direct user to start pro trial
|
||||||
const url = await mutateAsync({
|
const url = await mutateAsync({
|
||||||
orgId: currentOrg._id,
|
orgId: currentOrg.id,
|
||||||
success_url: window.location.href
|
success_url: window.location.href
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default function RenderCreateIntegrationPage() {
|
|||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
router.push(`/integrations/render/create?integrationAuthId=${integrationAuth._id}`);
|
router.push(`/integrations/render/create?integrationAuthId=${integrationAuth.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ export default function CloudflareWorkersIntegrationPage() {
|
|||||||
|
|
||||||
const handleButtonClick = async () => {
|
const handleButtonClick = async () => {
|
||||||
try {
|
try {
|
||||||
if (!integrationAuth?._id) return;
|
if (!integrationAuth?.id) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
integrationAuthId: integrationAuth?._id,
|
integrationAuthId: integrationAuth?.id,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
app: targetApp,
|
app: targetApp,
|
||||||
appId: targetAppId,
|
appId: targetAppId,
|
||||||
|
|||||||
+190
-207
@@ -5,27 +5,25 @@ import * as yup from "yup";
|
|||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem
|
SelectItem
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import { useCreateIdentity, useGetRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||||
useCreateIdentity,
|
|
||||||
useGetRoles,
|
|
||||||
useUpdateIdentity
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup
|
||||||
|
.object({
|
||||||
name: yup.string().required("MI name is required"),
|
name: yup.string().required("MI name is required"),
|
||||||
role: yup.string(),
|
role: yup.string()
|
||||||
}).required();
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
@@ -34,206 +32,191 @@ type Props = {
|
|||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
data: {
|
data: {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
name: string;
|
name: string;
|
||||||
authMethod?: IdentityAuthMethod;
|
authMethod?: IdentityAuthMethod;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityModal = ({
|
export const IdentityModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
|
||||||
popUp,
|
const { createNotification } = useNotificationContext();
|
||||||
handlePopUpOpen,
|
|
||||||
handlePopUpToggle
|
|
||||||
}: Props) => {
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?._id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
|
|
||||||
const { data: roles } = useGetRoles({
|
const { data: roles } = useGetRoles({
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
const { mutateAsync: createMutateAsync } = useCreateIdentity();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: ""
|
name: ""
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
|
|
||||||
const identity = popUp?.identity?.data as {
|
|
||||||
identityId: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
customRole: {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!roles?.length) return;
|
|
||||||
|
|
||||||
if (identity) {
|
|
||||||
reset({
|
|
||||||
name: identity.name,
|
|
||||||
role: identity?.customRole?.slug ?? identity.role
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reset({
|
|
||||||
name: "",
|
|
||||||
role: roles[0].slug
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [popUp?.identity?.data, roles]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
|
||||||
name,
|
|
||||||
role,
|
|
||||||
}: FormData) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const identity = popUp?.identity?.data as {
|
|
||||||
identityId: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (identity) {
|
|
||||||
// update
|
|
||||||
|
|
||||||
await updateMutateAsync({
|
|
||||||
identityId: identity.identityId,
|
|
||||||
name,
|
|
||||||
role: role || undefined,
|
|
||||||
organizationId: orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
handlePopUpToggle("identity", false);
|
|
||||||
} else {
|
|
||||||
// create
|
|
||||||
|
|
||||||
const {
|
|
||||||
_id: createdId,
|
|
||||||
name: createdName,
|
|
||||||
authMethod
|
|
||||||
} = await createMutateAsync({
|
|
||||||
name,
|
|
||||||
role: role || undefined,
|
|
||||||
organizationId: orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
handlePopUpToggle("identity", false);
|
|
||||||
handlePopUpOpen("identityAuthMethod", {
|
|
||||||
identityId: createdId,
|
|
||||||
name: createdName,
|
|
||||||
authMethod
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} identity`,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
reset();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message
|
|
||||||
?? `Failed to ${popUp?.identity?.data ? "updated" : "created"} identity`;
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<Modal
|
const identity = popUp?.identity?.data as {
|
||||||
isOpen={popUp?.identity?.isOpen}
|
identityId: string;
|
||||||
onOpenChange={(isOpen) => {
|
name: string;
|
||||||
handlePopUpToggle("identity", isOpen);
|
role: string;
|
||||||
reset();
|
customRole: {
|
||||||
}}
|
name: string;
|
||||||
>
|
slug: string;
|
||||||
<ModalContent title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}>
|
};
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
};
|
||||||
<Controller
|
|
||||||
control={control}
|
if (!roles?.length) return;
|
||||||
defaultValue=""
|
|
||||||
name="name"
|
if (identity) {
|
||||||
render={({ field, fieldState: { error } }) => (
|
reset({
|
||||||
<FormControl
|
name: identity.name,
|
||||||
label="Name"
|
role: identity?.customRole?.slug ?? identity.role
|
||||||
isError={Boolean(error)}
|
});
|
||||||
errorText={error?.message}
|
} else {
|
||||||
>
|
reset({
|
||||||
<Input
|
name: "",
|
||||||
{...field}
|
role: roles[0].slug
|
||||||
placeholder="Machine 1"
|
});
|
||||||
/>
|
}
|
||||||
</FormControl>
|
}, [popUp?.identity?.data, roles]);
|
||||||
)}
|
|
||||||
/>
|
const onFormSubmit = async ({ name, role }: FormData) => {
|
||||||
<Controller
|
try {
|
||||||
control={control}
|
const identity = popUp?.identity?.data as {
|
||||||
name="role"
|
identityId: string;
|
||||||
defaultValue=""
|
name: string;
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
role: string;
|
||||||
<FormControl
|
};
|
||||||
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
|
|
||||||
errorText={error?.message}
|
if (identity) {
|
||||||
isError={Boolean(error)}
|
// update
|
||||||
className="mt-4"
|
|
||||||
>
|
await updateMutateAsync({
|
||||||
<Select
|
identityId: identity.identityId,
|
||||||
defaultValue={field.value}
|
name,
|
||||||
{...field}
|
role: role || undefined,
|
||||||
onValueChange={(e) => onChange(e)}
|
organizationId: orgId
|
||||||
className="w-full"
|
});
|
||||||
>
|
|
||||||
{(roles || []).map(({ name, slug }) => (
|
handlePopUpToggle("identity", false);
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
} else {
|
||||||
{name}
|
// create
|
||||||
</SelectItem>
|
|
||||||
))}
|
const {
|
||||||
</Select>
|
id: createdId,
|
||||||
</FormControl>
|
name: createdName,
|
||||||
)}
|
authMethod
|
||||||
/>
|
} = await createMutateAsync({
|
||||||
<div className="flex items-center">
|
name,
|
||||||
<Button
|
role: role || undefined,
|
||||||
className="mr-4"
|
organizationId: orgId
|
||||||
size="sm"
|
});
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
handlePopUpToggle("identity", false);
|
||||||
isDisabled={isSubmitting}
|
handlePopUpOpen("identityAuthMethod", {
|
||||||
>
|
identityId: createdId,
|
||||||
{popUp?.identity?.data ? "Update" : "Create"}
|
name: createdName,
|
||||||
</Button>
|
authMethod
|
||||||
<Button
|
});
|
||||||
colorSchema="secondary"
|
}
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpToggle("identity", false)}
|
createNotification({
|
||||||
>
|
text: `Successfully ${popUp?.identity?.data ? "updated" : "created"} identity`,
|
||||||
Cancel
|
type: "success"
|
||||||
</Button>
|
});
|
||||||
</div>
|
|
||||||
</form>
|
reset();
|
||||||
</ModalContent>
|
} catch (err) {
|
||||||
</Modal>
|
console.error(err);
|
||||||
);
|
const error = err as any;
|
||||||
}
|
const text =
|
||||||
|
error?.response?.data?.message ??
|
||||||
|
`Failed to ${popUp?.identity?.data ? "updated" : "created"} identity`;
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.identity?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("identity", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`${popUp?.identity?.data ? "Update" : "Create"} Identity`}>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} placeholder="Machine 1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="role"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={`${popUp?.identity?.data ? "Update" : ""} Role`}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{(roles || []).map(({ name, slug }) => (
|
||||||
|
<SelectItem value={slug} key={`st-role-${slug}`}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{popUp?.identity?.data ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("identity", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+99
-104
@@ -4,14 +4,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
Button,
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
DeleteActionModal
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
useOrganization} from "@app/context";
|
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission } from "@app/hoc";
|
||||||
import { useDeleteIdentity } from "@app/hooks/api";
|
import { useDeleteIdentity } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
@@ -22,109 +16,110 @@ import { IdentityTable } from "./IdentityTable";
|
|||||||
import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal";
|
import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal";
|
||||||
|
|
||||||
export const IdentitySection = withPermission(
|
export const IdentitySection = withPermission(
|
||||||
() => {
|
() => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?._id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
|
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
|
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"identity",
|
"identity",
|
||||||
"identityAuthMethod",
|
"identityAuthMethod",
|
||||||
"deleteIdentity",
|
"deleteIdentity",
|
||||||
"universalAuthClientSecret",
|
"universalAuthClientSecret",
|
||||||
"deleteUniversalAuthClientSecret",
|
"deleteUniversalAuthClientSecret",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onDeleteIdentitySubmit = async (identityId: string) => {
|
const onDeleteIdentitySubmit = async (identityId: string) => {
|
||||||
try {
|
try {
|
||||||
|
await deleteMutateAsync({
|
||||||
|
identityId,
|
||||||
|
organizationId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
await deleteMutateAsync({
|
createNotification({
|
||||||
identityId,
|
text: "Successfully deleted identity",
|
||||||
organizationId: orgId
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
handlePopUpClose("deleteIdentity");
|
||||||
text: "Successfully deleted identity",
|
} catch (err) {
|
||||||
type: "success"
|
console.error(err);
|
||||||
});
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to delete identity";
|
||||||
|
|
||||||
handlePopUpClose("deleteIdentity");
|
createNotification({
|
||||||
} catch (err) {
|
text,
|
||||||
console.error(err);
|
type: "error"
|
||||||
const error = err as any;
|
});
|
||||||
const text = error?.response?.data?.message
|
}
|
||||||
?? "Failed to delete identity"
|
};
|
||||||
|
|
||||||
createNotification({
|
return (
|
||||||
text,
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
type: "error"
|
<div className="mb-4 flex justify-between">
|
||||||
});
|
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||||
}
|
<div className="flex w-full justify-end pr-4">
|
||||||
}
|
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
|
||||||
|
<a
|
||||||
return (
|
target="_blank"
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
rel="noopener noreferrer"
|
||||||
<div className="flex justify-between mb-4">
|
className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white"
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">
|
>
|
||||||
Identities
|
Documentation{" "}
|
||||||
</p>
|
<FontAwesomeIcon
|
||||||
<div className="flex justify-end w-full pr-4">
|
icon={faArrowUpRightFromSquare}
|
||||||
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
|
className="mb-[0.06rem] ml-1 text-xs"
|
||||||
<a target="_blank" rel="noopener noreferrer" className="rounded-md px-4 py-2 w-max text-mineshaft-200 hover:text-white bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/10 hover:border-primary/40 duration-200 cursor-pointer">
|
|
||||||
Documentation <FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-xs mb-[0.06rem] ml-1"/>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Create}
|
|
||||||
a={OrgPermissionSubjects.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
type="submit"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => handlePopUpOpen("identity")}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Create identity
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
|
||||||
<IdentityModal
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
<IdentityAuthMethodModal
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
<IdentityUniversalAuthClientSecretModal
|
|
||||||
popUp={popUp}
|
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
|
||||||
/>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.deleteIdentity.isOpen}
|
|
||||||
title={`Are you sure want to delete ${
|
|
||||||
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
|
||||||
}?`}
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
|
||||||
deleteKey="confirm"
|
|
||||||
onDeleteApproved={() =>
|
|
||||||
onDeleteIdentitySubmit(
|
|
||||||
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Identity}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("identity")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create identity
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
);
|
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
},
|
<IdentityModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
<IdentityAuthMethodModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
<IdentityUniversalAuthClientSecretModal
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteIdentity.isOpen}
|
||||||
|
title={`Are you sure want to delete ${
|
||||||
|
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
||||||
|
}?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onDeleteIdentitySubmit(
|
||||||
|
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Identity }
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Identity }
|
||||||
);
|
);
|
||||||
|
|||||||
+232
-253
@@ -1,269 +1,248 @@
|
|||||||
import { faKey, faLock,faPencil, faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faKey, faLock, faPencil, faServer, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
OrgPermissionActions,
|
import { useGetIdentityMembershipOrgs, useGetRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||||
OrgPermissionSubjects,
|
|
||||||
useOrganization} from "@app/context";
|
|
||||||
import {
|
|
||||||
useGetIdentityMembershipOrgs,
|
|
||||||
useGetRoles,
|
|
||||||
useUpdateIdentity} from "@app/hooks/api";
|
|
||||||
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
// TODO: some kind of map
|
// TODO: some kind of map
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteIdentity", "identity", "universalAuthClientSecret", "identityAuthMethod"]>,
|
popUpName: keyof UsePopUpState<
|
||||||
data?: {
|
["deleteIdentity", "identity", "universalAuthClientSecret", "identityAuthMethod"]
|
||||||
identityId?: string;
|
>,
|
||||||
name?: string;
|
data?: {
|
||||||
authMethod?: string;
|
identityId?: string;
|
||||||
role?: string;
|
name?: string;
|
||||||
customRole?: {
|
authMethod?: string;
|
||||||
name: string;
|
role?: string;
|
||||||
slug: string;
|
customRole?: {
|
||||||
};
|
name: string;
|
||||||
}
|
slug: string;
|
||||||
) => void;
|
};
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const orgId = currentOrg?.id || "";
|
||||||
|
|
||||||
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||||
|
const { data, isLoading } = useGetIdentityMembershipOrgs(orgId);
|
||||||
|
|
||||||
|
const { data: roles } = useGetRoles({
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
|
||||||
|
try {
|
||||||
|
await updateMutateAsync({
|
||||||
|
identityId,
|
||||||
|
role,
|
||||||
|
organizationId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated identity role",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to update identity role";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityTable = ({
|
return (
|
||||||
handlePopUpOpen
|
<TableContainer>
|
||||||
}: Props) => {
|
<Table>
|
||||||
const { createNotification } = useNotificationContext();
|
<THead>
|
||||||
const { currentOrg } = useOrganization();
|
<Tr>
|
||||||
const orgId = currentOrg?._id || "";
|
<Th>Name</Th>
|
||||||
|
<Th>ID</Th>
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
<Th>Role</Th>
|
||||||
const { data, isLoading } = useGetIdentityMembershipOrgs(orgId);
|
<Th>Auth Method</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
const { data: roles } = useGetRoles({
|
</Tr>
|
||||||
orgId
|
</THead>
|
||||||
});
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} innerKey="org-identities" />}
|
||||||
const handleChangeRole = async ({
|
{!isLoading &&
|
||||||
identityId,
|
data &&
|
||||||
role
|
data.length > 0 &&
|
||||||
}: {
|
data.map(({ identity: { id, name, authMethod }, role, customRole }) => {
|
||||||
identityId: string;
|
return (
|
||||||
role: string;
|
<Tr className="h-10" key={`identity-${id}`}>
|
||||||
}) => {
|
<Td>{name}</Td>
|
||||||
try {
|
<Td>{id}</Td>
|
||||||
|
<Td>
|
||||||
await updateMutateAsync({
|
<OrgPermissionCan
|
||||||
identityId,
|
I={OrgPermissionActions.Edit}
|
||||||
role,
|
a={OrgPermissionSubjects.Identity}
|
||||||
organizationId: orgId
|
>
|
||||||
});
|
{(isAllowed) => {
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text: "Successfully updated identity role",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message ?? "Failed to update identity role"
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContainer>
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Name</Th>
|
|
||||||
<Th>ID</Th>
|
|
||||||
<Th>Role</Th>
|
|
||||||
<Th>Auth Method</Th>
|
|
||||||
<Th className="w-5" />
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{isLoading && <TableSkeleton columns={4} innerKey="org-identities" />}
|
|
||||||
{!isLoading &&
|
|
||||||
data &&
|
|
||||||
data.length > 0 &&
|
|
||||||
data.map(({
|
|
||||||
identity: {
|
|
||||||
_id,
|
|
||||||
name,
|
|
||||||
authMethod
|
|
||||||
},
|
|
||||||
role,
|
|
||||||
customRole
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`identity-${_id}`}>
|
<Select
|
||||||
<Td>{name}</Td>
|
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||||
<Td>{_id}</Td>
|
isDisabled={!isAllowed}
|
||||||
<Td>
|
className="w-40 bg-mineshaft-600"
|
||||||
<OrgPermissionCan
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
I={OrgPermissionActions.Edit}
|
onValueChange={(selectedRole) =>
|
||||||
a={OrgPermissionSubjects.Identity}
|
handleChangeRole({
|
||||||
>
|
identityId: id,
|
||||||
{(isAllowed) => {
|
role: selectedRole
|
||||||
return (
|
})
|
||||||
<Select
|
}
|
||||||
value={
|
>
|
||||||
role === "custom" ? (customRole?.slug as string) : role
|
{(roles || []).map(({ slug, name: roleName }) => (
|
||||||
}
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
isDisabled={!isAllowed}
|
{roleName}
|
||||||
className="w-40 bg-mineshaft-600"
|
</SelectItem>
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
))}
|
||||||
onValueChange={(selectedRole) =>
|
</Select>
|
||||||
handleChangeRole({
|
|
||||||
identityId: _id,
|
|
||||||
role: selectedRole
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(roles || [])
|
|
||||||
.map(({ slug, name: roleName }) => (
|
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
|
||||||
{roleName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</Td>
|
|
||||||
<Td>{authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}</Td>
|
|
||||||
<Td>
|
|
||||||
<div className="flex justify-end items-center">
|
|
||||||
{authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
|
|
||||||
<Tooltip content="Manage client ID/secrets">
|
|
||||||
<IconButton
|
|
||||||
onClick={async () => {
|
|
||||||
handlePopUpOpen("universalAuthClientSecret", {
|
|
||||||
identityId: _id,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
// isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faKey} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Tooltip content="Manage auth method">
|
|
||||||
<IconButton
|
|
||||||
onClick={async () => {
|
|
||||||
handlePopUpOpen("identityAuthMethod", {
|
|
||||||
identityId: _id,
|
|
||||||
name,
|
|
||||||
authMethod
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="ml-4"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faLock} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Edit}
|
|
||||||
a={OrgPermissionSubjects.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
onClick={async () => {
|
|
||||||
handlePopUpOpen("identity", {
|
|
||||||
identityId: _id,
|
|
||||||
name,
|
|
||||||
role,
|
|
||||||
customRole
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="ml-4"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
<OrgPermissionCan
|
|
||||||
I={OrgPermissionActions.Delete}
|
|
||||||
a={OrgPermissionSubjects.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("deleteIdentity", {
|
|
||||||
identityId: _id,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="ml-4"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
);
|
||||||
})}
|
}}
|
||||||
{!isLoading && data && data?.length === 0 && (
|
</OrgPermissionCan>
|
||||||
<Tr>
|
</Td>
|
||||||
<Td colSpan={4}>
|
<Td>{authMethod ? identityAuthToNameMap[authMethod] : "Not configured"}</Td>
|
||||||
<EmptyState title="No identities have been created in this organization" icon={faServer} />
|
<Td>
|
||||||
</Td>
|
<div className="flex items-center justify-end">
|
||||||
</Tr>
|
{authMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
|
||||||
)}
|
<Tooltip content="Manage client ID/secrets">
|
||||||
</TBody>
|
<IconButton
|
||||||
</Table>
|
onClick={async () => {
|
||||||
</TableContainer>
|
handlePopUpOpen("universalAuthClientSecret", {
|
||||||
);
|
identityId: id,
|
||||||
}
|
name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
// isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faKey} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Identity}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Tooltip content="Manage auth method">
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
handlePopUpOpen("identityAuthMethod", {
|
||||||
|
identityId: id,
|
||||||
|
name,
|
||||||
|
authMethod
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLock} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Identity}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
handlePopUpOpen("identity", {
|
||||||
|
identityId: id,
|
||||||
|
name,
|
||||||
|
role,
|
||||||
|
customRole
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPencil} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Identity}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("deleteIdentity", {
|
||||||
|
identityId: id,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4}>
|
||||||
|
<EmptyState
|
||||||
|
title="No identities have been created in this organization"
|
||||||
|
icon={faServer}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+351
-360
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { faCheck, faCopy, faKey,faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
@@ -9,403 +9,394 @@ import * as yup from "yup";
|
|||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateIdentityUniversalAuthClientSecret,
|
useCreateIdentityUniversalAuthClientSecret,
|
||||||
useGetIdentityUniversalAuth,
|
useGetIdentityUniversalAuth,
|
||||||
useGetIdentityUniversalAuthClientSecrets, useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api";
|
useGetIdentityUniversalAuthClientSecrets,
|
||||||
|
useRevokeIdentityUniversalAuthClientSecret
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
description: yup.string(),
|
description: yup.string(),
|
||||||
ttl: yup.string(),
|
ttl: yup.string(),
|
||||||
numUsesLimit: yup.string()
|
numUsesLimit: yup.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>;
|
popUp: UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>;
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteUniversalAuthClientSecret"]>,
|
popUpName: keyof UsePopUpState<["deleteUniversalAuthClientSecret"]>,
|
||||||
data?: {
|
data?: {
|
||||||
clientSecretPrefix: string;
|
clientSecretPrefix: string;
|
||||||
clientSecretId: string;
|
clientSecretId: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]>, state?: boolean) => void;
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<
|
||||||
|
["universalAuthClientSecret", "deleteUniversalAuthClientSecret"]
|
||||||
|
>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityUniversalAuthClientSecretModal = ({
|
export const IdentityUniversalAuthClientSecretModal = ({
|
||||||
popUp,
|
popUp,
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle
|
handlePopUpToggle
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const [token, setToken] = useState("");
|
const [token, setToken] = useState("");
|
||||||
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
|
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
|
||||||
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
|
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
|
||||||
|
|
||||||
const popUpData = (popUp?.universalAuthClientSecret?.data as {
|
const popUpData = popUp?.universalAuthClientSecret?.data as {
|
||||||
identityId?: string;
|
identityId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
});
|
};
|
||||||
|
|
||||||
const { data, isLoading } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
|
const { data, isLoading } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
|
||||||
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
|
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
|
||||||
|
|
||||||
const { mutateAsync: createClientSecretMutateAsync } = useCreateIdentityUniversalAuthClientSecret();
|
const { mutateAsync: createClientSecretMutateAsync } =
|
||||||
const { mutateAsync: revokeClientSecretMutateAsync } = useRevokeIdentityUniversalAuthClientSecret();
|
useCreateIdentityUniversalAuthClientSecret();
|
||||||
|
const { mutateAsync: revokeClientSecretMutateAsync } =
|
||||||
|
useRevokeIdentityUniversalAuthClientSecret();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
description: "",
|
description: "",
|
||||||
ttl: "",
|
ttl: "",
|
||||||
numUsesLimit: ""
|
numUsesLimit: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
if (isClientSecretCopied) {
|
if (isClientSecretCopied) {
|
||||||
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
|
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isClientSecretCopied]);
|
}, [isClientSecretCopied]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
if (isClientIdCopied) {
|
if (isClientIdCopied) {
|
||||||
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
|
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [isClientIdCopied]);
|
}, [isClientIdCopied]);
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!popUpData?.identityId) return;
|
||||||
|
|
||||||
|
const { clientSecret } = await createClientSecretMutateAsync({
|
||||||
|
identityId: popUpData.identityId,
|
||||||
description,
|
description,
|
||||||
ttl,
|
ttl: Number(ttl),
|
||||||
numUsesLimit
|
numUsesLimit: Number(numUsesLimit)
|
||||||
}: FormData) => {
|
});
|
||||||
try {
|
|
||||||
|
|
||||||
if (!popUpData?.identityId) return;
|
setToken(clientSecret);
|
||||||
|
|
||||||
const { clientSecret } = await createClientSecretMutateAsync({
|
createNotification({
|
||||||
identityId: popUpData.identityId,
|
text: "Successfully created client secret",
|
||||||
description,
|
type: "success"
|
||||||
ttl: Number(ttl),
|
});
|
||||||
numUsesLimit: Number(numUsesLimit)
|
} catch (err) {
|
||||||
});
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
setToken(clientSecret);
|
text: "Failed to create client secret",
|
||||||
|
type: "error"
|
||||||
createNotification({
|
});
|
||||||
text: "Successfully created client secret",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to create client secret",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onDeleteClientSecretSubmit = async ({
|
const onDeleteClientSecretSubmit = async ({
|
||||||
clientSecretId,
|
clientSecretId,
|
||||||
clientSecretPrefix
|
clientSecretPrefix
|
||||||
}: {
|
}: {
|
||||||
clientSecretId: string;
|
clientSecretId: string;
|
||||||
clientSecretPrefix: string;
|
clientSecretPrefix: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
if (!popUpData?.identityId) return;
|
||||||
|
|
||||||
if (!popUpData?.identityId) return;
|
await revokeClientSecretMutateAsync({
|
||||||
|
identityId: popUpData.identityId,
|
||||||
|
clientSecretId
|
||||||
|
});
|
||||||
|
|
||||||
await revokeClientSecretMutateAsync({
|
if (token.startsWith(clientSecretPrefix)) {
|
||||||
identityId: popUpData.identityId,
|
reset();
|
||||||
clientSecretId
|
setToken("");
|
||||||
});
|
}
|
||||||
|
|
||||||
if (token.startsWith(clientSecretPrefix)) {
|
handlePopUpToggle("deleteUniversalAuthClientSecret", false);
|
||||||
reset();
|
|
||||||
setToken("");
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpToggle("deleteUniversalAuthClientSecret", false);
|
createNotification({
|
||||||
|
text: "Successfully deleted client secret",
|
||||||
createNotification({
|
type: "success"
|
||||||
text: "Successfully deleted client secret",
|
});
|
||||||
type: "success"
|
} catch (err) {
|
||||||
});
|
console.error(err);
|
||||||
} catch (err) {
|
createNotification({
|
||||||
console.error(err);
|
text: "Failed to delete client secret",
|
||||||
createNotification({
|
type: "error"
|
||||||
text: "Failed to delete client secret",
|
});
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const hasToken = Boolean(token);
|
const hasToken = Boolean(token);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.universalAuthClientSecret?.isOpen}
|
isOpen={popUp?.universalAuthClientSecret?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("universalAuthClientSecret", isOpen);
|
handlePopUpToggle("universalAuthClientSecret", isOpen);
|
||||||
reset();
|
reset();
|
||||||
setToken("");
|
setToken("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={`Manage Client ID/Secrets for ${popUpData?.name ?? ""}`}>
|
||||||
|
<h2 className="mb-4">Client ID</h2>
|
||||||
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{identityUniversalAuth?.clientId ?? ""}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "");
|
||||||
|
setIsClientIdCopied.on();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title={`Manage Client ID/Secrets for ${popUpData?.name ?? ""}`}>
|
<FontAwesomeIcon icon={isClientIdCopied ? faCheck : faCopy} />
|
||||||
<h2 className="mb-4">Client ID</h2>
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
{t("common.click-to-copy")}
|
||||||
<p className="mr-4 break-all">{identityUniversalAuth?.clientId ?? ""}</p>
|
</span>
|
||||||
<IconButton
|
</IconButton>
|
||||||
ariaLabel="copy icon"
|
</div>
|
||||||
colorSchema="secondary"
|
<h2 className="mb-4">New Client Secret</h2>
|
||||||
className="group relative"
|
{hasToken ? (
|
||||||
onClick={() => {
|
<div>
|
||||||
navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "")
|
<div className="mb-4 flex items-center justify-between">
|
||||||
setIsClientIdCopied.on();
|
<p>We will only show this secret once</p>
|
||||||
}}
|
<Button
|
||||||
>
|
colorSchema="secondary"
|
||||||
<FontAwesomeIcon icon={isClientIdCopied ? faCheck : faCopy} />
|
type="submit"
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
onClick={() => {
|
||||||
{t("common.click-to-copy")}
|
reset();
|
||||||
</span>
|
setToken("");
|
||||||
</IconButton>
|
}}
|
||||||
</div>
|
>
|
||||||
<h2 className="mb-4">New Client Secret</h2>
|
Got it
|
||||||
{hasToken ? (
|
</Button>
|
||||||
<div>
|
</div>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
<p>We will only show this secret once</p>
|
<p className="mr-4 break-all">{token}</p>
|
||||||
<Button
|
<IconButton
|
||||||
colorSchema="secondary"
|
ariaLabel="copy icon"
|
||||||
type="submit"
|
colorSchema="secondary"
|
||||||
onClick={() => {
|
className="group relative"
|
||||||
reset();
|
onClick={() => {
|
||||||
setToken("");
|
navigator.clipboard.writeText(token);
|
||||||
}}
|
setIsClientSecretCopied.on();
|
||||||
>
|
}}
|
||||||
Got it
|
>
|
||||||
</Button>
|
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
|
||||||
</div>
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
{t("common.click-to-copy")}
|
||||||
<p className="mr-4 break-all">{token}</p>
|
</span>
|
||||||
<IconButton
|
</IconButton>
|
||||||
ariaLabel="copy icon"
|
</div>
|
||||||
colorSchema="secondary"
|
</div>
|
||||||
className="group relative"
|
) : (
|
||||||
onClick={() => {
|
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
|
||||||
navigator.clipboard.writeText(token);
|
<Controller
|
||||||
setIsClientSecretCopied.on();
|
control={control}
|
||||||
}}
|
defaultValue=""
|
||||||
>
|
name="description"
|
||||||
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
|
render={({ field, fieldState: { error } }) => (
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
<FormControl
|
||||||
{t("common.click-to-copy")}
|
label="Description (optional)"
|
||||||
</span>
|
isError={Boolean(error)}
|
||||||
</IconButton>
|
errorText={error?.message}
|
||||||
</div>
|
>
|
||||||
|
<Input {...field} placeholder="Description" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="ttl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="TTL (seconds - optional)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</FormControl>
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
|
||||||
className="mb-8"
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="description"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Description (optional)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="Description"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue=""
|
|
||||||
name="ttl"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="TTL (seconds - optional)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<div className="flex">
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="numUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="ml-4"
|
|
||||||
>
|
|
||||||
<div className="flex">
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
className="ml-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
)}
|
||||||
<h2 className="mb-4">Client Secrets</h2>
|
/>
|
||||||
<TableContainer>
|
<Controller
|
||||||
<Table>
|
control={control}
|
||||||
<THead>
|
defaultValue="0"
|
||||||
<Tr>
|
name="numUsesLimit"
|
||||||
<Th>Description</Th>
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Th>Num Uses</Th>
|
<FormControl
|
||||||
<Th>Expires At</Th>
|
label="Max Number of Uses"
|
||||||
<Th>Client Secret</Th>
|
isError={Boolean(error)}
|
||||||
<Th className="w-5" />
|
errorText={error?.message}
|
||||||
</Tr>
|
className="ml-4"
|
||||||
</THead>
|
>
|
||||||
<TBody>
|
<div className="flex">
|
||||||
{isLoading && <TableSkeleton columns={5} innerKey="org-identities-client-secrets" />}
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
{!isLoading &&
|
<Button
|
||||||
data &&
|
className="ml-4"
|
||||||
data.length > 0 &&
|
size="sm"
|
||||||
data.map(({
|
type="submit"
|
||||||
_id,
|
isLoading={isSubmitting}
|
||||||
description,
|
isDisabled={isSubmitting}
|
||||||
clientSecretTTL,
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<h2 className="mb-4">Client Secrets</h2>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Description</Th>
|
||||||
|
<Th>Num Uses</Th>
|
||||||
|
<Th>Expires At</Th>
|
||||||
|
<Th>Client Secret</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={5} innerKey="org-identities-client-secrets" />}
|
||||||
|
{!isLoading &&
|
||||||
|
data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
description,
|
||||||
|
clientSecretTTL,
|
||||||
|
clientSecretPrefix,
|
||||||
|
clientSecretNumUses,
|
||||||
|
clientSecretNumUsesLimit,
|
||||||
|
createdAt
|
||||||
|
}) => {
|
||||||
|
let expiresAt;
|
||||||
|
if (clientSecretTTL > 0) {
|
||||||
|
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
|
||||||
|
<Td>{description === "" ? "-" : description}</Td>
|
||||||
|
<Td>{`${clientSecretNumUses}${
|
||||||
|
clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""
|
||||||
|
}`}</Td>
|
||||||
|
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}</Td>
|
||||||
|
<Td>{`${clientSecretPrefix}****`}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("deleteUniversalAuthClientSecret", {
|
||||||
clientSecretPrefix,
|
clientSecretPrefix,
|
||||||
clientSecretNumUses,
|
clientSecretId: id
|
||||||
clientSecretNumUsesLimit,
|
});
|
||||||
createdAt
|
}}
|
||||||
}) => {
|
size="lg"
|
||||||
let expiresAt;
|
colorSchema="primary"
|
||||||
if (clientSecretTTL > 0) {
|
variant="plain"
|
||||||
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
|
ariaLabel="update"
|
||||||
}
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
title="No client secrets have been created for this identity yet"
|
||||||
|
icon={faKey}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteUniversalAuthClientSecret.isOpen}
|
||||||
|
title={`Are you sure want to delete the client secret ${
|
||||||
|
(popUp?.deleteUniversalAuthClientSecret?.data as { clientSecretPrefix: string })
|
||||||
|
?.clientSecretPrefix || ""
|
||||||
|
}************?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteUniversalAuthClientSecret", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() => {
|
||||||
|
const deleteClientSecretData = popUp?.deleteUniversalAuthClientSecret?.data as {
|
||||||
|
clientSecretId: string;
|
||||||
|
clientSecretPrefix: string;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return onDeleteClientSecretSubmit({
|
||||||
<Tr className="h-10 items-center" key={`mi-client-secret-${_id}`}>
|
clientSecretId: deleteClientSecretData.clientSecretId,
|
||||||
<Td>{description === "" ? "-" : description}</Td>
|
clientSecretPrefix: deleteClientSecretData.clientSecretPrefix
|
||||||
<Td>{`${clientSecretNumUses}${clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""}`}</Td>
|
});
|
||||||
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}</Td>
|
}}
|
||||||
<Td>{`${clientSecretPrefix}****`}</Td>
|
/>
|
||||||
<Td>
|
</ModalContent>
|
||||||
<IconButton
|
</Modal>
|
||||||
onClick={() => {
|
);
|
||||||
handlePopUpOpen("deleteUniversalAuthClientSecret", {
|
};
|
||||||
clientSecretPrefix,
|
|
||||||
clientSecretId: _id
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{!isLoading && data && data?.length === 0 && (
|
|
||||||
<Tr>
|
|
||||||
<Td colSpan={5}>
|
|
||||||
<EmptyState title="No client secrets have been created for this identity yet" icon={faKey} />
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
<DeleteActionModal
|
|
||||||
isOpen={popUp.deleteUniversalAuthClientSecret.isOpen}
|
|
||||||
title={`Are you sure want to delete the client secret ${
|
|
||||||
(popUp?.deleteUniversalAuthClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix || ""
|
|
||||||
}************?`}
|
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteUniversalAuthClientSecret", isOpen)}
|
|
||||||
deleteKey="confirm"
|
|
||||||
onDeleteApproved={() => {
|
|
||||||
const deleteClientSecretData = (popUp?.deleteUniversalAuthClientSecret?.data as {
|
|
||||||
clientSecretId: string;
|
|
||||||
clientSecretPrefix: string;
|
|
||||||
});
|
|
||||||
|
|
||||||
return onDeleteClientSecretSubmit({
|
|
||||||
clientSecretId: deleteClientSecretData.clientSecretId,
|
|
||||||
clientSecretPrefix: deleteClientSecretData.clientSecretPrefix
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+351
-391
@@ -6,427 +6,387 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
|
||||||
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
Button,
|
useAddIdentityUniversalAuth,
|
||||||
FormControl,
|
useGetIdentityUniversalAuth,
|
||||||
IconButton,
|
useUpdateIdentityUniversalAuth
|
||||||
Input,
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
useOrganization,
|
|
||||||
useSubscription
|
|
||||||
} from "@app/context";
|
|
||||||
import {
|
|
||||||
useAddIdentityUniversalAuth,
|
|
||||||
useGetIdentityUniversalAuth,
|
|
||||||
useUpdateIdentityUniversalAuth
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup
|
||||||
accessTokenTTL: yup
|
.object({
|
||||||
.string()
|
accessTokenTTL: yup.string().required("Access Token TTL is required"),
|
||||||
.required("Access Token TTL is required"),
|
accessTokenMaxTTL: yup.string().required("Access Max Token TTL is required"),
|
||||||
accessTokenMaxTTL: yup
|
accessTokenNumUsesLimit: yup.string().required("Access Token Max Number of Uses is required"),
|
||||||
.string()
|
|
||||||
.required("Access Max Token TTL is required"),
|
|
||||||
accessTokenNumUsesLimit: yup
|
|
||||||
.string()
|
|
||||||
.required("Access Token Max Number of Uses is required"),
|
|
||||||
clientSecretTrustedIps: yup
|
clientSecretTrustedIps: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1)
|
.min(1)
|
||||||
.required()
|
.required()
|
||||||
.label("Client Secret Trusted IP"),
|
.label("Client Secret Trusted IP"),
|
||||||
accessTokenTrustedIps: yup
|
accessTokenTrustedIps: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1)
|
.min(1)
|
||||||
.required()
|
.required()
|
||||||
.label("Access Token Trusted IP")
|
.label("Access Token Trusted IP")
|
||||||
}).required();
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identityAuthMethod"]>, state?: boolean) => void;
|
handlePopUpToggle: (
|
||||||
identityAuthMethodData: {
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
identityId: string;
|
state?: boolean
|
||||||
name: string;
|
) => void;
|
||||||
authMethod?: IdentityAuthMethod;
|
identityAuthMethodData: {
|
||||||
}
|
identityId: string;
|
||||||
}
|
name: string;
|
||||||
|
authMethod?: IdentityAuthMethod;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const IdentityUniversalAuthForm = ({
|
export const IdentityUniversalAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityAuthMethodData
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?._id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
||||||
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "");
|
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
accessTokenTTL: "2592000",
|
accessTokenTTL: "2592000",
|
||||||
accessTokenMaxTTL: "2592000",
|
accessTokenMaxTTL: "2592000",
|
||||||
accessTokenNumUsesLimit: "0",
|
accessTokenNumUsesLimit: "0",
|
||||||
clientSecretTrustedIps: [
|
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
{ ipAddress: "0.0.0.0/0" },
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||||
{ ipAddress: "::/0" }
|
}
|
||||||
],
|
});
|
||||||
accessTokenTrustedIps: [
|
|
||||||
{ ipAddress: "0.0.0.0/0" },
|
|
||||||
{ ipAddress: "::/0" }
|
|
||||||
],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fields: clientSecretTrustedIpsFields,
|
fields: clientSecretTrustedIpsFields,
|
||||||
append: appendClientSecretTrustedIp,
|
append: appendClientSecretTrustedIp,
|
||||||
remove: removeClientSecretTrustedIp
|
remove: removeClientSecretTrustedIp
|
||||||
} = useFieldArray({ control, name: "clientSecretTrustedIps" });
|
} = useFieldArray({ control, name: "clientSecretTrustedIps" });
|
||||||
const {
|
const {
|
||||||
fields: accessTokenTrustedIpsFields,
|
fields: accessTokenTrustedIpsFields,
|
||||||
append: appendAccessTokenTrustedIp,
|
append: appendAccessTokenTrustedIp,
|
||||||
remove: removeAccessTokenTrustedIp
|
remove: removeAccessTokenTrustedIp
|
||||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
reset({
|
reset({
|
||||||
accessTokenTTL: String(data.accessTokenTTL),
|
accessTokenTTL: String(data.accessTokenTTL),
|
||||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||||
clientSecretTrustedIps: data.clientSecretTrustedIps.map(({
|
clientSecretTrustedIps: data.clientSecretTrustedIps.map(
|
||||||
ipAddress,
|
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||||
prefix
|
return {
|
||||||
}: IdentityTrustedIp) => {
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
return ({
|
};
|
||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
}
|
||||||
});
|
),
|
||||||
}),
|
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
||||||
accessTokenTrustedIps: data.accessTokenTrustedIps.map(({
|
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||||
ipAddress,
|
return {
|
||||||
prefix
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
}: IdentityTrustedIp) => {
|
};
|
||||||
return ({
|
}
|
||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
)
|
||||||
});
|
});
|
||||||
})
|
} else {
|
||||||
});
|
reset({
|
||||||
} else {
|
accessTokenTTL: "2592000",
|
||||||
reset({
|
accessTokenMaxTTL: "2592000",
|
||||||
accessTokenTTL: "2592000",
|
accessTokenNumUsesLimit: "0",
|
||||||
accessTokenMaxTTL: "2592000",
|
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
accessTokenNumUsesLimit: "0",
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||||
clientSecretTrustedIps: [
|
});
|
||||||
{ ipAddress: "0.0.0.0/0" },
|
}
|
||||||
{ ipAddress: "::/0" }
|
}, [data]);
|
||||||
],
|
|
||||||
accessTokenTrustedIps: [
|
|
||||||
{ ipAddress: "0.0.0.0/0" },
|
|
||||||
{ ipAddress: "::/0" }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
|
if (!identityAuthMethodData) return;
|
||||||
|
|
||||||
if (!identityAuthMethodData) return;
|
if (data) {
|
||||||
|
// update universal auth configuration
|
||||||
|
await updateMutateAsync({
|
||||||
|
organizationId: orgId,
|
||||||
|
identityId: identityAuthMethodData.identityId,
|
||||||
|
clientSecretTrustedIps,
|
||||||
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
|
accessTokenTrustedIps
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// create new universal auth configuration
|
||||||
|
|
||||||
if (data) {
|
await addMutateAsync({
|
||||||
// update universal auth configuration
|
organizationId: orgId,
|
||||||
await updateMutateAsync({
|
identityId: identityAuthMethodData.identityId,
|
||||||
organizationId: orgId,
|
clientSecretTrustedIps,
|
||||||
identityId: identityAuthMethodData.identityId,
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
clientSecretTrustedIps,
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenTrustedIps
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
});
|
||||||
accessTokenTrustedIps,
|
}
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
handlePopUpToggle("identityAuthMethod", false);
|
||||||
// create new universal auth configuration
|
|
||||||
|
|
||||||
await addMutateAsync({
|
createNotification({
|
||||||
organizationId: orgId,
|
text: `Successfully ${
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityAuthMethodData?.authMethod ? "updated" : "configured"
|
||||||
clientSecretTrustedIps,
|
} auth method`,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
type: "success"
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
});
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
|
||||||
accessTokenTrustedIps,
|
reset();
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text =
|
||||||
|
error?.response?.data?.message ??
|
||||||
|
`Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`;
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="2592000"
|
||||||
|
name="accessTokenMaxTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="0"
|
||||||
|
name="accessTokenNumUsesLimit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token Max Number of Uses"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
removeClientSecretTrustedIp(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
appendClientSecretTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpToggle("identityAuthMethod", false);
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Add IP Address
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
handlePopUpOpen("upgradePlan");
|
||||||
text: `Successfully ${identityAuthMethodData?.authMethod ? "updated" : "configured"} auth method`,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
reset();
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message
|
|
||||||
?? `Failed to ${identityAuthMethodData?.authMethod ? "update" : "configure"} identity`;
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="2592000"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="2592000"
|
|
||||||
name="accessTokenMaxTTL"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max TTL (seconds)"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="2592000"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
defaultValue="0"
|
|
||||||
name="accessTokenNumUsesLimit"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="Access Token Max Number of Uses"
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
{...field}
|
|
||||||
placeholder="0"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
|
||||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`clientSecretTrustedIps.${index}.ipAddress`}
|
|
||||||
defaultValue="0.0.0.0/0"
|
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Client Secret Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
placeholder="123.456.789.0"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
removeClientSecretTrustedIp(index);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-4 ml-1">
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
appendClientSecretTrustedIp({
|
|
||||||
ipAddress: "0.0.0.0/0"
|
|
||||||
})
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
}}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
placeholder="123.456.789.0"
|
||||||
size="xs"
|
/>
|
||||||
>
|
</FormControl>
|
||||||
Add IP Address
|
);
|
||||||
</Button>
|
}}
|
||||||
</div>
|
/>
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
<IconButton
|
||||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
onClick={() => {
|
||||||
<Controller
|
if (subscription?.ipAllowlisting) {
|
||||||
control={control}
|
removeAccessTokenTrustedIp(index);
|
||||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
return;
|
||||||
defaultValue="0.0.0.0/0"
|
}
|
||||||
render={({ field, fieldState: { error } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={field.value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (subscription?.ipAllowlisting) {
|
|
||||||
field.onChange(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
placeholder="123.456.789.0"
|
size="lg"
|
||||||
/>
|
colorSchema="danger"
|
||||||
</FormControl>
|
variant="plain"
|
||||||
);
|
ariaLabel="update"
|
||||||
}}
|
className="p-3"
|
||||||
/>
|
>
|
||||||
<IconButton
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
onClick={() => {
|
</IconButton>
|
||||||
if (subscription?.ipAllowlisting) {
|
</div>
|
||||||
removeAccessTokenTrustedIp(index);
|
))}
|
||||||
return;
|
<div className="my-4 ml-1">
|
||||||
}
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
appendAccessTokenTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
size="lg"
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
colorSchema="danger"
|
size="xs"
|
||||||
variant="plain"
|
>
|
||||||
ariaLabel="update"
|
Add IP Address
|
||||||
className="p-3"
|
</Button>
|
||||||
>
|
</div>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<div className="flex items-center">
|
||||||
</IconButton>
|
<Button
|
||||||
</div>
|
className="mr-4"
|
||||||
))}
|
size="sm"
|
||||||
<div className="my-4 ml-1">
|
type="submit"
|
||||||
<Button
|
isLoading={isSubmitting}
|
||||||
variant="outline_bg"
|
isDisabled={isSubmitting}
|
||||||
onClick={() => {
|
>
|
||||||
if (subscription?.ipAllowlisting) {
|
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
||||||
appendAccessTokenTrustedIp({
|
</Button>
|
||||||
ipAddress: "0.0.0.0/0"
|
<Button
|
||||||
})
|
colorSchema="secondary"
|
||||||
return;
|
variant="plain"
|
||||||
}
|
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||||
|
>
|
||||||
handlePopUpOpen("upgradePlan");
|
Cancel
|
||||||
}}
|
</Button>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
</div>
|
||||||
size="xs"
|
</form>
|
||||||
>
|
);
|
||||||
Add IP Address
|
};
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{identityAuthMethodData?.authMethod ? "Update" : "Configure"}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+134
-147
@@ -1,176 +1,163 @@
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||||
faCheck,
|
|
||||||
faCopy,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import {
|
import { Button, FormControl, IconButton, Input, Modal, ModalContent } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import {
|
import { useAddUserToOrg, useFetchServerStatus } from "@app/hooks/api";
|
||||||
useAddUserToOrg,
|
|
||||||
useFetchServerStatus
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const addMemberFormSchema = yup.object({
|
const addMemberFormSchema = yup.object({
|
||||||
email: yup.string().email().required().label("Email").trim().lowercase()
|
email: yup.string().email().required().label("Email").trim().lowercase()
|
||||||
});
|
});
|
||||||
|
|
||||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["addMember"]>;
|
popUp: UsePopUpState<["addMember"]>;
|
||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
|
||||||
completeInviteLink: string;
|
completeInviteLink: string;
|
||||||
setCompleteInviteLink: (link: string) => void;
|
setCompleteInviteLink: (link: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddOrgMemberModal = ({
|
export const AddOrgMemberModal = ({
|
||||||
popUp,
|
popUp,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
completeInviteLink,
|
completeInviteLink,
|
||||||
setCompleteInviteLink
|
setCompleteInviteLink
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
|
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
|
||||||
|
|
||||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||||
|
|
||||||
const onAddMember = async ({ email }: TAddMemberForm) => {
|
const onAddMember = async ({ email }: TAddMemberForm) => {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await addUserMutateAsync({
|
const { data } = await addUserMutateAsync({
|
||||||
organizationId: currentOrg?._id,
|
organizationId: currentOrg?.id,
|
||||||
inviteeEmail: email
|
inviteeEmail: email
|
||||||
});
|
});
|
||||||
|
|
||||||
setCompleteInviteLink(data?.completeInviteLink ??"");
|
setCompleteInviteLink(data?.completeInviteLink ?? "");
|
||||||
|
|
||||||
// only show this notification when email is configured.
|
// only show this notification when email is configured.
|
||||||
// A [completeInviteLink] will not be sent if smtp is configured
|
// A [completeInviteLink] will not be sent if smtp is configured
|
||||||
|
|
||||||
if (!data.completeInviteLink) {
|
if (!data.completeInviteLink) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully invited user to the organization.",
|
text: "Successfully invited user to the organization.",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to invite user to org",
|
text: "Failed to invite user to org",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverDetails?.emailConfigured) {
|
||||||
|
handlePopUpToggle("addMember", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(completeInviteLink as string);
|
||||||
|
setInviteLinkCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.addMember?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("addMember", isOpen);
|
||||||
|
setCompleteInviteLink("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title={`Invite others to ${currentOrg?.name}`}
|
||||||
|
subTitle={
|
||||||
|
<div>
|
||||||
|
{!completeInviteLink && (
|
||||||
|
<div>
|
||||||
|
An invite is specific to an email address and expires after 1 day.
|
||||||
|
<br />
|
||||||
|
For security reasons, you will need to separately add members to projects.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{completeInviteLink &&
|
||||||
|
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
>
|
||||||
if (serverDetails?.emailConfigured) {
|
{!completeInviteLink && (
|
||||||
handlePopUpToggle("addMember", false);
|
<form onSubmit={handleSubmit(onAddMember)}>
|
||||||
}
|
<Controller
|
||||||
|
control={control}
|
||||||
reset();
|
defaultValue=""
|
||||||
};
|
name="email"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
const copyTokenToClipboard = () => {
|
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||||
navigator.clipboard.writeText(completeInviteLink as string);
|
<Input {...field} />
|
||||||
setInviteLinkCopied.on();
|
</FormControl>
|
||||||
};
|
)}
|
||||||
|
/>
|
||||||
return (
|
<div className="mt-8 flex items-center">
|
||||||
<Modal
|
<Button
|
||||||
isOpen={popUp?.addMember?.isOpen}
|
className="mr-4"
|
||||||
onOpenChange={(isOpen) => {
|
size="sm"
|
||||||
handlePopUpToggle("addMember", isOpen);
|
type="submit"
|
||||||
setCompleteInviteLink("");
|
isLoading={isSubmitting}
|
||||||
}}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
<ModalContent
|
Add Member
|
||||||
title={`Invite others to ${currentOrg?.name}`}
|
</Button>
|
||||||
subTitle={
|
<Button
|
||||||
<div>
|
colorSchema="secondary"
|
||||||
{!completeInviteLink && (
|
variant="plain"
|
||||||
<div>
|
onClick={() => handlePopUpToggle("addMember", false)}
|
||||||
An invite is specific to an email address and expires after 1 day.
|
>
|
||||||
<br />
|
Cancel
|
||||||
For security reasons, you will need to separately add members to projects.
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</form>
|
||||||
{completeInviteLink &&
|
)}
|
||||||
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
|
{completeInviteLink && (
|
||||||
</div>
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
}
|
<p className="mr-4 break-all">{completeInviteLink}</p>
|
||||||
>
|
<IconButton
|
||||||
{!completeInviteLink && (
|
ariaLabel="copy icon"
|
||||||
<form onSubmit={handleSubmit(onAddMember)}>
|
colorSchema="secondary"
|
||||||
<Controller
|
className="group relative"
|
||||||
control={control}
|
onClick={copyTokenToClipboard}
|
||||||
defaultValue=""
|
>
|
||||||
name="email"
|
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
||||||
render={({ field, fieldState: { error } }) => (
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
click to copy
|
||||||
<Input {...field} />
|
</span>
|
||||||
</FormControl>
|
</IconButton>
|
||||||
)}
|
</div>
|
||||||
/>
|
)}
|
||||||
<div className="mt-8 flex items-center">
|
</ModalContent>
|
||||||
<Button
|
</Modal>
|
||||||
className="mr-4"
|
);
|
||||||
size="sm"
|
};
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
Add Member
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
colorSchema="secondary"
|
|
||||||
variant="plain"
|
|
||||||
onClick={() => handlePopUpToggle("addMember", false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
{completeInviteLink && (
|
|
||||||
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
|
||||||
<p className="mr-4 break-all">{completeInviteLink}</p>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="copy icon"
|
|
||||||
colorSchema="secondary"
|
|
||||||
className="group relative"
|
|
||||||
onClick={copyTokenToClipboard}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
|
|
||||||
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
|
||||||
click to copy
|
|
||||||
</span>
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+105
-113
@@ -14,134 +14,126 @@ import {
|
|||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription,
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import { useDeleteOrgMembership, useGetSSOConfig } from "@app/hooks/api";
|
||||||
useDeleteOrgMembership,
|
|
||||||
useGetSSOConfig,
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||||
import { OrgMembersTable } from "./OrgMembersTable";
|
import { OrgMembersTable } from "./OrgMembersTable";
|
||||||
|
|
||||||
export const OrgMembersSection = () => {
|
export const OrgMembersSection = () => {
|
||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?._id ?? "";
|
const orgId = currentOrg?.id ?? "";
|
||||||
|
|
||||||
const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
|
const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
|
||||||
|
|
||||||
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
|
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addMember",
|
"addMember",
|
||||||
"removeMember",
|
"removeMember",
|
||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
"setUpEmail"
|
"setUpEmail"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership();
|
||||||
|
|
||||||
const isMoreUsersNotAllowed = subscription?.memberLimit
|
const isMoreUsersNotAllowed = subscription?.memberLimit
|
||||||
? subscription.membersUsed >= subscription.memberLimit
|
? subscription.membersUsed >= subscription.memberLimit
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const handleAddMemberModal = () => {
|
const handleAddMemberModal = () => {
|
||||||
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
|
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "You cannot invite users when SAML SSO is configured for your organization",
|
text: "You cannot invite users when SAML SSO is configured for your organization",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (isMoreUsersNotAllowed) {
|
|
||||||
handlePopUpOpen("upgradePlan", {
|
|
||||||
description: "You can add more members if you upgrade your Infisical plan."
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("addMember");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRemoveMemberSubmit = async (orgMembershipId: string) => {
|
if (isMoreUsersNotAllowed) {
|
||||||
try {
|
handlePopUpOpen("upgradePlan", {
|
||||||
await deleteMutateAsync({
|
description: "You can add more members if you upgrade your Infisical plan."
|
||||||
orgId,
|
});
|
||||||
membershipId: orgMembershipId
|
} else {
|
||||||
});
|
handlePopUpOpen("addMember");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
createNotification({
|
const onRemoveMemberSubmit = async (orgMembershipId: string) => {
|
||||||
text: "Successfully removed user from org",
|
try {
|
||||||
type: "success"
|
await deleteMutateAsync({
|
||||||
});
|
orgId,
|
||||||
} catch (err) {
|
membershipId: orgMembershipId
|
||||||
console.error(err);
|
});
|
||||||
createNotification({
|
|
||||||
text: "Failed to remove user from the organization",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpClose("removeMember");
|
createNotification({
|
||||||
|
text: "Successfully removed user from org",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to remove user from the organization",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
handlePopUpClose("removeMember");
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
};
|
||||||
<div className="flex justify-between mb-4">
|
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">
|
return (
|
||||||
Members
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
</p>
|
<div className="mb-4 flex justify-between">
|
||||||
<OrgPermissionCan
|
<p className="text-xl font-semibold text-mineshaft-100">Members</p>
|
||||||
I={OrgPermissionActions.Create}
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||||
a={OrgPermissionSubjects.Member}
|
{(isAllowed) => (
|
||||||
>
|
<Button
|
||||||
{(isAllowed) => (
|
colorSchema="primary"
|
||||||
<Button
|
type="submit"
|
||||||
colorSchema="primary"
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
type="submit"
|
onClick={() => handleAddMemberModal()}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
isDisabled={!isAllowed}
|
||||||
onClick={() => handleAddMemberModal()}
|
>
|
||||||
isDisabled={!isAllowed}
|
Add Member
|
||||||
>
|
</Button>
|
||||||
Add Member
|
)}
|
||||||
</Button>
|
</OrgPermissionCan>
|
||||||
)}
|
</div>
|
||||||
</OrgPermissionCan>
|
<OrgMembersTable
|
||||||
</div>
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
<OrgMembersTable
|
setCompleteInviteLink={setCompleteInviteLink}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
/>
|
||||||
setCompleteInviteLink={setCompleteInviteLink}
|
<AddOrgMemberModal
|
||||||
/>
|
popUp={popUp}
|
||||||
<AddOrgMemberModal
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
popUp={popUp}
|
completeInviteLink={completeInviteLink}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
setCompleteInviteLink={setCompleteInviteLink}
|
||||||
completeInviteLink={completeInviteLink}
|
/>
|
||||||
setCompleteInviteLink={setCompleteInviteLink}
|
<DeleteActionModal
|
||||||
/>
|
isOpen={popUp.removeMember.isOpen}
|
||||||
<DeleteActionModal
|
title={`Are you sure want to remove member with email ${
|
||||||
isOpen={popUp.removeMember.isOpen}
|
(popUp?.removeMember?.data as { email: string })?.email || ""
|
||||||
title={`Are you sure want to remove member with email ${
|
}?`}
|
||||||
(popUp?.removeMember?.data as { email: string })?.email || ""
|
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||||
}?`}
|
deleteKey="confirm"
|
||||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
onDeleteApproved={() =>
|
||||||
deleteKey="confirm"
|
onRemoveMemberSubmit(
|
||||||
onDeleteApproved={() =>
|
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
||||||
onRemoveMemberSubmit(
|
)
|
||||||
(popUp?.removeMember?.data as { orgMembershipId: string })?.orgMembershipId
|
}
|
||||||
)
|
/>
|
||||||
}
|
<UpgradePlanModal
|
||||||
/>
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
<UpgradePlanModal
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
/>
|
||||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
<EmailServiceSetupModal
|
||||||
/>
|
isOpen={popUp.setUpEmail?.isOpen}
|
||||||
<EmailServiceSetupModal
|
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||||
isOpen={popUp.setUpEmail?.isOpen}
|
/>
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
</div>
|
||||||
/>
|
);
|
||||||
</div>
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+230
-237
@@ -1,272 +1,265 @@
|
|||||||
import { useCallback,useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import {
|
import { faMagnifyingGlass, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
faMagnifyingGlass,
|
|
||||||
faUsers,
|
|
||||||
faXmark
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser} from "@app/context";
|
useUser
|
||||||
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddUserToOrg,
|
useAddUserToOrg,
|
||||||
useFetchServerStatus,
|
useFetchServerStatus,
|
||||||
useGetOrgUsers,
|
useGetOrgUsers,
|
||||||
useGetRoles,
|
useGetRoles,
|
||||||
useUpdateOrgUserRole
|
useUpdateOrgUserRole
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
|
popUpName: keyof UsePopUpState<["removeMember", "upgradePlan"]>,
|
||||||
data?: {
|
data?: {
|
||||||
orgMembershipId?: string;
|
orgMembershipId?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
setCompleteInviteLink: (link: string) => void;
|
setCompleteInviteLink: (link: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OrgMembersTable = ({
|
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => {
|
||||||
handlePopUpOpen,
|
const { createNotification } = useNotificationContext();
|
||||||
setCompleteInviteLink
|
const { subscription } = useSubscription();
|
||||||
}: Props) => {
|
const { currentOrg } = useOrganization();
|
||||||
const { createNotification } = useNotificationContext();
|
const { user } = useUser();
|
||||||
const { subscription } = useSubscription();
|
const userId = user?.id || "";
|
||||||
const { currentOrg } = useOrganization();
|
const orgId = currentOrg?.id || "";
|
||||||
const { user } = useUser();
|
|
||||||
const userId = user?._id || "";
|
|
||||||
const orgId = currentOrg?._id || "";
|
|
||||||
|
|
||||||
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
const { data: roles, isLoading: isRolesLoading } = useGetRoles({
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||||
|
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
|
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
|
||||||
|
|
||||||
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
|
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg();
|
||||||
const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole();
|
const { mutateAsync: updateUserOrgRole } = useUpdateOrgUserRole();
|
||||||
|
|
||||||
const onRoleChange = async (membershipId: string, role: string) => {
|
const onRoleChange = async (membershipId: string, role: string) => {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: replace hardcoding default role
|
// TODO: replace hardcoding default role
|
||||||
const isCustomRole = !["admin", "member"].includes(role);
|
const isCustomRole = !["admin", "member"].includes(role);
|
||||||
|
|
||||||
if (isCustomRole && subscription && !subscription?.rbac) {
|
if (isCustomRole && subscription && !subscription?.rbac) {
|
||||||
handlePopUpOpen("upgradePlan", {
|
handlePopUpOpen("upgradePlan", {
|
||||||
description: "You can assign custom roles to members if you upgrade your Infisical plan."
|
description: "You can assign custom roles to members if you upgrade your Infisical plan."
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateUserOrgRole({
|
await updateUserOrgRole({
|
||||||
organizationId: currentOrg?._id,
|
organizationId: currentOrg?.id,
|
||||||
membershipId, role
|
membershipId,
|
||||||
});
|
role
|
||||||
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully updated user role",
|
text: "Successfully updated user role",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to update user role",
|
text: "Failed to update user role",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResendInvite = async (email: string) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const { data } = await addUserMutateAsync({
|
|
||||||
organizationId: orgId,
|
|
||||||
inviteeEmail: email
|
|
||||||
});
|
|
||||||
|
|
||||||
setCompleteInviteLink(data?.completeInviteLink || "");
|
|
||||||
|
|
||||||
if (!data.completeInviteLink) {
|
|
||||||
createNotification({
|
|
||||||
text: `Successfully resent invite to ${email}`,
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
createNotification({
|
|
||||||
text: `Failed to resend invite to ${email}`,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isLoading = isMembersLoading || isRolesLoading;
|
const onResendInvite = async (email: string) => {
|
||||||
|
try {
|
||||||
|
const { data } = await addUserMutateAsync({
|
||||||
|
organizationId: orgId,
|
||||||
|
inviteeEmail: email
|
||||||
|
});
|
||||||
|
|
||||||
const isIamOwner = useMemo(
|
setCompleteInviteLink(data?.completeInviteLink || "");
|
||||||
() => members?.find(({ user: u }) => userId === u?._id)?.role === "owner",
|
|
||||||
[userId, members]
|
|
||||||
);
|
|
||||||
|
|
||||||
const findRoleFromId = useCallback(
|
if (!data.completeInviteLink) {
|
||||||
(roleId: string) => {
|
createNotification({
|
||||||
return (roles || []).find(({ _id: id }) => id === roleId);
|
text: `Successfully resent invite to ${email}`,
|
||||||
},
|
type: "success"
|
||||||
[roles]
|
});
|
||||||
);
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to resend invite to ${email}`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const filterdUser = useMemo(
|
const isLoading = isMembersLoading || isRolesLoading;
|
||||||
() =>
|
|
||||||
members?.filter(
|
|
||||||
({ user: u, inviteEmail }) =>
|
|
||||||
u?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
u?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
u?.email?.toLowerCase().includes(searchMemberFilter) ||
|
|
||||||
inviteEmail?.includes(searchMemberFilter)
|
|
||||||
),
|
|
||||||
[members, searchMemberFilter]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
const isIamOwner = useMemo(
|
||||||
<div>
|
() => members?.find(({ user: u }) => userId === u?.id)?.role === "owner",
|
||||||
<Input
|
[userId, members]
|
||||||
value={searchMemberFilter}
|
);
|
||||||
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
const findRoleFromId = useCallback(
|
||||||
placeholder="Search members..."
|
(roleId: string) => {
|
||||||
/>
|
return (roles || []).find(({ id }) => id === roleId);
|
||||||
<TableContainer className="mt-4">
|
},
|
||||||
<Table>
|
[roles]
|
||||||
<THead>
|
);
|
||||||
<Tr>
|
|
||||||
<Th>Name</Th>
|
const filterdUser = useMemo(
|
||||||
<Th>Email</Th>
|
() =>
|
||||||
<Th>Role</Th>
|
members?.filter(
|
||||||
<Th className="w-5" />
|
({ user: u, inviteEmail }) =>
|
||||||
</Tr>
|
u?.firstName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
</THead>
|
u?.lastName?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
<TBody>
|
u?.email?.toLowerCase().includes(searchMemberFilter) ||
|
||||||
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
inviteEmail?.includes(searchMemberFilter)
|
||||||
{!isLoading &&
|
),
|
||||||
filterdUser?.map(
|
[members, searchMemberFilter]
|
||||||
({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => {
|
);
|
||||||
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
|
||||||
const email = u?.email || inviteEmail;
|
return (
|
||||||
return (
|
<div>
|
||||||
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
<Input
|
||||||
<Td>{name}</Td>
|
value={searchMemberFilter}
|
||||||
<Td>{email}</Td>
|
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||||
<Td>
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search members..."
|
||||||
|
/>
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th>Role</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
||||||
|
{!isLoading &&
|
||||||
|
filterdUser?.map(
|
||||||
|
({ user: u, inviteEmail, role, customRole, id: orgMembershipId, status }) => {
|
||||||
|
const name = u ? `${u.firstName} ${u.lastName}` : "-";
|
||||||
|
const email = u?.email || inviteEmail;
|
||||||
|
return (
|
||||||
|
<Tr key={`org-membership-${orgMembershipId}`} className="w-full">
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td>{email}</Td>
|
||||||
|
<Td>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Member}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<>
|
||||||
|
{status === "accepted" && (
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
||||||
|
}
|
||||||
|
isDisabled={userId === u?.id || !isAllowed}
|
||||||
|
className="w-40 bg-mineshaft-600"
|
||||||
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={(selectedRole) =>
|
||||||
|
onRoleChange(orgMembershipId, selectedRole)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(roles || [])
|
||||||
|
.filter(({ slug }) =>
|
||||||
|
slug === "owner" ? isIamOwner || role === "owner" : true
|
||||||
|
)
|
||||||
|
.map(({ slug, name: roleName }) => (
|
||||||
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
|
{roleName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{(status === "invited" || status === "verified") &&
|
||||||
|
serverDetails?.emailConfigured && (
|
||||||
|
<Button
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="w-40"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => onResendInvite(email)}
|
||||||
|
>
|
||||||
|
Resend Invite
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{userId !== u?.id && (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Delete}
|
||||||
a={OrgPermissionSubjects.Member}
|
a={OrgPermissionSubjects.Member}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<>
|
<IconButton
|
||||||
{status === "accepted" && (
|
onClick={() => {
|
||||||
<Select
|
handlePopUpOpen("removeMember", { orgMembershipId, email });
|
||||||
value={
|
}}
|
||||||
role === "custom" ? findRoleFromId(customRole)?.slug : role
|
size="lg"
|
||||||
}
|
colorSchema="danger"
|
||||||
isDisabled={userId === u?._id || !isAllowed}
|
variant="plain"
|
||||||
className="w-40 bg-mineshaft-600"
|
ariaLabel="update"
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
className="ml-4"
|
||||||
onValueChange={(selectedRole) =>
|
isDisabled={!isAllowed}
|
||||||
onRoleChange(orgMembershipId, selectedRole)
|
>
|
||||||
}
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
>
|
</IconButton>
|
||||||
{(roles || [])
|
|
||||||
.filter(({ slug }) =>
|
|
||||||
slug === "owner" ? isIamOwner || role === "owner" : true
|
|
||||||
)
|
|
||||||
.map(({ slug, name: roleName }) => (
|
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
|
||||||
{roleName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
{(status === "invited" || status === "verified") &&
|
|
||||||
serverDetails?.emailConfigured && (
|
|
||||||
<Button
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
className="w-40"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => onResendInvite(email)}
|
|
||||||
>
|
|
||||||
Resend Invite
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
</Td>
|
)}
|
||||||
<Td>
|
</Td>
|
||||||
{userId !== u?._id && (
|
</Tr>
|
||||||
<OrgPermissionCan
|
);
|
||||||
I={OrgPermissionActions.Delete}
|
}
|
||||||
a={OrgPermissionSubjects.Member}
|
)}
|
||||||
>
|
</TBody>
|
||||||
{(isAllowed) => (
|
</Table>
|
||||||
<IconButton
|
{!isLoading && filterdUser?.length === 0 && (
|
||||||
onClick={() => {
|
<EmptyState title="No project members found" icon={faUsers} />
|
||||||
handlePopUpOpen("removeMember", { orgMembershipId, email })
|
)}
|
||||||
}}
|
</TableContainer>
|
||||||
size="lg"
|
</div>
|
||||||
colorSchema="danger"
|
);
|
||||||
variant="plain"
|
};
|
||||||
ariaLabel="update"
|
|
||||||
className="ml-4"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</OrgPermissionCan>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
{!isLoading && filterdUser?.length === 0 && (
|
|
||||||
<EmptyState title="No project members found" icon={faUsers} />
|
|
||||||
)}
|
|
||||||
</TableContainer>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -158,21 +158,21 @@ export const LogsTableRow = ({ auditLog }: Props) => {
|
|||||||
<p>{`Name: ${event.metadata.name}`}</p>
|
<p>{`Name: ${event.metadata.name}`}</p>
|
||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
case EventType.CREATE_IDENTITY:
|
case EventType.CREATEidENTITY:
|
||||||
return (
|
return (
|
||||||
<Td>
|
<Td>
|
||||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||||
<p>{`Name: ${event.metadata.name}`}</p>
|
<p>{`Name: ${event.metadata.name}`}</p>
|
||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
case EventType.UPDATE_IDENTITY:
|
case EventType.UPDATEidENTITY:
|
||||||
return (
|
return (
|
||||||
<Td>
|
<Td>
|
||||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||||
<p>{`Name: ${event.metadata.name}`}</p>
|
<p>{`Name: ${event.metadata.name}`}</p>
|
||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
case EventType.DELETE_IDENTITY:
|
case EventType.DELETEidENTITY:
|
||||||
return (
|
return (
|
||||||
<Td>
|
<Td>
|
||||||
<p>{`ID: ${event.metadata.identityId}`}</p>
|
<p>{`ID: ${event.metadata.identityId}`}</p>
|
||||||
|
|||||||
+147
-168
@@ -5,30 +5,22 @@ import { yupResolver } from "@hookform/resolvers/yup";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
Button,
|
useAddIdentityToWorkspace,
|
||||||
FormControl,
|
useGetIdentityMembershipOrgs,
|
||||||
Modal,
|
useGetRoles,
|
||||||
ModalContent,
|
useGetWorkspaceIdentityMemberships
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
useOrganization,
|
|
||||||
useWorkspace
|
|
||||||
} from "@app/context";
|
|
||||||
import {
|
|
||||||
useAddIdentityToWorkspace,
|
|
||||||
useGetIdentityMembershipOrgs,
|
|
||||||
useGetRoles,
|
|
||||||
useGetWorkspaceIdentityMemberships
|
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup
|
||||||
|
.object({
|
||||||
identityId: yup.string().required("Identity id is required"),
|
identityId: yup.string().required("Identity id is required"),
|
||||||
role: yup.string()
|
role: yup.string()
|
||||||
}).required();
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
@@ -37,167 +29,154 @@ type Props = {
|
|||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["identity"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityModal = ({
|
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
popUp,
|
const { createNotification } = useNotificationContext();
|
||||||
handlePopUpToggle
|
const { currentOrg } = useOrganization();
|
||||||
}: Props) => {
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const { createNotification } = useNotificationContext();
|
const orgId = currentOrg?.id || "";
|
||||||
const { currentOrg } = useOrganization();
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
|
|
||||||
const orgId = currentOrg?._id || "";
|
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
|
||||||
const workspaceId = currentWorkspace?._id || "";
|
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
|
||||||
|
|
||||||
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
|
const { data: roles } = useGetRoles({
|
||||||
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
|
orgId,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
const { data: roles } = useGetRoles({
|
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||||
orgId,
|
|
||||||
workspaceId
|
const filteredIdentityMembershipOrgs = useMemo(() => {
|
||||||
|
const wsIdentityIds = new Map();
|
||||||
|
|
||||||
|
identityMemberships?.forEach((identityMembership) => {
|
||||||
|
wsIdentityIds.set(identityMembership.identity.id, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
return (identityMembershipOrgs || []).filter(({ identity: i }) => !wsIdentityIds.has(i.id));
|
||||||
|
}, [identityMembershipOrgs, identityMemberships]);
|
||||||
|
|
||||||
const filteredIdentityMembershipOrgs = useMemo(() => {
|
const {
|
||||||
const wsIdentityIds = new Map();
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
identityMemberships?.forEach((identityMembership) => {
|
const onFormSubmit = async ({ identityId, role }: FormData) => {
|
||||||
wsIdentityIds.set(identityMembership.identity._id, true);
|
try {
|
||||||
});
|
await addIdentityToWorkspaceMutateAsync({
|
||||||
|
workspaceId,
|
||||||
return (identityMembershipOrgs || []).filter(
|
|
||||||
({ identity: i }) => !wsIdentityIds.has(i._id)
|
|
||||||
);
|
|
||||||
}, [identityMembershipOrgs, identityMemberships]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
control,
|
|
||||||
handleSubmit,
|
|
||||||
reset,
|
|
||||||
formState: { isSubmitting }
|
|
||||||
} = useForm<FormData>({
|
|
||||||
resolver: yupResolver(schema)
|
|
||||||
});
|
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
|
||||||
identityId,
|
identityId,
|
||||||
role
|
role: role || undefined
|
||||||
}: FormData) => {
|
});
|
||||||
try {
|
|
||||||
|
|
||||||
await addIdentityToWorkspaceMutateAsync({
|
createNotification({
|
||||||
workspaceId,
|
text: "Successfully added identity to project",
|
||||||
identityId,
|
type: "success"
|
||||||
role: role || undefined
|
});
|
||||||
});
|
|
||||||
|
|
||||||
createNotification({
|
reset();
|
||||||
text: "Successfully added identity to project",
|
handlePopUpToggle("identity", false);
|
||||||
type: "success"
|
} catch (err) {
|
||||||
});
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to add identity to project";
|
||||||
|
|
||||||
reset();
|
createNotification({
|
||||||
handlePopUpToggle("identity", false);
|
text,
|
||||||
} catch (err) {
|
type: "error"
|
||||||
console.error(err);
|
});
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message
|
|
||||||
?? "Failed to add identity to project";
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.identity?.isOpen}
|
isOpen={popUp?.identity?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("identity", isOpen);
|
handlePopUpToggle("identity", isOpen);
|
||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title="Add Identity to Project">
|
<ModalContent title="Add Identity to Project">
|
||||||
{filteredIdentityMembershipOrgs.length ? (
|
{filteredIdentityMembershipOrgs.length ? (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="identityId"
|
name="identityId"
|
||||||
defaultValue={filteredIdentityMembershipOrgs?.[0]?._id}
|
defaultValue={filteredIdentityMembershipOrgs?.[0]?.id}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||||
label="Identity"
|
<Select
|
||||||
errorText={error?.message}
|
defaultValue={field.value}
|
||||||
isError={Boolean(error)}
|
{...field}
|
||||||
>
|
onValueChange={(e) => onChange(e)}
|
||||||
<Select
|
className="w-full"
|
||||||
defaultValue={field.value}
|
>
|
||||||
{...field}
|
{filteredIdentityMembershipOrgs.map(({ identity }) => (
|
||||||
onValueChange={(e) => onChange(e)}
|
<SelectItem value={identity.id} key={`org-identity-${identity.id}`}>
|
||||||
className="w-full"
|
{identity.name}
|
||||||
>
|
</SelectItem>
|
||||||
{filteredIdentityMembershipOrgs.map(({ identity }) => (
|
))}
|
||||||
<SelectItem value={identity._id} key={`org-identity-${identity._id}`}>
|
</Select>
|
||||||
{identity.name}
|
</FormControl>
|
||||||
</SelectItem>
|
)}
|
||||||
))}
|
/>
|
||||||
</Select>
|
<Controller
|
||||||
</FormControl>
|
control={control}
|
||||||
)}
|
name="role"
|
||||||
/>
|
defaultValue=""
|
||||||
<Controller
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
control={control}
|
<FormControl
|
||||||
name="role"
|
label="Role"
|
||||||
defaultValue=""
|
errorText={error?.message}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
isError={Boolean(error)}
|
||||||
<FormControl
|
className="mt-4"
|
||||||
label="Role"
|
>
|
||||||
errorText={error?.message}
|
<Select
|
||||||
isError={Boolean(error)}
|
defaultValue={field.value}
|
||||||
className="mt-4"
|
{...field}
|
||||||
>
|
onValueChange={(e) => onChange(e)}
|
||||||
<Select
|
className="w-full"
|
||||||
defaultValue={field.value}
|
>
|
||||||
{...field}
|
{(roles || []).map(({ name, slug }) => (
|
||||||
onValueChange={(e) => onChange(e)}
|
<SelectItem value={slug} key={`st-role-${slug}`}>
|
||||||
className="w-full"
|
{name}
|
||||||
>
|
</SelectItem>
|
||||||
{(roles || []).map(({ name, slug }) => (
|
))}
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
</Select>
|
||||||
{name}
|
</FormControl>
|
||||||
</SelectItem>
|
)}
|
||||||
))}
|
/>
|
||||||
</Select>
|
<div className="flex items-center">
|
||||||
</FormControl>
|
<Button
|
||||||
)}
|
className="mr-4"
|
||||||
/>
|
size="sm"
|
||||||
<div className="flex items-center">
|
type="submit"
|
||||||
<Button
|
isLoading={isSubmitting}
|
||||||
className="mr-4"
|
isDisabled={isSubmitting}
|
||||||
size="sm"
|
>
|
||||||
type="submit"
|
{popUp?.identity?.data ? "Update" : "Create"}
|
||||||
isLoading={isSubmitting}
|
</Button>
|
||||||
isDisabled={isSubmitting}
|
<Button colorSchema="secondary" variant="plain">
|
||||||
>
|
Cancel
|
||||||
{popUp?.identity?.data ? "Update" : "Create"}
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
<Button colorSchema="secondary" variant="plain">
|
</form>
|
||||||
Cancel
|
) : (
|
||||||
</Button>
|
<div className="flex flex-col space-y-4">
|
||||||
</div>
|
<div className="text-sm">
|
||||||
</form>
|
All identities in your organization have already been added to this project.
|
||||||
) : (
|
</div>
|
||||||
<div className="flex flex-col space-y-4">
|
<Link href={`/org/${currentWorkspace?.organization}/members`}>
|
||||||
<div className="text-sm">All identities in your organization have already been added to this project.</div>
|
<Button variant="outline_bg">Create a new identity</Button>
|
||||||
<Link href={`/org/${currentWorkspace?.organization}/members`}>
|
</Link>
|
||||||
<Button variant="outline_bg">Create a new identity</Button>
|
</div>
|
||||||
</Link>
|
)}
|
||||||
</div>
|
</ModalContent>
|
||||||
)}
|
</Modal>
|
||||||
</ModalContent>
|
);
|
||||||
</Modal>
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+57
-67
@@ -4,14 +4,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
Button,
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
DeleteActionModal
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
useWorkspace} from "@app/context";
|
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
import { useDeleteIdentityFromWorkspace } from "@app/hooks/api";
|
import { useDeleteIdentityFromWorkspace } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
@@ -24,11 +18,11 @@ export const IdentitySection = withProjectPermission(
|
|||||||
const { createNotification } = useNotificationContext();
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const workspaceId = currentWorkspace?._id ?? "";
|
const workspaceId = currentWorkspace?.id ?? "";
|
||||||
|
|
||||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"identity",
|
"identity",
|
||||||
"deleteIdentity",
|
"deleteIdentity",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
@@ -36,81 +30,77 @@ export const IdentitySection = withProjectPermission(
|
|||||||
|
|
||||||
const onRemoveIdentitySubmit = async (identityId: string) => {
|
const onRemoveIdentitySubmit = async (identityId: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
await deleteMutateAsync({
|
await deleteMutateAsync({
|
||||||
identityId,
|
identityId,
|
||||||
workspaceId
|
workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully removed identity from project",
|
text: "Successfully removed identity from project",
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpClose("deleteIdentity");
|
handlePopUpClose("deleteIdentity");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const error = err as any;
|
const error = err as any;
|
||||||
const text = error?.response?.data?.message ?? "Failed to remove identity from project"
|
const text = error?.response?.data?.message ?? "Failed to remove identity from project";
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text,
|
text,
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">
|
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||||
Identities
|
<div className="flex w-full justify-end pr-4">
|
||||||
</p>
|
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
|
||||||
<div className="flex justify-end w-full pr-4">
|
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||||
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
|
Documentation{" "}
|
||||||
<span className="rounded-md px-4 py-2 w-max text-mineshaft-200 hover:text-white bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/10 hover:border-primary/40 duration-200 cursor-pointer">
|
<FontAwesomeIcon
|
||||||
Documentation <FontAwesomeIcon icon={faArrowUpRightFromSquare} className="text-xs mb-[0.06rem] ml-1"/>
|
icon={faArrowUpRightFromSquare}
|
||||||
</span>
|
className="mb-[0.06rem] ml-1 text-xs"
|
||||||
</Link>
|
/>
|
||||||
</div>
|
</span>
|
||||||
<ProjectPermissionCan
|
</Link>
|
||||||
I={ProjectPermissionActions.Create}
|
|
||||||
a={ProjectPermissionSub.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Button
|
|
||||||
colorSchema="primary"
|
|
||||||
type="submit"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
onClick={() => handlePopUpOpen("identity")}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
Add identity
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</div>
|
</div>
|
||||||
<IdentityTable
|
<ProjectPermissionCan
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
I={ProjectPermissionActions.Create}
|
||||||
/>
|
a={ProjectPermissionSub.Identity}
|
||||||
<IdentityModal
|
>
|
||||||
popUp={popUp}
|
{(isAllowed) => (
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
<Button
|
||||||
/>
|
colorSchema="primary"
|
||||||
<DeleteActionModal
|
type="submit"
|
||||||
isOpen={popUp.deleteIdentity.isOpen}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
title={`Are you sure want to remove ${
|
onClick={() => handlePopUpOpen("identity")}
|
||||||
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
isDisabled={!isAllowed}
|
||||||
} from the project?`}
|
>
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
Add identity
|
||||||
deleteKey="confirm"
|
</Button>
|
||||||
onDeleteApproved={() =>
|
)}
|
||||||
onRemoveIdentitySubmit(
|
</ProjectPermissionCan>
|
||||||
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteIdentity.isOpen}
|
||||||
|
title={`Are you sure want to remove ${
|
||||||
|
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
||||||
|
} from the project?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemoveIdentitySubmit(
|
||||||
|
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
|
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
|
||||||
|
|||||||
+145
-164
@@ -5,187 +5,168 @@ import { format } from "date-fns";
|
|||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
IconButton,
|
IconButton,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
TBody,
|
TBody,
|
||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useWorkspace,
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetRoles,
|
useGetRoles,
|
||||||
useGetWorkspaceIdentityMemberships,
|
useGetWorkspaceIdentityMemberships,
|
||||||
useUpdateIdentityWorkspaceRole} from "@app/hooks/api";
|
useUpdateIdentityWorkspaceRole
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["deleteIdentity", "identity"]>,
|
popUpName: keyof UsePopUpState<["deleteIdentity", "identity"]>,
|
||||||
data?: {
|
data?: {
|
||||||
identityId?: string;
|
identityId?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityTable = ({
|
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||||
handlePopUpOpen
|
const { createNotification } = useNotificationContext();
|
||||||
}: Props) => {
|
const { currentOrg } = useOrganization();
|
||||||
const { createNotification } = useNotificationContext();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const orgId = currentOrg?.id || "";
|
||||||
const { currentWorkspace } = useWorkspace();
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const orgId = currentOrg?._id || "";
|
const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?.id || "");
|
||||||
const workspaceId = currentWorkspace?._id || "";
|
|
||||||
const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?._id || "");
|
|
||||||
|
|
||||||
const { data: roles } = useGetRoles({
|
const { data: roles } = useGetRoles({
|
||||||
orgId,
|
orgId,
|
||||||
workspaceId
|
workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityWorkspaceRole();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityWorkspaceRole();
|
||||||
|
|
||||||
const handleChangeRole = async ({
|
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
|
||||||
|
try {
|
||||||
|
await updateMutateAsync({
|
||||||
identityId,
|
identityId,
|
||||||
|
workspaceId,
|
||||||
role
|
role
|
||||||
}: {
|
});
|
||||||
identityId: string;
|
|
||||||
role: string;
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
await updateMutateAsync({
|
createNotification({
|
||||||
identityId,
|
text: "Successfully updated identity role",
|
||||||
workspaceId,
|
type: "success"
|
||||||
role
|
});
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to update identity role";
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Successfully updated identity role",
|
text,
|
||||||
type: "success"
|
type: "error"
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
const error = err as any;
|
|
||||||
const text = error?.response?.data?.message ?? "Failed to update identity role"
|
|
||||||
|
|
||||||
createNotification({
|
|
||||||
text,
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Role</Th>
|
<Th>Role</Th>
|
||||||
<Th>Added on</Th>
|
<Th>Added on</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{isLoading && <TableSkeleton columns={7} innerKey="project-identities" />}
|
{isLoading && <TableSkeleton columns={7} innerKey="project-identities" />}
|
||||||
{!isLoading &&
|
{!isLoading &&
|
||||||
data &&
|
data &&
|
||||||
data.length > 0 &&
|
data.length > 0 &&
|
||||||
data.map(({
|
data.map(({ identity: { id, name }, role, customRole, createdAt }) => {
|
||||||
identity: {
|
return (
|
||||||
_id,
|
<Tr className="h-10" key={`st-v3-${id}`}>
|
||||||
name
|
<Td>{name}</Td>
|
||||||
},
|
<Td>
|
||||||
role,
|
<ProjectPermissionCan
|
||||||
customRole,
|
I={ProjectPermissionActions.Edit}
|
||||||
createdAt
|
a={ProjectPermissionSub.Identity}
|
||||||
}) => {
|
>
|
||||||
|
{(isAllowed) => {
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
<Select
|
||||||
<Td>{name}</Td>
|
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||||
<Td>
|
isDisabled={!isAllowed}
|
||||||
<ProjectPermissionCan
|
className="w-40 bg-mineshaft-600"
|
||||||
I={ProjectPermissionActions.Edit}
|
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||||
a={ProjectPermissionSub.Identity}
|
onValueChange={(selectedRole) =>
|
||||||
>
|
handleChangeRole({
|
||||||
{(isAllowed) => {
|
identityId: id,
|
||||||
return (
|
role: selectedRole
|
||||||
<Select
|
})
|
||||||
value={
|
}
|
||||||
role === "custom" ? (customRole?.slug as string) : role
|
>
|
||||||
}
|
{(roles || []).map(({ slug, name: roleName }) => (
|
||||||
isDisabled={!isAllowed}
|
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||||
className="w-40 bg-mineshaft-600"
|
{roleName}
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
</SelectItem>
|
||||||
onValueChange={(selectedRole) =>
|
))}
|
||||||
handleChangeRole({
|
</Select>
|
||||||
identityId: _id,
|
|
||||||
role: selectedRole
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(roles || [])
|
|
||||||
.map(({ slug, name: roleName }) => (
|
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
|
||||||
{roleName}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</Td>
|
|
||||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
|
||||||
<Td className="flex justify-end">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Delete}
|
|
||||||
a={ProjectPermissionSub.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("deleteIdentity", {
|
|
||||||
identityId: _id,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="ml-4"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
);
|
||||||
})}
|
}}
|
||||||
{!isLoading && data && data?.length === 0 && (
|
</ProjectPermissionCan>
|
||||||
<Tr>
|
</Td>
|
||||||
<Td colSpan={7}>
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||||
<EmptyState title="No identities have been added to this project" icon={faServer} />
|
<Td className="flex justify-end">
|
||||||
</Td>
|
<ProjectPermissionCan
|
||||||
</Tr>
|
I={ProjectPermissionActions.Delete}
|
||||||
)}
|
a={ProjectPermissionSub.Identity}
|
||||||
</TBody>
|
>
|
||||||
</Table>
|
{(isAllowed) => (
|
||||||
</TableContainer>
|
<IconButton
|
||||||
);
|
onClick={() => {
|
||||||
}
|
handlePopUpOpen("deleteIdentity", {
|
||||||
|
identityId: id,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={7}>
|
||||||
|
<EmptyState title="No identities have been added to this project" icon={faServer} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export const SSOModal = ({ popUp, handlePopUpClose, handlePopUpToggle }: Props)
|
|||||||
<>
|
<>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-sm text-mineshaft-400">{renderLabels(authProvider).acsUrl}</h3>
|
<h3 className="text-sm text-mineshaft-400">{renderLabels(authProvider).acsUrl}</h3>
|
||||||
<p className="text-md break-all text-gray-400">{`${window.origin}/api/v1/sso/saml2/${data._id}`}</p>
|
<p className="text-md break-all text-gray-400">{`${window.origin}/api/v1/sso/saml2/${data.id}`}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-sm text-mineshaft-400">
|
<h3 className="text-sm text-mineshaft-400">
|
||||||
|
|||||||
Reference in New Issue
Block a user