mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-22 19:39:01 -05:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0878428c79 | |||
| b655e649ac | |||
| ddb95b7cbb | |||
| eced597e8a | |||
| 93c72df4d9 | |||
| 49560ccba8 | |||
| 3f98283d4d | |||
| 7b64422a3f | |||
| a7ee1f189f | |||
| 46a590311b | |||
| 0faeffb624 | |||
| d9727a336a | |||
| 330e0db668 | |||
| f5b7f73199 | |||
| c02f070307 | |||
| bc489e050a | |||
| 3062059ed5 | |||
| f27ede6b2c | |||
| e460ff5100 | |||
| 4699c0014b | |||
| 52f69be05d | |||
| 619c0983a4 | |||
| 964fb8d4f4 | |||
| 5391c60bba |
+1
@@ -94,6 +94,7 @@ describe("LandingSidebar component", () => {
|
|||||||
organizationId: "o1",
|
organizationId: "o1",
|
||||||
redirect: true,
|
redirect: true,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
@@ -130,6 +130,7 @@ export const LandingSidebar = ({
|
|||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
redirect: true,
|
redirect: true,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||||
|
|||||||
@@ -221,7 +221,6 @@ describe("MainNavigation", () => {
|
|||||||
vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut });
|
vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut });
|
||||||
|
|
||||||
// Set up localStorage spy on the mocked localStorage
|
// Set up localStorage spy on the mocked localStorage
|
||||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
|
||||||
|
|
||||||
render(<MainNavigation {...defaultProps} />);
|
render(<MainNavigation {...defaultProps} />);
|
||||||
|
|
||||||
@@ -243,23 +242,18 @@ describe("MainNavigation", () => {
|
|||||||
const logoutButton = screen.getByText("common.logout");
|
const logoutButton = screen.getByText("common.logout");
|
||||||
await userEvent.click(logoutButton);
|
await userEvent.click(logoutButton);
|
||||||
|
|
||||||
// Verify localStorage.removeItem is called with the correct key
|
|
||||||
expect(removeItemSpy).toHaveBeenCalledWith("formbricks-environment-id");
|
|
||||||
|
|
||||||
expect(mockSignOut).toHaveBeenCalledWith({
|
expect(mockSignOut).toHaveBeenCalledWith({
|
||||||
reason: "user_initiated",
|
reason: "user_initiated",
|
||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
organizationId: "org1",
|
organizationId: "org1",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockRouterPush).toHaveBeenCalledWith("/auth/login");
|
expect(mockRouterPush).toHaveBeenCalledWith("/auth/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up spy
|
|
||||||
removeItemSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles organization switching", async () => {
|
test("handles organization switching", async () => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
|
|||||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||||
@@ -391,14 +390,13 @@ export const MainNavigation = ({
|
|||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
|
|
||||||
const route = await signOutWithAudit({
|
const route = await signOutWithAudit({
|
||||||
reason: "user_initiated",
|
reason: "user_initiated",
|
||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
router.push(route?.url || "/auth/login"); // NOSONAR // We want to check for empty strings
|
router.push(route?.url || "/auth/login"); // NOSONAR // We want to check for empty strings
|
||||||
}}
|
}}
|
||||||
|
|||||||
+19
-1
@@ -13,7 +13,7 @@ import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/co
|
|||||||
import { rateLimit } from "@/lib/utils/rate-limit";
|
import { rateLimit } from "@/lib/utils/rate-limit";
|
||||||
import { updateBrevoCustomer } from "@/modules/auth/lib/brevo";
|
import { updateBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||||
import { sendVerificationNewEmail } from "@/modules/email";
|
import { sendForgotPasswordEmail, sendVerificationNewEmail } from "@/modules/email";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import {
|
import {
|
||||||
@@ -162,3 +162,21 @@ export const removeAvatarAction = authenticatedActionClient.schema(ZRemoveAvatar
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const resetPasswordAction = authenticatedActionClient.action(
|
||||||
|
withAuditLogging(
|
||||||
|
"passwordReset",
|
||||||
|
"user",
|
||||||
|
async ({ ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: undefined }) => {
|
||||||
|
if (ctx.user.identityProvider !== "email") {
|
||||||
|
throw new OperationNotAllowedError("auth.reset-password.not-allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendForgotPasswordEmail(ctx.user);
|
||||||
|
|
||||||
|
ctx.auditLoggingCtx.userId = ctx.user.id;
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
+93
-4
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { updateUserAction } from "../actions";
|
import { resetPasswordAction, updateUserAction } from "../actions";
|
||||||
import { EditProfileDetailsForm } from "./EditProfileDetailsForm";
|
import { EditProfileDetailsForm } from "./EditProfileDetailsForm";
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
@@ -24,6 +24,8 @@ const mockUser = {
|
|||||||
objective: "other",
|
objective: "other",
|
||||||
} as unknown as TUser;
|
} as unknown as TUser;
|
||||||
|
|
||||||
|
vi.mock("next-auth/react", () => ({ signOut: vi.fn() }));
|
||||||
|
|
||||||
// Mock window.location.reload
|
// Mock window.location.reload
|
||||||
const originalLocation = window.location;
|
const originalLocation = window.location;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -35,6 +37,11 @@ beforeEach(() => {
|
|||||||
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
||||||
updateUserAction: vi.fn(),
|
updateUserAction: vi.fn(),
|
||||||
|
resetPasswordAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/auth/forgot-password/actions", () => ({
|
||||||
|
forgotPasswordAction: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -50,7 +57,13 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
test("renders with initial user data and updates successfully", async () => {
|
test("renders with initial user data and updates successfully", async () => {
|
||||||
vi.mocked(updateUserAction).mockResolvedValue({ ...mockUser, name: "New Name" } as any);
|
vi.mocked(updateUserAction).mockResolvedValue({ ...mockUser, name: "New Name" } as any);
|
||||||
|
|
||||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={true} />);
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={true}
|
||||||
|
isPasswordResetEnabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||||
expect(nameInput).toHaveValue(mockUser.name);
|
expect(nameInput).toHaveValue(mockUser.name);
|
||||||
@@ -91,7 +104,13 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
const errorMessage = "Update failed";
|
const errorMessage = "Update failed";
|
||||||
vi.mocked(updateUserAction).mockRejectedValue(new Error(errorMessage));
|
vi.mocked(updateUserAction).mockRejectedValue(new Error(errorMessage));
|
||||||
|
|
||||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={false} />);
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||||
await userEvent.clear(nameInput);
|
await userEvent.clear(nameInput);
|
||||||
@@ -109,7 +128,13 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("update button is disabled initially and enables on change", async () => {
|
test("update button is disabled initially and enables on change", async () => {
|
||||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={false} />);
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
const updateButton = screen.getByText("common.update");
|
const updateButton = screen.getByText("common.update");
|
||||||
expect(updateButton).toBeDisabled();
|
expect(updateButton).toBeDisabled();
|
||||||
|
|
||||||
@@ -117,4 +142,68 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
await userEvent.type(nameInput, " updated");
|
await userEvent.type(nameInput, " updated");
|
||||||
expect(updateButton).toBeEnabled();
|
expect(updateButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("reset password button works", async () => {
|
||||||
|
vi.mocked(resetPasswordAction).mockResolvedValue({ data: { success: true } });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||||
|
await userEvent.click(resetButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(resetPasswordAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toast.success).toHaveBeenCalledWith("auth.forgot-password.email-sent.heading");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reset password button handles error correctly", async () => {
|
||||||
|
const errorMessage = "Reset failed";
|
||||||
|
vi.mocked(resetPasswordAction).mockResolvedValue({ serverError: errorMessage });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||||
|
await userEvent.click(resetButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(resetPasswordAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reset password button shows loading state", async () => {
|
||||||
|
vi.mocked(resetPasswordAction).mockImplementation(() => new Promise(() => {})); // Never resolves
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||||
|
await userEvent.click(resetButton);
|
||||||
|
|
||||||
|
expect(resetButton).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+55
-5
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/modules/ui/components/dropdown-menu";
|
} from "@/modules/ui/components/dropdown-menu";
|
||||||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
@@ -22,7 +23,7 @@ import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TUser, TUserUpdateInput, ZUser, ZUserEmail } from "@formbricks/types/user";
|
import { TUser, TUserUpdateInput, ZUser, ZUserEmail } from "@formbricks/types/user";
|
||||||
import { updateUserAction } from "../actions";
|
import { resetPasswordAction, updateUserAction } from "../actions";
|
||||||
|
|
||||||
// Schema & types
|
// Schema & types
|
||||||
const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email: true }).extend({
|
const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email: true }).extend({
|
||||||
@@ -30,13 +31,17 @@ const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email:
|
|||||||
});
|
});
|
||||||
type TEditProfileNameForm = z.infer<typeof ZEditProfileNameFormSchema>;
|
type TEditProfileNameForm = z.infer<typeof ZEditProfileNameFormSchema>;
|
||||||
|
|
||||||
|
interface IEditProfileDetailsFormProps {
|
||||||
|
user: TUser;
|
||||||
|
isPasswordResetEnabled?: boolean;
|
||||||
|
emailVerificationDisabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const EditProfileDetailsForm = ({
|
export const EditProfileDetailsForm = ({
|
||||||
user,
|
user,
|
||||||
|
isPasswordResetEnabled,
|
||||||
emailVerificationDisabled,
|
emailVerificationDisabled,
|
||||||
}: {
|
}: IEditProfileDetailsFormProps) => {
|
||||||
user: TUser;
|
|
||||||
emailVerificationDisabled: boolean;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
const form = useForm<TEditProfileNameForm>({
|
const form = useForm<TEditProfileNameForm>({
|
||||||
@@ -50,6 +55,8 @@ export const EditProfileDetailsForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { isSubmitting, isDirty } = form.formState;
|
const { isSubmitting, isDirty } = form.formState;
|
||||||
|
|
||||||
|
const [isResettingPassword, setIsResettingPassword] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
||||||
|
|
||||||
@@ -90,6 +97,7 @@ export const EditProfileDetailsForm = ({
|
|||||||
redirectUrl: "/email-change-without-verification-success",
|
redirectUrl: "/email-change-without-verification-success",
|
||||||
redirect: true,
|
redirect: true,
|
||||||
callbackUrl: "/email-change-without-verification-success",
|
callbackUrl: "/email-change-without-verification-success",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,6 +129,28 @@ export const EditProfileDetailsForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
setIsResettingPassword(true);
|
||||||
|
|
||||||
|
const result = await resetPasswordAction();
|
||||||
|
if (result?.data) {
|
||||||
|
toast.success(t("auth.forgot-password.email-sent.heading"));
|
||||||
|
|
||||||
|
await signOutWithAudit({
|
||||||
|
reason: "password_reset",
|
||||||
|
redirectUrl: "/auth/login",
|
||||||
|
redirect: true,
|
||||||
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const errorMessage = getFormattedErrorMessage(result);
|
||||||
|
toast.error(t(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsResettingPassword(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
@@ -205,6 +235,26 @@ export const EditProfileDetailsForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isPasswordResetEnabled && (
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<Label htmlFor="reset-password">{t("auth.forgot-password.reset_password")}</Label>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
|
{t("auth.forgot-password.reset_password_description")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Input type="email" id="reset-password" defaultValue={user.email} disabled />
|
||||||
|
<Button
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
loading={isResettingPassword}
|
||||||
|
disabled={isResettingPassword}
|
||||||
|
size="default"
|
||||||
|
variant="secondary">
|
||||||
|
{t("auth.forgot-password.reset_password")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
|
|||||||
+2
-1
@@ -12,7 +12,8 @@ import Page from "./page";
|
|||||||
|
|
||||||
// Mock services and utils
|
// Mock services and utils
|
||||||
vi.mock("@/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: true,
|
IS_FORMBRICKS_CLOUD: 1,
|
||||||
|
PASSWORD_RESET_DISABLED: 1,
|
||||||
EMAIL_VERIFICATION_DISABLED: true,
|
EMAIL_VERIFICATION_DISABLED: true,
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/organization/service", () => ({
|
vi.mock("@/lib/organization/service", () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
||||||
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||||
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||||
import { getUser } from "@/lib/user/service";
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
@@ -32,6 +32,8 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
throw new Error(t("common.user_not_found"));
|
throw new Error(t("common.user_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPasswordResetEnabled = !PASSWORD_RESET_DISABLED && user.identityProvider === "email";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<PageHeader pageTitle={t("common.account_settings")}>
|
<PageHeader pageTitle={t("common.account_settings")}>
|
||||||
@@ -42,7 +44,11 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
<SettingsCard
|
<SettingsCard
|
||||||
title={t("environments.settings.profile.personal_information")}
|
title={t("environments.settings.profile.personal_information")}
|
||||||
description={t("environments.settings.profile.update_personal_info")}>
|
description={t("environments.settings.profile.update_personal_info")}>
|
||||||
<EditProfileDetailsForm emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED} user={user} />
|
<EditProfileDetailsForm
|
||||||
|
user={user}
|
||||||
|
emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED}
|
||||||
|
isPasswordResetEnabled={isPasswordResetEnabled}
|
||||||
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title={t("common.avatar")}
|
title={t("common.avatar")}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Du kannst Dich jetzt mit deinem neuen Passwort einloggen"
|
"text": "Du kannst Dich jetzt mit deinem neuen Passwort einloggen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Passwort zurücksetzen"
|
"reset_password": "Passwort zurücksetzen",
|
||||||
|
"reset_password_description": "Du wirst abgemeldet, um dein Passwort zurückzusetzen."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Konto erstellen",
|
"create_account": "Konto erstellen",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "You can now log in with your new password"
|
"text": "You can now log in with your new password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Reset password"
|
"reset_password": "Reset password",
|
||||||
|
"reset_password_description": "You will be logged out to reset your password."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Create an account",
|
"create_account": "Create an account",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
|
"text": "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Réinitialiser le mot de passe"
|
"reset_password": "Réinitialiser le mot de passe",
|
||||||
|
"reset_password_description": "Vous serez déconnecté pour réinitialiser votre mot de passe."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Créer un compte",
|
"create_account": "Créer un compte",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Agora você pode fazer login com sua nova senha"
|
"text": "Agora você pode fazer login com sua nova senha"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Redefinir senha"
|
"reset_password": "Redefinir senha",
|
||||||
|
"reset_password_description": "Você será desconectado para redefinir sua senha."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Cria uma conta",
|
"create_account": "Cria uma conta",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Pode agora iniciar sessão com a sua nova palavra-passe"
|
"text": "Pode agora iniciar sessão com a sua nova palavra-passe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Redefinir palavra-passe"
|
"reset_password": "Redefinir palavra-passe",
|
||||||
|
"reset_password_description": "Será desconectado para redefinir a sua palavra-passe."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Criar uma conta",
|
"create_account": "Criar uma conta",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "您現在可以使用新密碼登入"
|
"text": "您現在可以使用新密碼登入"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "重設密碼"
|
"reset_password": "重設密碼",
|
||||||
|
"reset_password_description": "您將被登出以重設您的密碼。"
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "建立帳戶",
|
"create_account": "建立帳戶",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
@@ -100,8 +99,6 @@ describe("DeleteAccountModal", () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
|
||||||
|
|
||||||
const input = screen.getByTestId("deleteAccountConfirmation");
|
const input = screen.getByTestId("deleteAccountConfirmation");
|
||||||
fireEvent.change(input, { target: { value: mockUser.email } });
|
fireEvent.change(input, { target: { value: mockUser.email } });
|
||||||
|
|
||||||
@@ -113,8 +110,8 @@ describe("DeleteAccountModal", () => {
|
|||||||
expect(mockSignOut).toHaveBeenCalledWith({
|
expect(mockSignOut).toHaveBeenCalledWith({
|
||||||
reason: "account_deletion",
|
reason: "account_deletion",
|
||||||
redirect: false, // Updated to match new implementation
|
redirect: false, // Updated to match new implementation
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
expect(removeItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
expect(window.location.replace).toHaveBeenCalledWith("/auth/login");
|
expect(window.location.replace).toHaveBeenCalledWith("/auth/login");
|
||||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
@@ -151,15 +148,13 @@ describe("DeleteAccountModal", () => {
|
|||||||
const form = screen.getByTestId("deleteAccountForm");
|
const form = screen.getByTestId("deleteAccountForm");
|
||||||
fireEvent.submit(form);
|
fireEvent.submit(form);
|
||||||
|
|
||||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(deleteUserAction).toHaveBeenCalled();
|
expect(deleteUserAction).toHaveBeenCalled();
|
||||||
expect(mockSignOut).toHaveBeenCalledWith({
|
expect(mockSignOut).toHaveBeenCalledWith({
|
||||||
reason: "account_deletion",
|
reason: "account_deletion",
|
||||||
redirect: false, // Updated to match new implementation
|
redirect: false, // Updated to match new implementation
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
expect(removeItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
expect(window.location.replace).toHaveBeenCalledWith(
|
expect(window.location.replace).toHaveBeenCalledWith(
|
||||||
"https://app.formbricks.com/s/clri52y3z8f221225wjdhsoo2"
|
"https://app.formbricks.com/s/clri52y3z8f221225wjdhsoo2"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
@@ -39,12 +38,11 @@ export const DeleteAccountModal = ({
|
|||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
await deleteUserAction();
|
await deleteUserAction();
|
||||||
|
|
||||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
|
|
||||||
// Sign out with account deletion reason (no automatic redirect)
|
// Sign out with account deletion reason (no automatic redirect)
|
||||||
await signOutWithAudit({
|
await signOutWithAudit({
|
||||||
reason: "account_deletion",
|
reason: "account_deletion",
|
||||||
redirect: false, // Prevent NextAuth automatic redirect
|
redirect: false, // Prevent NextAuth automatic redirect
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manual redirect after signOut completes
|
// Manual redirect after signOut completes
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ export const logSignOutAction = async (
|
|||||||
userId: string,
|
userId: string,
|
||||||
userEmail: string,
|
userEmail: string,
|
||||||
context: {
|
context: {
|
||||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
reason?:
|
||||||
|
| "user_initiated"
|
||||||
|
| "account_deletion"
|
||||||
|
| "email_change"
|
||||||
|
| "session_timeout"
|
||||||
|
| "forced_logout"
|
||||||
|
| "password_reset";
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||||
import { actionClient } from "@/lib/utils/action-client";
|
import { actionClient } from "@/lib/utils/action-client";
|
||||||
import { getUserByEmail } from "@/modules/auth/lib/user";
|
import { getUserByEmail } from "@/modules/auth/lib/user";
|
||||||
import { sendForgotPasswordEmail } from "@/modules/email";
|
import { sendForgotPasswordEmail } from "@/modules/email";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
import { ZUserEmail } from "@formbricks/types/user";
|
import { ZUserEmail } from "@formbricks/types/user";
|
||||||
|
|
||||||
const ZForgotPasswordAction = z.object({
|
const ZForgotPasswordAction = z.object({
|
||||||
@@ -13,9 +15,15 @@ const ZForgotPasswordAction = z.object({
|
|||||||
export const forgotPasswordAction = actionClient
|
export const forgotPasswordAction = actionClient
|
||||||
.schema(ZForgotPasswordAction)
|
.schema(ZForgotPasswordAction)
|
||||||
.action(async ({ parsedInput }) => {
|
.action(async ({ parsedInput }) => {
|
||||||
|
if (PASSWORD_RESET_DISABLED) {
|
||||||
|
throw new OperationNotAllowedError("Password reset is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await getUserByEmail(parsedInput.email);
|
const user = await getUserByEmail(parsedInput.email);
|
||||||
if (user) {
|
|
||||||
|
if (user && user.identityProvider === "email") {
|
||||||
await sendForgotPasswordEmail(user);
|
await sendForgotPasswordEmail(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||||
import { logSignOutAction } from "@/modules/auth/actions/sign-out";
|
import { logSignOutAction } from "@/modules/auth/actions/sign-out";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
|
|
||||||
interface UseSignOutOptions {
|
interface UseSignOutOptions {
|
||||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
reason?:
|
||||||
|
| "user_initiated"
|
||||||
|
| "account_deletion"
|
||||||
|
| "email_change"
|
||||||
|
| "session_timeout"
|
||||||
|
| "forced_logout"
|
||||||
|
| "password_reset";
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
redirect?: boolean;
|
redirect?: boolean;
|
||||||
callbackUrl?: string;
|
callbackUrl?: string;
|
||||||
|
clearEnvironmentId?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionUser {
|
interface SessionUser {
|
||||||
@@ -36,6 +44,10 @@ export const useSignOut = (sessionUser?: SessionUser | null) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.clearEnvironmentId) {
|
||||||
|
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||||
|
}
|
||||||
|
|
||||||
// Call NextAuth signOut
|
// Call NextAuth signOut
|
||||||
return await signOut({
|
return await signOut({
|
||||||
redirect: options?.redirect,
|
redirect: options?.redirect,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const getUserByEmail = reactCache(async (email: string) => {
|
|||||||
email: true,
|
email: true,
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
identityProvider: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,13 @@ export const logSignOut = (
|
|||||||
userId: string,
|
userId: string,
|
||||||
userEmail: string,
|
userEmail: string,
|
||||||
context?: {
|
context?: {
|
||||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
reason?:
|
||||||
|
| "user_initiated"
|
||||||
|
| "account_deletion"
|
||||||
|
| "email_change"
|
||||||
|
| "session_timeout"
|
||||||
|
| "forced_logout"
|
||||||
|
| "password_reset";
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const ZAuditAction = z.enum([
|
|||||||
"twoFactorRequired",
|
"twoFactorRequired",
|
||||||
"emailVerificationAttempted",
|
"emailVerificationAttempted",
|
||||||
"userSignedOut",
|
"userSignedOut",
|
||||||
|
"passwordReset",
|
||||||
]);
|
]);
|
||||||
export const ZActor = z.enum(["user", "api", "system"]);
|
export const ZActor = z.enum(["user", "api", "system"]);
|
||||||
export const ZAuditStatus = z.enum(["success", "failure"]);
|
export const ZAuditStatus = z.enum(["success", "failure"]);
|
||||||
|
|||||||
@@ -60,11 +60,7 @@ const getRatingContent = (scale: string, i: number, range: number, isColorCoding
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (scale === "number") {
|
if (scale === "number") {
|
||||||
return (
|
return <Text className="m-0 h-[44px] text-center text-[14px] leading-[44px]">{i + 1}</Text>;
|
||||||
<Text className="m-0 h-[44px] text-center text-[14px] leading-[44px]">
|
|
||||||
{i + 1}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (scale === "star") {
|
if (scale === "star") {
|
||||||
return <Text className="m-auto text-3xl">⭐</Text>;
|
return <Text className="m-auto text-3xl">⭐</Text>;
|
||||||
@@ -232,8 +228,8 @@ export async function PreviewEmailTemplate({
|
|||||||
{ "rounded-l-lg border-l": i === 0 },
|
{ "rounded-l-lg border-l": i === 0 },
|
||||||
{ "rounded-r-lg": i === firstQuestion.range - 1 },
|
{ "rounded-r-lg": i === firstQuestion.range - 1 },
|
||||||
firstQuestion.isColorCodingEnabled &&
|
firstQuestion.isColorCodingEnabled &&
|
||||||
firstQuestion.scale === "number" &&
|
firstQuestion.scale === "number" &&
|
||||||
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
|
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
|
||||||
firstQuestion.scale === "star" && "border-transparent"
|
firstQuestion.scale === "star" && "border-transparent"
|
||||||
)}
|
)}
|
||||||
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
|
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ export const QuestionFormInput = ({
|
|||||||
(question &&
|
(question &&
|
||||||
(id.includes(".")
|
(id.includes(".")
|
||||||
? // Handle nested properties
|
? // Handle nested properties
|
||||||
(question[id.split(".")[0] as keyof TSurveyQuestion] as any)?.[id.split(".")[1]]
|
(question[id.split(".")[0] as keyof TSurveyQuestion] as any)?.[id.split(".")[1]]
|
||||||
: // Original behavior
|
: // Original behavior
|
||||||
(question[id as keyof TSurveyQuestion] as TI18nString))) ||
|
(question[id as keyof TSurveyQuestion] as TI18nString))) ||
|
||||||
createI18nString("", surveyLanguageCodes)
|
createI18nString("", surveyLanguageCodes)
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
@@ -351,8 +351,9 @@ export const QuestionFormInput = ({
|
|||||||
<div className="h-10 w-full"></div>
|
<div className="h-10 w-full"></div>
|
||||||
<div
|
<div
|
||||||
ref={highlightContainerRef}
|
ref={highlightContainerRef}
|
||||||
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${localSurvey.languages?.length > 1 ? "pr-24" : ""
|
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${
|
||||||
}`}
|
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||||
|
}`}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
key={highlightedJSX.toString()}>
|
key={highlightedJSX.toString()}>
|
||||||
{highlightedJSX}
|
{highlightedJSX}
|
||||||
@@ -379,8 +380,9 @@ export const QuestionFormInput = ({
|
|||||||
maxLength={maxLength}
|
maxLength={maxLength}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
className={`absolute top-0 text-black caret-black ${localSurvey.languages?.length > 1 ? "pr-24" : ""
|
className={`absolute top-0 text-black caret-black ${
|
||||||
} ${className}`}
|
localSurvey.languages?.length > 1 ? "pr-24" : ""
|
||||||
|
} ${className}`}
|
||||||
isInvalid={
|
isInvalid={
|
||||||
isInvalid &&
|
isInvalid &&
|
||||||
text[usedLanguageCode]?.trim() === "" &&
|
text[usedLanguageCode]?.trim() === "" &&
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const EndScreenForm = ({
|
|||||||
|
|
||||||
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
|
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
|
||||||
endingCard.type === "endScreen" &&
|
endingCard.type === "endScreen" &&
|
||||||
(!!getLocalizedValue(endingCard.buttonLabel, selectedLanguageCode) || !!endingCard.buttonLink)
|
(!!getLocalizedValue(endingCard.buttonLabel, selectedLanguageCode) || !!endingCard.buttonLink)
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<form>
|
<form>
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ import { render } from "@testing-library/react";
|
|||||||
import { type MockedFunction, beforeEach, describe, expect, test, vi } from "vitest";
|
import { type MockedFunction, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { ClientLogout } from "./index";
|
import { ClientLogout } from "./index";
|
||||||
|
|
||||||
// Mock the localStorage
|
|
||||||
const mockRemoveItem = vi.fn();
|
|
||||||
Object.defineProperty(window, "localStorage", {
|
|
||||||
value: {
|
|
||||||
removeItem: mockRemoveItem,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock next-auth/react
|
// Mock next-auth/react
|
||||||
const mockSignOut = vi.fn();
|
const mockSignOut = vi.fn();
|
||||||
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
|
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
|
||||||
@@ -37,6 +29,7 @@ describe("ClientLogout", () => {
|
|||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,14 +43,10 @@ describe("ClientLogout", () => {
|
|||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("removes environment ID from localStorage", () => {
|
|
||||||
render(<ClientLogout />);
|
|
||||||
expect(mockRemoveItem).toHaveBeenCalledWith("formbricks-environment-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders null", () => {
|
test("renders null", () => {
|
||||||
const { container } = render(<ClientLogout />);
|
const { container } = render(<ClientLogout />);
|
||||||
expect(container.firstChild).toBeNull();
|
expect(container.firstChild).toBeNull();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
@@ -8,12 +7,12 @@ export const ClientLogout = () => {
|
|||||||
const { signOut: signOutWithAudit } = useSignOut();
|
const { signOut: signOutWithAudit } = useSignOut();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
signOutWithAudit({
|
signOutWithAudit({
|
||||||
reason: "forced_logout",
|
reason: "forced_logout",
|
||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user