mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-13 21:29:31 +00:00
Make progress on SCIM
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button } from "../v2";
|
||||
@@ -14,7 +12,6 @@ export default function InitialSignupStep({
|
||||
setIsSignupWithEmail: (value: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full flex-col items-center justify-center">
|
||||
@@ -76,17 +73,6 @@ export default function InitialSignupStep({
|
||||
Continue with Email
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
|
||||
<Button
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => router.push("/saml-sso")}
|
||||
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
|
||||
className="mx-0 h-12 w-full"
|
||||
>
|
||||
Continue with SSO
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6 w-1/4 min-w-[20rem] px-8 text-center text-xs text-bunker-400 lg:w-1/6">
|
||||
{t("signup.create-policy")}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const OrgProvider = ({ children }: Props): JSX.Element => {
|
||||
const value = useMemo<TOrgContext>(
|
||||
() => ({
|
||||
orgs: userOrgs,
|
||||
currentOrg: (userOrgs || []).find(({ id }) => id === currentWsOrgID) || (userOrgs || [])[0],
|
||||
currentOrg: (userOrgs || []).find(({ id }) => id === currentWsOrgID),
|
||||
isLoading
|
||||
}),
|
||||
[currentWsOrgID, userOrgs, isLoading]
|
||||
|
||||
@@ -17,6 +17,6 @@ export {
|
||||
useGetOrgPmtMethods,
|
||||
useGetOrgTaxIds,
|
||||
useGetOrgTrialUrl,
|
||||
useRenameOrg,
|
||||
useUpdateOrg,
|
||||
useUpdateOrgBillingDetails
|
||||
} from "./queries";
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
PlanBillingInfo,
|
||||
PmtMethod,
|
||||
ProductsTable,
|
||||
RenameOrgDTO,
|
||||
TaxID
|
||||
TaxID,
|
||||
UpdateOrgDTO
|
||||
} from "./types";
|
||||
|
||||
export const organizationKeys = {
|
||||
@@ -65,12 +65,20 @@ export const useCreateOrg = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useRenameOrg = () => {
|
||||
export const useUpdateOrg = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, RenameOrgDTO>({
|
||||
mutationFn: ({ newOrgName, orgId }) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}/name`, { name: newOrgName });
|
||||
return useMutation<{}, {}, UpdateOrgDTO>({
|
||||
mutationFn: ({
|
||||
name,
|
||||
authEnforced,
|
||||
slug,
|
||||
orgId
|
||||
}) => {
|
||||
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
|
||||
name,
|
||||
authEnforced,
|
||||
slug
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
|
||||
|
||||
@@ -3,11 +3,15 @@ export type Organization = {
|
||||
name: string;
|
||||
createAt: string;
|
||||
updatedAt: string;
|
||||
authEnforced: boolean;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type RenameOrgDTO = {
|
||||
export type UpdateOrgDTO = {
|
||||
orgId: string;
|
||||
newOrgName: string;
|
||||
name?: string;
|
||||
authEnforced?: boolean;
|
||||
slug?: string;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
|
||||
@@ -310,10 +310,25 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="px-2 py-1 text-xs text-mineshaft-400">{user?.email}</div>
|
||||
{orgs?.map((org) => (
|
||||
{orgs?.map((org) => {
|
||||
return (
|
||||
<DropdownMenuItem key={org.id}>
|
||||
<Button
|
||||
onClick={() => changeOrg(org?.id)}
|
||||
onClick={async () => {
|
||||
if (currentOrg?.id === org.id) return;
|
||||
|
||||
if (org.authEnforced) {
|
||||
// org has an org-level auth method enabled (e.g. SAML)
|
||||
// -> logout + redirect to SAML SSO
|
||||
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
changeOrg(org?.id)
|
||||
}}
|
||||
variant="plain"
|
||||
colorSchema="secondary"
|
||||
size="xs"
|
||||
@@ -329,7 +344,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
{/* <DropdownMenuItem key="add-org">
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("createOrg")}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
MFAStep,
|
||||
SAMLSSOStep
|
||||
} from "./components";
|
||||
// import { navigateUserToOrg } from "../../Login.utils";
|
||||
import { navigateUserToOrg } from "./Login.utils";
|
||||
|
||||
export const Login = () => {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||
|
||||
export const navigateUserToOrg = async (router: NextRouter, organizationId?: string) => {
|
||||
const userOrgs = await fetchOrganizations();
|
||||
|
||||
const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced);
|
||||
|
||||
if (organizationId) {
|
||||
localStorage.setItem("orgData.id", organizationId);
|
||||
@@ -11,13 +13,13 @@ export const navigateUserToOrg = async (router: NextRouter, organizationId?: str
|
||||
return;
|
||||
}
|
||||
|
||||
if (userOrgs.length > 0) {
|
||||
// user is part of at least 1 org
|
||||
const userOrg = userOrgs[0] && userOrgs[0].id;
|
||||
if (nonAuthEnforcedOrgs.length > 0) {
|
||||
// user is part of at least 1 non-auth enforced org
|
||||
const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id;
|
||||
localStorage.setItem("orgData.id", userOrg);
|
||||
router.push(`/org/${userOrg}/overview`);
|
||||
} else {
|
||||
// user is not part of any org
|
||||
// user is not part of any non-auth enforced orgs
|
||||
localStorage.removeItem("orgData.id");
|
||||
router.push("/org/none");
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
||||
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
|
||||
className="mx-0 h-10 w-full"
|
||||
>
|
||||
Continue with SSO
|
||||
Continue with SAML SSO
|
||||
</Button>
|
||||
</div>
|
||||
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
|
||||
|
||||
@@ -18,14 +18,14 @@ export const SAMLSSOStep = ({
|
||||
const handleSubmission = (e:React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
window.open(`/api/v1/sso/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
|
||||
window.close();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md md:px-6">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||
What's your SSO Identifier?
|
||||
What's your organization slug?
|
||||
</p>
|
||||
<form onSubmit={handleSubmission}>
|
||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
@@ -34,7 +34,7 @@ export const SAMLSSOStep = ({
|
||||
value={ssoIdentifier}
|
||||
onChange={(e) => setSSOIdentifier(e.target.value)}
|
||||
type="text"
|
||||
placeholder="Enter your SSO identifier..."
|
||||
placeholder="acme-123"
|
||||
isRequired
|
||||
autoComplete="email"
|
||||
id="email"
|
||||
@@ -50,7 +50,7 @@ export const SAMLSSOStep = ({
|
||||
isFullWidth
|
||||
className="h-14"
|
||||
>
|
||||
{t("login.login")}
|
||||
Continue with SAML
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+4
-5
@@ -16,7 +16,7 @@ import {
|
||||
useOrganization,
|
||||
useSubscription
|
||||
} from "@app/context";
|
||||
import { useDeleteOrgMembership, useGetSSOConfig } from "@app/hooks/api";
|
||||
import { useDeleteOrgMembership } from "@app/hooks/api";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { AddOrgMemberModal } from "./AddOrgMemberModal";
|
||||
@@ -27,10 +27,9 @@ export const OrgMembersSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id ?? "";
|
||||
|
||||
|
||||
const [completeInviteLink, setCompleteInviteLink] = useState<string>("");
|
||||
|
||||
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
"addMember",
|
||||
"removeMember",
|
||||
@@ -45,9 +44,9 @@ export const OrgMembersSection = () => {
|
||||
: false;
|
||||
|
||||
const handleAddMemberModal = () => {
|
||||
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
|
||||
if (currentOrg?.authEnforced) {
|
||||
createNotification({
|
||||
text: "You cannot invite users when SAML SSO is configured for your organization",
|
||||
text: "You cannot invite users when org-level auth is configured for your organization",
|
||||
type: "error"
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -1,577 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faCheck,
|
||||
faCopy,
|
||||
faMagnifyingGlass,
|
||||
faPlus,
|
||||
faTrash,
|
||||
faUsers
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
decryptAssymmetric,
|
||||
encryptAssymmetric
|
||||
} from "@app/components/utilities/cryptography/crypto";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
EmailServiceSetupModal,
|
||||
EmptyState,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
Tag,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tr,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useSubscription,
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
useAddUserToOrg,
|
||||
useDeleteOrgMembership,
|
||||
useGetOrgUsers,
|
||||
useGetSSOConfig,
|
||||
useGetUserWorkspaceMemberships,
|
||||
useGetUserWsKey,
|
||||
useUpdateOrgUserRole,
|
||||
useUploadWsKey
|
||||
} from "@app/hooks/api";
|
||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
|
||||
type Props = {
|
||||
roles?: TProjectRole[];
|
||||
isRolesLoading?: boolean;
|
||||
};
|
||||
|
||||
const addMemberFormSchema = yup.object({
|
||||
email: yup.string().email().required().label("Email").trim().lowercase()
|
||||
});
|
||||
|
||||
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>;
|
||||
|
||||
export const OrgMembersTable = ({ roles = [], isRolesLoading }: Props) => {
|
||||
const router = useRouter();
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const { currentOrg } = useOrganization();
|
||||
const { workspaces, currentWorkspace } = useWorkspace();
|
||||
const { user } = useUser();
|
||||
const userId = user?.id || "";
|
||||
const orgId = currentOrg?.id || "";
|
||||
const workspaceId = currentWorkspace?.id || "";
|
||||
|
||||
const { data: ssoConfig, isLoading: isLoadingSSOConfig } = useGetSSOConfig(orgId);
|
||||
const [searchMemberFilter, setSearchMemberFilter] = useState("");
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"addMember",
|
||||
"removeMember",
|
||||
"upgradePlan",
|
||||
"setUpEmail"
|
||||
] as const);
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
|
||||
const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
|
||||
useGetUserWorkspaceMemberships(orgId);
|
||||
const { data: wsKey } = useGetUserWsKey(workspaceId);
|
||||
|
||||
const removeUserOrgMembership = useDeleteOrgMembership();
|
||||
const addUserToOrg = useAddUserToOrg();
|
||||
const updateOrgUserRole = useUpdateOrgUserRole();
|
||||
const uploadWsKey = useUploadWsKey();
|
||||
|
||||
const [completeInviteLink, setCompleteInviteLink] = useState<string | undefined>("");
|
||||
|
||||
const isMoreUsersNotAllowed = subscription?.memberLimit
|
||||
? subscription.membersUsed >= subscription.memberLimit
|
||||
: false;
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.action === "invite") {
|
||||
handlePopUpOpen("addMember");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) });
|
||||
|
||||
const onAddMember = async ({ email }: TAddMemberForm) => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
const { data } = await addUserToOrg.mutateAsync({
|
||||
organizationId: currentOrg?.id,
|
||||
inviteeEmail: email
|
||||
});
|
||||
setCompleteInviteLink(data?.completeInviteLink);
|
||||
// only show this notification when email is configured.
|
||||
// A [completeInviteLink] will not be sent if smtp is configured
|
||||
if (!data.completeInviteLink) {
|
||||
createNotification({
|
||||
text: "Successfully invited user to the organization.",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to invite user to org",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
if (serverDetails?.emailConfigured) {
|
||||
handlePopUpClose("addMember");
|
||||
}
|
||||
reset();
|
||||
};
|
||||
|
||||
const onAddUserToOrg = async (email: string) => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
const { data } = await addUserToOrg.mutateAsync({
|
||||
organizationId: currentOrg?.id,
|
||||
inviteeEmail: email
|
||||
});
|
||||
setCompleteInviteLink(data?.completeInviteLink);
|
||||
|
||||
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
|
||||
if (!data.completeInviteLink) {
|
||||
createNotification({
|
||||
text: "Successfully invited user to the organization.",
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to invite user to org",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveOrgMemberApproved = async () => {
|
||||
const membershipId = (popUp?.removeMember?.data as { id: string })?.id;
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?.id, membershipId });
|
||||
createNotification({
|
||||
text: "Successfully removed user from org",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to remove user from the organization",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
handlePopUpClose("removeMember");
|
||||
};
|
||||
|
||||
const isIamOwner = useMemo(
|
||||
() => members?.find(({ user: u }) => userId === u?.id)?.role === "owner",
|
||||
[userId, members]
|
||||
);
|
||||
|
||||
const findRoleFromId = useCallback(
|
||||
(roleId: string) => {
|
||||
return roles.find(({ id }) => id === roleId);
|
||||
},
|
||||
[roles]
|
||||
);
|
||||
|
||||
const filterdUser = useMemo(
|
||||
() =>
|
||||
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]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isInviteLinkCopied) {
|
||||
timer = setTimeout(() => setInviteLinkCopied.off(), 2000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [isInviteLinkCopied]);
|
||||
|
||||
const onRoleChange = async (membershipId: string, role: string) => {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
try {
|
||||
await updateOrgUserRole.mutateAsync({ organizationId: currentOrg?.id, membershipId, role });
|
||||
createNotification({
|
||||
text: "Successfully updated user role",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to update user role",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onGrantAccess = async (grantedUserId: string, publicKey: string) => {
|
||||
try {
|
||||
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
|
||||
if (!PRIVATE_KEY || !wsKey) return;
|
||||
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: wsKey.encryptedKey,
|
||||
nonce: wsKey.nonce,
|
||||
publicKey: wsKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
const { ciphertext, nonce } = encryptAssymmetric({
|
||||
plaintext: key,
|
||||
publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
await uploadWsKey.mutateAsync({
|
||||
userId: grantedUserId,
|
||||
nonce,
|
||||
encryptedKey: ciphertext,
|
||||
workspaceId: currentWorkspace?.id || ""
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to grant access to user",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
navigator.clipboard.writeText(completeInviteLink as string);
|
||||
setInviteLinkCopied.on();
|
||||
};
|
||||
|
||||
const isLoading = isMembersLoading || IsWsMembershipLoading || isRolesLoading;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex">
|
||||
<div className="mr-4 flex-1">
|
||||
<Input
|
||||
value={searchMemberFilter}
|
||||
onChange={(e) => setSearchMemberFilter(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search members..."
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (!isLoadingSSOConfig && ssoConfig && ssoConfig.isActive) {
|
||||
createNotification({
|
||||
text: "You cannot invite users when SAML SSO is configured for your organization",
|
||||
type: "error"
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMoreUsersNotAllowed) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
} else {
|
||||
handlePopUpOpen("addMember");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Email</Th>
|
||||
<Th>Role</Th>
|
||||
<Th>Projects</Th>
|
||||
<Th aria-label="actions" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={5} innerKey="org-members" />}
|
||||
{!isLoading &&
|
||||
filterdUser?.map(
|
||||
({ user: u, inviteEmail, role, roleId, id: orgMembershipId, status }) => {
|
||||
const name = u ? `${u.firstName || "-"} ${u.lastName || ""}` : "-";
|
||||
const email = u?.email || inviteEmail;
|
||||
const userWs = workspaceMemberships?.[u?.id];
|
||||
|
||||
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
|
||||
defaultValue={
|
||||
role === "custom" ? findRoleFromId(roleId)?.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={() => onAddUserToOrg(email)}
|
||||
>
|
||||
Resend Invite
|
||||
</Button>
|
||||
)}
|
||||
{status === "completed" && (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => onGrantAccess(u?.id, u?.publicKey)}
|
||||
>
|
||||
Grant Access
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
{userWs ? (
|
||||
userWs?.map(({ name: wsName, id }) => (
|
||||
<Tag key={`user-${u.id}-workspace-${id}`} className="my-1">
|
||||
{wsName}
|
||||
</Tag>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-row">
|
||||
{(status === "invited" || status === "verified") &&
|
||||
serverDetails?.emailConfigured ? (
|
||||
<Tag colorSchema="red">
|
||||
This user hasn't accepted the invite yet
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag colorSchema="red">
|
||||
This user isn't part of any projects yet
|
||||
</Tag>
|
||||
)}
|
||||
{router.query.id !== "undefined" &&
|
||||
!(
|
||||
(status === "invited" || status === "verified") &&
|
||||
serverDetails?.emailConfigured
|
||||
) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
router.push(`/project/${workspaces[0]?.id}/members`)
|
||||
}
|
||||
className="w-max cursor-pointer rounded-sm bg-mineshaft px-1.5 py-0.5 text-sm duration-200 hover:bg-primary hover:text-black"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
||||
Add to projects
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Td>
|
||||
<Td>
|
||||
{userId !== u?.id && (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="danger"
|
||||
isDisabled={userId === u?.id || !isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("removeMember", { id: orgMembershipId })
|
||||
}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && filterdUser?.length === 0 && (
|
||||
<EmptyState title="No project members found" icon={faUsers} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<Modal
|
||||
isOpen={popUp?.addMember?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addMember", isOpen);
|
||||
setCompleteInviteLink(undefined);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
{!completeInviteLink && (
|
||||
<form onSubmit={handleSubmit(onAddMember)}>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="email"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add Member
|
||||
</Button>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpClose("addMember")}
|
||||
>
|
||||
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>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this user from the org?"
|
||||
onChange={(isOpen) => handlePopUpToggle("removeMember", isOpen)}
|
||||
onDeleteApproved={onRemoveOrgMemberApproved}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can add custom environments if you switch to Infisical's Team plan."
|
||||
/>
|
||||
<EmailServiceSetupModal
|
||||
isOpen={popUp.setUpEmail?.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("setUpEmail", isOpen)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { CreateOrgModal } from "../components";
|
||||
|
||||
export const NonePage = () => {
|
||||
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
|
||||
|
||||
useEffect(() => {
|
||||
handlePopUpToggle("createOrg", true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
@@ -13,4 +19,4 @@ export const NonePage = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
@@ -1,15 +1,17 @@
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { OrgSCIMSection } from "./OrgSCIMSection";
|
||||
import { OrgScimSection } from "./OrgScimSection";
|
||||
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
|
||||
import { OrgSSOSection } from "./OrgSSOSection";
|
||||
|
||||
export const OrgAuthTab = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<div>
|
||||
<OrgGeneralAuthSection />
|
||||
<OrgSSOSection />
|
||||
<OrgSCIMSection />
|
||||
<OrgScimSection />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Switch } from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization
|
||||
} from "@app/context";
|
||||
import { useLogoutUser,useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
export const OrgGeneralAuthSection = () => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { currentOrg } = useOrganization();
|
||||
|
||||
const { mutateAsync } = useUpdateOrg();
|
||||
|
||||
const logout = useLogoutUser();
|
||||
|
||||
const handleEnforceOrgAuthToggle = async (value: boolean) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
orgId: currentOrg?.id,
|
||||
authEnforced: value
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${value ? "enforced" : "un-enforced"} org-level auth`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
if (value) {
|
||||
await logout.mutateAsync();
|
||||
window.open(`/api/v1/sso/redirect/saml2/organizations/${currentOrg.slug}`);
|
||||
window.close();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: `Failed to ${value ? "enforce" : "un-enforce"} org-level auth`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="flex-1 text-xl font-semibold text-white mb-8">Settings</h2>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="enforce-org-auth"
|
||||
onCheckedChange={(value) => handleEnforceOrgAuthToggle(value)}
|
||||
isChecked={currentOrg?.authEnforced ?? false}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Enforce SAML SSO
|
||||
</Switch>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+18
-4
@@ -1,11 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faPlus, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
// import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
// import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
// Button,
|
||||
Button,
|
||||
IconButton,
|
||||
Switch
|
||||
} from "@app/components/v2";
|
||||
@@ -21,7 +21,7 @@ import { useGetScimToken } from "@app/hooks/api";
|
||||
|
||||
// TODO: add permissioning for enteprise SCIM
|
||||
|
||||
export const OrgSCIMSection = () => {
|
||||
export const OrgScimSection = () => {
|
||||
const { currentOrg } = useOrganization();
|
||||
// const { createNotification } = useNotificationContext();
|
||||
// const { subscription } = useSubscription();
|
||||
@@ -36,6 +36,10 @@ export const OrgSCIMSection = () => {
|
||||
|
||||
// TODO: get SCIM stuf
|
||||
|
||||
const addScimTokenBtnClick = () => {
|
||||
|
||||
}
|
||||
|
||||
const handleSCIMToggle = (value: boolean) => {
|
||||
// TODO
|
||||
try {
|
||||
@@ -52,7 +56,17 @@ export const OrgSCIMSection = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<h2 className="flex-1 text-xl font-semibold text-white mb-8">SCIM Configuration</h2>
|
||||
<div className="mb-8 flex items-center">
|
||||
<h2 className="flex-1 text-xl font-semibold text-white">SCIM Configuration</h2>
|
||||
<Button
|
||||
onClick={addScimTokenBtnClick}
|
||||
colorSchema="secondary"
|
||||
// isDisabled={!isAllowed}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Add SCIM Token
|
||||
</Button>
|
||||
</div>
|
||||
<Switch
|
||||
id="enable-scim"
|
||||
onCheckedChange={(value) => handleSCIMToggle(value)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
@@ -136,6 +137,10 @@ export const OrgSSOSection = (): JSX.Element => {
|
||||
<h3 className="text-sm text-mineshaft-400">Issuer</h3>
|
||||
<p className="text-md text-gray-400">{data && data.issuer !== "" ? data.issuer : "-"}</p>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm text-mineshaft-400">Last Logged In</h3>
|
||||
<p className="text-md text-gray-400">{data?.lastUsed ? format(new Date(data?.lastUsed), "yyyy-MM-dd HH:mm:ss") : "-"}</p>
|
||||
</div>
|
||||
<SSOModal
|
||||
popUp={popUp}
|
||||
handlePopUpClose={handlePopUpClose}
|
||||
|
||||
+3
-1
@@ -3,6 +3,7 @@ import { useOrgPermission } from "@app/context";
|
||||
import { OrgDeleteSection } from "../OrgDeleteSection";
|
||||
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
|
||||
import { OrgNameChangeSection } from "../OrgNameChangeSection";
|
||||
import { OrgSlugChangeSection } from "../OrgSlugChangeSection";
|
||||
|
||||
export const OrgGeneralTab = () => {
|
||||
const { membership } = useOrgPermission();
|
||||
@@ -10,8 +11,9 @@ export const OrgGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<OrgNameChangeSection />
|
||||
<OrgSlugChangeSection />
|
||||
<OrgIncidentContactsSection />
|
||||
{membership && membership.role === "admin" && <OrgDeleteSection />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
+4
-4
@@ -7,7 +7,7 @@ import { useNotificationContext } from "@app/components/context/Notifications/No
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useRenameOrg } from "@app/hooks/api";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup.string().required().label("Project Name")
|
||||
@@ -21,7 +21,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
const { mutateAsync, isLoading } = useRenameOrg();
|
||||
const { mutateAsync, isLoading } = useUpdateOrg();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrg) {
|
||||
@@ -34,7 +34,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
|
||||
if (!currentOrg?.id) return;
|
||||
if (name === "") return;
|
||||
|
||||
await mutateAsync({ orgId: currentOrg?.id, newOrgName: name });
|
||||
await mutateAsync({ orgId: currentOrg?.id, name });
|
||||
createNotification({
|
||||
text: "Successfully renamed organization",
|
||||
type: "success"
|
||||
@@ -53,7 +53,7 @@ export const OrgNameChangeSection = (): JSX.Element => {
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Name</p>
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Organization Name</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useUpdateOrg } from "@app/hooks/api";
|
||||
|
||||
const formSchema = yup.object({
|
||||
slug: yup
|
||||
.string()
|
||||
.matches(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens")
|
||||
.required()
|
||||
.label("Project Slug")
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const OrgSlugChangeSection = (): JSX.Element => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({
|
||||
resolver: yupResolver(formSchema)
|
||||
});
|
||||
const { mutateAsync, isLoading } = useUpdateOrg();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentOrg) {
|
||||
reset({ slug: currentOrg.slug });
|
||||
}
|
||||
}, [currentOrg]);
|
||||
|
||||
const onFormSubmit = async ({ slug }: FormData) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
if (slug === "") return;
|
||||
|
||||
await mutateAsync({ orgId: currentOrg?.id, slug });
|
||||
createNotification({
|
||||
text: "Successfully updated organization slug",
|
||||
type: "success"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
createNotification({
|
||||
text: "Failed to update organization slug",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<p className="mb-4 text-xl font-semibold text-mineshaft-100">Organization Slug</p>
|
||||
<div className="mb-2 max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input placeholder="acme" {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="slug"
|
||||
/>
|
||||
</div>
|
||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Settings}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { OrgSlugChangeSection } from "./OrgSlugChangeSection";
|
||||
-13
@@ -25,8 +25,6 @@ const authMethodOpts: AuthMethodOption[] = [
|
||||
{ label: "GitLab", value: AuthMethod.GITLAB, icon: faGitlab }
|
||||
];
|
||||
|
||||
const samlProviders = [AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.AZURE_SAML];
|
||||
|
||||
const schema = yup.object({
|
||||
authMethods: yup.array().required("Auth method is required")
|
||||
});
|
||||
@@ -56,17 +54,6 @@ export const AuthMethodSection = () => {
|
||||
}, [user]);
|
||||
|
||||
const onAuthMethodToggle = async (value: boolean, authMethodOpt: AuthMethodOption) => {
|
||||
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
|
||||
samlProviders.includes(authMethod)
|
||||
);
|
||||
|
||||
if (hasSamlEnabled) {
|
||||
createNotification({
|
||||
text: "SAML authentication can only be configured in your organization settings",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
|
||||
const newAuthMethods = value
|
||||
? [...authMethods, authMethodOpt.value]
|
||||
: authMethods.filter((auth) => auth !== authMethodOpt.value);
|
||||
|
||||
Reference in New Issue
Block a user