show warning when changing idp identifier mapping

This commit is contained in:
miloschwartz
2026-09-01 11:38:53 -04:00
parent f1711ee0b0
commit b4f6ae74d7
4 changed files with 175 additions and 17 deletions
@@ -46,6 +46,7 @@ import { AxiosResponse } from "axios";
import { ListRolesResponse } from "@server/routers/role";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import {
@@ -75,6 +76,12 @@ export default function GeneralPage() {
>([createMappingBuilderRule()]);
const [rawRoleExpression, setRawRoleExpression] = useState("");
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
const [originalIdentifierPath, setOriginalIdentifierPath] = useState("");
const [identifierConfirmOpen, setIdentifierConfirmOpen] = useState(false);
const [pendingPayload, setPendingPayload] = useState<Record<
string,
unknown
> | null>(null);
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
const [redirectUrl, setRedirectUrl] = useState(
@@ -184,6 +191,9 @@ export default function GeneralPage() {
const data = res.data.data;
const roleMapping = data.idpOrg.roleMapping;
const idpVariant = data.idpOidcConfig?.variant || "oidc";
setOriginalIdentifierPath(
data.idpOidcConfig?.identifierPath ?? "sub"
);
setRedirectUrl(res.data.data.redirectUrl);
// Set the variant
@@ -378,18 +388,56 @@ export default function GeneralPage() {
};
}
const res = await api.post(
`/org/${orgId}/idp/${idpId}/oidc`,
payload
);
const nextIdentifierPath =
variant === "oidc"
? (data as OidcFormValues).identifierPath
: undefined;
if (res.status === 200) {
toast({
title: t("success"),
description: t("idpUpdatedDescription")
});
router.refresh();
if (
typeof nextIdentifierPath === "string" &&
nextIdentifierPath !== originalIdentifierPath
) {
setPendingPayload(payload);
setIdentifierConfirmOpen(true);
return;
}
await persistIdp(payload);
} catch (e) {
toast({
title: t("error"),
description: formatAxiosError(e),
variant: "destructive"
});
} finally {
setLoading(false);
}
}
async function persistIdp(payload: Record<string, unknown>) {
const res = await api.post(`/org/${orgId}/idp/${idpId}/oidc`, payload);
if (res.status === 200) {
if (typeof payload.identifierPath === "string") {
setOriginalIdentifierPath(payload.identifierPath);
}
toast({
title: t("success"),
description: t("idpUpdatedDescription")
});
router.refresh();
}
}
async function confirmIdentifierChange() {
if (!pendingPayload) {
return;
}
setLoading(true);
try {
await persistIdp(pendingPayload);
setPendingPayload(null);
} catch (e) {
toast({
title: t("error"),
@@ -407,6 +455,16 @@ export default function GeneralPage() {
return (
<>
<IdpIdentifierChangeDialog
open={identifierConfirmOpen}
setOpen={(open) => {
setIdentifierConfirmOpen(open);
if (!open) {
setPendingPayload(null);
}
}}
onConfirm={confirmIdentifierChange}
/>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
+68 -7
View File
@@ -41,6 +41,7 @@ import {
} from "@app/components/InfoSection";
import CopyToClipboard from "@app/components/CopyToClipboard";
import IdpTypeBadge from "@app/components/IdpTypeBadge";
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
import { useTranslations } from "next-intl";
export default function GeneralPage() {
@@ -51,6 +52,12 @@ export default function GeneralPage() {
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
const [originalIdentifierPath, setOriginalIdentifierPath] = useState("");
const [identifierConfirmOpen, setIdentifierConfirmOpen] = useState(false);
const [pendingPayload, setPendingPayload] = useState<Record<
string,
unknown
> | null>(null);
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
const t = useTranslations();
@@ -141,6 +148,9 @@ export default function GeneralPage() {
| "google"
| "azure") || "oidc";
setVariant(idpVariant);
setOriginalIdentifierPath(
data.idpOidcConfig?.identifierPath ?? "sub"
);
let tenantId = "";
if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
@@ -258,15 +268,56 @@ export default function GeneralPage() {
};
}
const res = await api.post(`/idp/${idpId}/oidc`, payload);
const nextIdentifierPath =
variant === "oidc"
? (data as OidcFormValues).identifierPath
: undefined;
if (res.status === 200) {
toast({
title: t("success"),
description: t("idpUpdatedDescription")
});
router.refresh();
if (
typeof nextIdentifierPath === "string" &&
nextIdentifierPath !== originalIdentifierPath
) {
setPendingPayload(payload);
setIdentifierConfirmOpen(true);
return;
}
await persistIdp(payload);
} catch (e) {
toast({
title: t("error"),
description: formatAxiosError(e),
variant: "destructive"
});
} finally {
setLoading(false);
}
}
async function persistIdp(payload: Record<string, unknown>) {
const res = await api.post(`/idp/${idpId}/oidc`, payload);
if (res.status === 200) {
if (typeof payload.identifierPath === "string") {
setOriginalIdentifierPath(payload.identifierPath);
}
toast({
title: t("success"),
description: t("idpUpdatedDescription")
});
router.refresh();
}
}
async function confirmIdentifierChange() {
if (!pendingPayload) {
return;
}
setLoading(true);
try {
await persistIdp(pendingPayload);
setPendingPayload(null);
} catch (e) {
toast({
title: t("error"),
@@ -284,6 +335,16 @@ export default function GeneralPage() {
return (
<>
<IdpIdentifierChangeDialog
open={identifierConfirmOpen}
setOpen={(open) => {
setIdentifierConfirmOpen(open);
if (!open) {
setPendingPayload(null);
}
}}
onConfirm={confirmIdentifierChange}
/>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
@@ -0,0 +1,35 @@
"use client";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useTranslations } from "next-intl";
type IdpIdentifierChangeDialogProps = {
open: boolean;
setOpen: (open: boolean) => void;
onConfirm: () => Promise<void>;
};
export default function IdpIdentifierChangeDialog({
open,
setOpen,
onConfirm
}: IdpIdentifierChangeDialogProps) {
const t = useTranslations();
return (
<ConfirmDeleteDialog
open={open}
setOpen={setOpen}
dialog={
<div className="space-y-2">
<p>{t("idpIdentifierChangeDescription")}</p>
</div>
}
buttonText={t("saveGeneralSettings")}
onConfirm={onConfirm}
string={t("idpIdentifierChangeConfirmMessage")}
title={t("idpIdentifierChangeTitle")}
warningText={t("idpIdentifierChangeWarningText")}
/>
);
}