Files
calendar/apps/web/modules/settings/security/two-factor-auth-view.tsx
Anik Dhabal BabuGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>bot_apk
f86767c125 fix: correct admin password banner message and auto-sign-out after 2FA enable (#28129)
* fix: correct admin password banner message to require both password length and 2FA

The banner message incorrectly used 'or' implying only one condition was needed,
but the code requires BOTH a password of at least 15 characters AND 2FA enabled.

Updated the message to clearly state both requirements and added a hint that
users need to log out and log back in after updating their security settings.

Fixes #9527

Co-Authored-By: unknown <>

* fix: auto-sign-out INACTIVE_ADMIN users after enabling 2FA

When an INACTIVE_ADMIN user enables 2FA, automatically sign them out so
they can log back in with refreshed session role, dismissing the banner.
This matches the existing behavior for password changes.

Co-Authored-By: unknown <>

* feat: add dynamic admin banner message based on inactiveAdminReason

Co-Authored-By: unknown <>

* Remove and add various localization strings

* Update common.json

* Add cookie consent checkbox message and remove entries

* test: add unit tests for AdminPasswordBanner and inactiveAdminReason logic

Co-Authored-By: unknown <>

* fix: add expires field to session mock to fix type check

Co-Authored-By: unknown <>

* fix: wrap CALENDSO_ENCRYPTION_KEY mutations in try/finally to prevent env state leaks

Addresses Cubic AI review feedback (confidence 9/10): when a test fails
early, the CALENDSO_ENCRYPTION_KEY env var was not being restored, which
could leak state into subsequent tests. Wrapped the env mutation in
try/finally blocks to guarantee cleanup.

Co-Authored-By: bot_apk <apk@cognition.ai>

* fix: add missing 'expires' field to buildSession in AdminPasswordBanner test

The Session type requires 'expires' to be a string, but buildSession was
not providing it, causing a type error caught by CI type-check.

Co-Authored-By: bot_apk <apk@cognition.ai>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: bot_apk <apk@cognition.ai>
2026-03-10 12:46:31 -03:00

96 lines
3.3 KiB
TypeScript

"use client";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/components/alert";
import { Badge } from "@calcom/ui/components/badge";
import { SettingsToggle } from "@calcom/ui/components/form";
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
import { signOut, useSession } from "next-auth/react";
import { useState } from "react";
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<div className="mb-8 mt-6 stack-y-6">
<div className="flex items-center">
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
<SkeletonText className="h-8 w-full" />
</div>
</div>
</SkeletonContainer>
);
};
const TwoFactorAuthView = () => {
const utils = trpc.useUtils();
const { data: sessionData } = useSession();
const { t } = useLocale();
const { data: user, isPending } = trpc.viewer.me.get.useQuery({ includePasswordAdded: true });
const [enableModalOpen, setEnableModalOpen] = useState<boolean>(false);
const [disableModalOpen, setDisableModalOpen] = useState<boolean>(false);
if (isPending) return <SkeletonLoader />;
const isCalProvider = user?.identityProvider === "CAL";
const canSetupTwoFactor = !isCalProvider && !user?.twoFactorEnabled && !user?.passwordAdded;
return (
<>
{canSetupTwoFactor && <Alert severity="neutral" message={t("2fa_disabled")} />}
<SettingsToggle
toggleSwitchAtTheEnd={true}
data-testid="two-factor-switch"
title={t("two_factor_auth")}
description={t("add_an_extra_layer_of_security")}
checked={user?.twoFactorEnabled ?? false}
onCheckedChange={() =>
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
}
Badge={
<Badge className="mx-2 text-xs" variant={user?.twoFactorEnabled ? "success" : "gray"}>
{user?.twoFactorEnabled ? t("enabled") : t("disabled")}
</Badge>
}
switchContainerClassName="rounded-t-none border-t-0"
/>
<EnableTwoFactorModal
open={enableModalOpen}
onOpenChange={() => setEnableModalOpen(!enableModalOpen)}
onEnable={() => {
setEnableModalOpen(false);
if (sessionData?.user.role === "INACTIVE_ADMIN") {
// Session role is set at login time, so we need to sign out
// and sign back in to refresh the role and dismiss the banner
signOut({ callbackUrl: "/auth/login" });
} else {
utils.viewer.me.invalidate();
}
}}
onCancel={() => {
setEnableModalOpen(false);
}}
/>
<DisableTwoFactorModal
open={disableModalOpen}
disablePassword={!isCalProvider}
onOpenChange={() => setDisableModalOpen(!disableModalOpen)}
onDisable={() => {
setDisableModalOpen(false);
utils.viewer.me.invalidate();
}}
onCancel={() => {
setDisableModalOpen(false);
}}
/>
</>
);
};
export default TwoFactorAuthView;