mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-06 05:40:02 -06:00
Compare commits
20 Commits
release/v3
...
feat-reset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c72df4d9 | ||
|
|
49560ccba8 | ||
|
|
3f98283d4d | ||
|
|
7b64422a3f | ||
|
|
a7ee1f189f | ||
|
|
46a590311b | ||
|
|
0faeffb624 | ||
|
|
d9727a336a | ||
|
|
330e0db668 | ||
|
|
f5b7f73199 | ||
|
|
c02f070307 | ||
|
|
bc489e050a | ||
|
|
3062059ed5 | ||
|
|
f27ede6b2c | ||
|
|
e460ff5100 | ||
|
|
4699c0014b | ||
|
|
52f69be05d | ||
|
|
619c0983a4 | ||
|
|
964fb8d4f4 | ||
|
|
5391c60bba |
@@ -1,3 +1,4 @@
|
|||||||
|
import { forgotPasswordAction } from "@/modules/auth/forgot-password/actions";
|
||||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -24,6 +25,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(() => {
|
||||||
@@ -37,6 +40,10 @@ vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/act
|
|||||||
updateUserAction: vi.fn(),
|
updateUserAction: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/auth/forgot-password/actions", () => ({
|
||||||
|
forgotPasswordAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
});
|
});
|
||||||
@@ -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,63 @@ 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(forgotPasswordAction).mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
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(forgotPasswordAction).toHaveBeenCalledWith({ email: mockUser.email });
|
||||||
|
});
|
||||||
|
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(forgotPasswordAction).mockRejectedValue(new Error(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(forgotPasswordAction).toHaveBeenCalledWith({ email: mockUser.email });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reset password button shows loading state", async () => {
|
||||||
|
vi.mocked(forgotPasswordAction).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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { PasswordConfirmationModal } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/password-confirmation-modal";
|
import { PasswordConfirmationModal } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/password-confirmation-modal";
|
||||||
import { appLanguages } from "@/lib/i18n/utils";
|
import { appLanguages } from "@/lib/i18n/utils";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
|
import { forgotPasswordAction } from "@/modules/auth/forgot-password/actions";
|
||||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||||
import { Button } from "@/modules/ui/components/button";
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +15,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";
|
||||||
@@ -30,13 +32,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 +56,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 });
|
||||||
|
|
||||||
@@ -121,6 +129,24 @@ export const EditProfileDetailsForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
if (!user.email) return;
|
||||||
|
|
||||||
|
setIsResettingPassword(true);
|
||||||
|
|
||||||
|
await forgotPasswordAction({ email: user.email });
|
||||||
|
|
||||||
|
toast.success(t("auth.forgot-password.email-sent.heading"));
|
||||||
|
await signOutWithAudit({
|
||||||
|
reason: "password_reset",
|
||||||
|
redirectUrl: "/auth/login",
|
||||||
|
redirect: true,
|
||||||
|
callbackUrl: "/auth/login",
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsResettingPassword(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
@@ -205,6 +231,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"
|
||||||
|
|||||||
@@ -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": "建立帳戶",
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
const user = await getUserByEmail(parsedInput.email);
|
if (PASSWORD_RESET_DISABLED) {
|
||||||
if (user) {
|
throw new OperationNotAllowedError("Password reset is disabled");
|
||||||
await sendForgotPasswordEmail(user);
|
|
||||||
}
|
}
|
||||||
return { success: true };
|
|
||||||
|
const user = await getUserByEmail(parsedInput.email);
|
||||||
|
|
||||||
|
if (!user || user.identityProvider !== "email") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendForgotPasswordEmail(user);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ 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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user