mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-06 23:59:02 +00:00
remove idp user if unassociate idp, warn, and fix create user form bug
This commit is contained in:
@@ -3346,6 +3346,8 @@
|
|||||||
"idpUnassociateQuestion": "Are you sure you want to unassociate this identity provider from this organization?",
|
"idpUnassociateQuestion": "Are you sure you want to unassociate this identity provider from this organization?",
|
||||||
"idpUnassociateDescription": "All users associated with this identity provider will be removed from this organization, but the identity provider will still continue to exist for other associated organizations.",
|
"idpUnassociateDescription": "All users associated with this identity provider will be removed from this organization, but the identity provider will still continue to exist for other associated organizations.",
|
||||||
"idpUnassociateConfirm": "Confirm Unassociate Identity Provider",
|
"idpUnassociateConfirm": "Confirm Unassociate Identity Provider",
|
||||||
|
"idpConfirmDeleteAndRemoveMeFromOrg": "DELETE AND REMOVE ME FROM ORG",
|
||||||
|
"idpUnassociateAndRemoveMeFromOrg": "UNASSOCIATE AND REMOVE ME FROM ORG",
|
||||||
"idpUnassociateWarning": "This cannot be undone for this organization.",
|
"idpUnassociateWarning": "This cannot be undone for this organization.",
|
||||||
"idpUnassociatedDescription": "Identity provider unassociated from this organization successfully",
|
"idpUnassociatedDescription": "Identity provider unassociated from this organization successfully",
|
||||||
"idpUnassociateMenu": "Unassociate",
|
"idpUnassociateMenu": "Unassociate",
|
||||||
|
|||||||
@@ -13,13 +13,15 @@
|
|||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, idpOrg } from "@server/db";
|
import { db, idpOrg, orgs, primaryDb, users, userOrgs } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { and, eq, sql } from "drizzle-orm";
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
|
import { removeUserFromOrg } from "@server/lib/userOrg";
|
||||||
|
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
@@ -76,9 +78,55 @@ export async function unassociateOrgIdp(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db
|
const [org] = await db
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgUsersFromIdp = await db
|
||||||
|
.select({
|
||||||
|
userId: userOrgs.userId,
|
||||||
|
isOwner: userOrgs.isOwner
|
||||||
|
})
|
||||||
|
.from(userOrgs)
|
||||||
|
.innerJoin(users, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(and(eq(userOrgs.orgId, orgId), eq(users.idpId, idpId)));
|
||||||
|
|
||||||
|
if (orgUsersFromIdp.some((u) => u.isOwner)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Cannot unassociate identity provider while an organization owner uses it"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIdsToRemove = orgUsersFromIdp.map((u) => u.userId);
|
||||||
|
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
for (const userId of userIdsToRemove) {
|
||||||
|
await removeUserFromOrg(org, userId, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx
|
||||||
.delete(idpOrg)
|
.delete(idpOrg)
|
||||||
.where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId)));
|
.where(and(eq(idpOrg.idpId, idpId), eq(idpOrg.orgId, orgId)));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const userId of userIdsToRemove) {
|
||||||
|
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => {
|
||||||
|
logger.error(
|
||||||
|
`Failed to calculate user clients after removing user ${userId} from org ${orgId} during IdP unassociation: ${e}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return response<null>(res, {
|
return response<null>(res, {
|
||||||
data: null,
|
data: null,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { StrategyOption, StrategySelect } from "@app/components/StrategySelect";
|
|||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useActionState, useState } from "react";
|
import { useActionState, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -205,23 +205,7 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const prevSelectedOptionRef = useRef(selectedOption);
|
||||||
if (selectedOption === "internal") {
|
|
||||||
setSendEmail(env.email.emailEnabled);
|
|
||||||
internalForm.reset();
|
|
||||||
setInviteLink(null);
|
|
||||||
setExpiresInDays(1);
|
|
||||||
} else if (selectedOption && selectedOption !== "internal") {
|
|
||||||
googleAzureForm.reset();
|
|
||||||
genericOidcForm.reset();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
selectedOption,
|
|
||||||
env.email.emailEnabled,
|
|
||||||
internalForm,
|
|
||||||
googleAzureForm,
|
|
||||||
genericOidcForm
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
@@ -315,19 +299,10 @@ export default function Page() {
|
|||||||
onSubmitInternal,
|
onSubmitInternal,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [, submitGoogleAzureAction, isSubmittingGoogleAzure] = useActionState(
|
const [isSubmittingExternal, setIsSubmittingExternal] = useState(false);
|
||||||
onSubmitGoogleAzure,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [, submitGenericOidcAction, isSubmittingGenericOidc] = useActionState(
|
|
||||||
onSubmitGenericOidc,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const loading =
|
const loading =
|
||||||
isSubmittingInternal ||
|
isSubmittingInternal || isSubmittingExternal;
|
||||||
isSubmittingGoogleAzure ||
|
|
||||||
isSubmittingGenericOidc;
|
|
||||||
|
|
||||||
async function onSubmitInternal() {
|
async function onSubmitInternal() {
|
||||||
const isValid = await internalForm.trigger();
|
const isValid = await internalForm.trigger();
|
||||||
@@ -378,17 +353,16 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitGoogleAzure() {
|
async function onSubmitGoogleAzure(
|
||||||
const isValid = await googleAzureForm.trigger();
|
values: z.infer<typeof googleAzureFormSchema>
|
||||||
if (!isValid) return;
|
) {
|
||||||
|
|
||||||
const values = googleAzureForm.getValues();
|
|
||||||
|
|
||||||
const selectedUserOption = userOptions.find(
|
const selectedUserOption = userOptions.find(
|
||||||
(opt) => opt.id === selectedOption
|
(opt) => opt.id === selectedOption
|
||||||
);
|
);
|
||||||
if (!selectedUserOption?.idpId) return;
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
|
setIsSubmittingExternal(true);
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -419,19 +393,20 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSubmittingExternal(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSubmitGenericOidc() {
|
async function onSubmitGenericOidc(
|
||||||
const isValid = await genericOidcForm.trigger();
|
values: z.infer<typeof genericOidcFormSchema>
|
||||||
if (!isValid) return;
|
) {
|
||||||
|
|
||||||
const values = genericOidcForm.getValues();
|
|
||||||
|
|
||||||
const selectedUserOption = userOptions.find(
|
const selectedUserOption = userOptions.find(
|
||||||
(opt) => opt.id === selectedOption
|
(opt) => opt.id === selectedOption
|
||||||
);
|
);
|
||||||
if (!selectedUserOption?.idpId) return;
|
if (!selectedUserOption?.idpId) return;
|
||||||
|
|
||||||
|
setIsSubmittingExternal(true);
|
||||||
|
|
||||||
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
const roleIds = values.roles.map((r) => parseInt(r.id, 10));
|
||||||
|
|
||||||
const res = await api
|
const res = await api
|
||||||
@@ -462,6 +437,27 @@ export default function Page() {
|
|||||||
});
|
});
|
||||||
router.push(`/${orgId}/settings/access/users`);
|
router.push(`/${orgId}/settings/access/users`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsSubmittingExternal(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUserTypeChange(value: string) {
|
||||||
|
if (prevSelectedOptionRef.current === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevSelectedOptionRef.current = value;
|
||||||
|
setSelectedOption(value);
|
||||||
|
|
||||||
|
if (value === "internal") {
|
||||||
|
setSendEmail(env.email.emailEnabled);
|
||||||
|
internalForm.reset();
|
||||||
|
setInviteLink(null);
|
||||||
|
setExpiresInDays(1);
|
||||||
|
} else {
|
||||||
|
googleAzureForm.reset();
|
||||||
|
genericOidcForm.reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -496,16 +492,8 @@ export default function Page() {
|
|||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<StrategySelect
|
<StrategySelect
|
||||||
options={userOptions}
|
options={userOptions}
|
||||||
defaultValue={selectedOption || undefined}
|
value={selectedOption}
|
||||||
onChange={(value) => {
|
onChange={handleUserTypeChange}
|
||||||
setSelectedOption(value);
|
|
||||||
if (value === "internal") {
|
|
||||||
internalForm.reset();
|
|
||||||
} else {
|
|
||||||
googleAzureForm.reset();
|
|
||||||
genericOidcForm.reset();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
cols={3}
|
cols={3}
|
||||||
/>
|
/>
|
||||||
</SettingsSectionBody>
|
</SettingsSectionBody>
|
||||||
@@ -714,9 +702,9 @@ export default function Page() {
|
|||||||
})() && (
|
})() && (
|
||||||
<Form {...googleAzureForm}>
|
<Form {...googleAzureForm}>
|
||||||
<form
|
<form
|
||||||
action={
|
onSubmit={googleAzureForm.handleSubmit(
|
||||||
submitGoogleAzureAction
|
onSubmitGoogleAzure
|
||||||
}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
@@ -797,9 +785,9 @@ export default function Page() {
|
|||||||
})() && (
|
})() && (
|
||||||
<Form {...genericOidcForm}>
|
<Form {...genericOidcForm}>
|
||||||
<form
|
<form
|
||||||
action={
|
onSubmit={genericOidcForm.handleSubmit(
|
||||||
submitGenericOidcAction
|
onSubmitGenericOidc
|
||||||
}
|
)}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
id="create-user-form"
|
id="create-user-form"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -125,6 +125,13 @@ function IdpImportRowIcon({
|
|||||||
return <IdpTypeIcon type={type} variant={variant} size={20} />;
|
return <IdpTypeIcon type={type} variant={variant} size={20} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isUserMemberOfIdp(
|
||||||
|
userIdpId: number | null | undefined,
|
||||||
|
idpId: number
|
||||||
|
) {
|
||||||
|
return userIdpId != null && userIdpId === idpId;
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
idps: IdpRow[];
|
idps: IdpRow[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -362,9 +369,17 @@ export default function IdpTable({ idps, orgId }: Props) {
|
|||||||
<p>{t("idpDeleteGlobalDescription")}</p>
|
<p>{t("idpDeleteGlobalDescription")}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t("idpConfirmDelete")}
|
buttonText={
|
||||||
|
isUserMemberOfIdp(user.idpId, selectedIdp.idpId)
|
||||||
|
? t("idpConfirmDeleteAndRemoveMeFromOrg")
|
||||||
|
: t("idpConfirmDelete")
|
||||||
|
}
|
||||||
onConfirm={async () => deleteIdp(selectedIdp.idpId)}
|
onConfirm={async () => deleteIdp(selectedIdp.idpId)}
|
||||||
string={selectedIdp.name}
|
string={
|
||||||
|
isUserMemberOfIdp(user.idpId, selectedIdp.idpId)
|
||||||
|
? t("idpConfirmDeleteAndRemoveMeFromOrg")
|
||||||
|
: selectedIdp.name
|
||||||
|
}
|
||||||
title={t("idpDelete")}
|
title={t("idpDelete")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -381,11 +396,25 @@ export default function IdpTable({ idps, orgId }: Props) {
|
|||||||
<p>{t("idpUnassociateDescription")}</p>
|
<p>{t("idpUnassociateDescription")}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
buttonText={t("idpUnassociateConfirm")}
|
buttonText={
|
||||||
|
isUserMemberOfIdp(
|
||||||
|
user.idpId,
|
||||||
|
selectedUnassociateIdp.idpId
|
||||||
|
)
|
||||||
|
? t("idpUnassociateAndRemoveMeFromOrg")
|
||||||
|
: t("idpUnassociateConfirm")
|
||||||
|
}
|
||||||
onConfirm={async () =>
|
onConfirm={async () =>
|
||||||
unassociateIdp(selectedUnassociateIdp.idpId)
|
unassociateIdp(selectedUnassociateIdp.idpId)
|
||||||
}
|
}
|
||||||
string={selectedUnassociateIdp.name}
|
string={
|
||||||
|
isUserMemberOfIdp(
|
||||||
|
user.idpId,
|
||||||
|
selectedUnassociateIdp.idpId
|
||||||
|
)
|
||||||
|
? t("idpUnassociateAndRemoveMeFromOrg")
|
||||||
|
: selectedUnassociateIdp.name
|
||||||
|
}
|
||||||
title={t("idpUnassociateTitle")}
|
title={t("idpUnassociateTitle")}
|
||||||
warningText={t("idpUnassociateWarning")}
|
warningText={t("idpUnassociateWarning")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user