mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-13 11:29:31 -05:00
Compare commits
2 Commits
4.9.4-rc.2
..
4.9.5
| Author | SHA1 | Date | |
|---|---|---|---|
| bd05387d99 | |||
| 9b4be60dd9 |
+2
-4
@@ -6,11 +6,9 @@ import {
|
||||
TUserUpdateInput,
|
||||
ZUserPersonalInfoUpdateInput,
|
||||
} from "@formbricks/types/user";
|
||||
import {
|
||||
getIsEmailUnique,
|
||||
verifyUserPassword,
|
||||
} from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/lib/user";
|
||||
import { getIsEmailUnique } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/lib/user";
|
||||
import { EMAIL_VERIFICATION_DISABLED, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||
import { verifyUserPassword } from "@/lib/user/password";
|
||||
import { getUser, updateUser } from "@/lib/user/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/context";
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { verifyUserPassword } from "@/lib/user/password";
|
||||
import { verifyPassword as mockVerifyPasswordImported } from "@/modules/auth/lib/utils";
|
||||
import { getIsEmailUnique, verifyUserPassword } from "./user";
|
||||
import { getIsEmailUnique } from "./user";
|
||||
|
||||
vi.mock("@/modules/auth/lib/utils", () => ({
|
||||
verifyPassword: vi.fn(),
|
||||
|
||||
-37
@@ -1,42 +1,5 @@
|
||||
import { User } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { verifyPassword } from "@/modules/auth/lib/utils";
|
||||
|
||||
export const getUserById = reactCache(
|
||||
async (userId: string): Promise<Pick<User, "password" | "identityProvider">> => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
password: true,
|
||||
identityProvider: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new ResourceNotFoundError("user", userId);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
);
|
||||
|
||||
export const verifyUserPassword = async (userId: string, password: string): Promise<boolean> => {
|
||||
const user = await getUserById(userId);
|
||||
|
||||
if (user.identityProvider !== "email" || !user.password) {
|
||||
throw new InvalidInputError("Password is not set for this user");
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(password, user.password);
|
||||
|
||||
if (!isCorrectPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getIsEmailUnique = reactCache(async (email: string): Promise<boolean> => {
|
||||
const user = await prisma.user.findUnique({
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import "server-only";
|
||||
import { User } from "@prisma/client";
|
||||
import { cache as reactCache } from "react";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { verifyPassword } from "@/modules/auth/lib/utils";
|
||||
|
||||
const getUserAuthenticationData = reactCache(
|
||||
async (userId: string): Promise<Pick<User, "password" | "identityProvider">> => {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
password: true,
|
||||
identityProvider: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ResourceNotFoundError("user", userId);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
);
|
||||
|
||||
export const verifyUserPassword = async (userId: string, password: string): Promise<boolean> => {
|
||||
const user = await getUserAuthenticationData(userId);
|
||||
|
||||
if (user.identityProvider !== "email" || !user.password) {
|
||||
throw new InvalidInputError("Password is not set for this user");
|
||||
}
|
||||
|
||||
return await verifyPassword(password, user.password);
|
||||
};
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Zwei-Faktor-Authentifizierung mit einem höheren Plan freischalten",
|
||||
"update_personal_info": "Persönliche Daten aktualisieren",
|
||||
"warning_cannot_delete_account": "Du bist der einzige Besitzer dieser Organisation. Bitte übertrage das Eigentum zuerst an ein anderes Mitglied.",
|
||||
"warning_cannot_undo": "Das kann nicht rückgängig gemacht werden"
|
||||
"warning_cannot_undo": "Das kann nicht rückgängig gemacht werden",
|
||||
"wrong_password": "Falsches Passwort"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Füge Mitglieder zum Team hinzu und bestimme ihre Rolle.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Unlock two-factor authentication with a higher plan",
|
||||
"update_personal_info": "Update your personal information",
|
||||
"warning_cannot_delete_account": "You are the only owner of this organization. Please transfer ownership to another member first.",
|
||||
"warning_cannot_undo": "This cannot be undone"
|
||||
"warning_cannot_undo": "This cannot be undone",
|
||||
"wrong_password": "Wrong password"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Add members to the team and determine their role.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Desbloquea la autenticación de dos factores con un plan superior",
|
||||
"update_personal_info": "Actualiza tu información personal",
|
||||
"warning_cannot_delete_account": "Eres el único propietario de esta organización. Por favor, transfiere la propiedad a otro miembro primero.",
|
||||
"warning_cannot_undo": "Esto no se puede deshacer"
|
||||
"warning_cannot_undo": "Esto no se puede deshacer",
|
||||
"wrong_password": "Contraseña incorrecta"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Añade miembros al equipo y determina su rol.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Débloquez l'authentification à deux facteurs avec une offre supérieure",
|
||||
"update_personal_info": "Mettez à jour vos informations personnelles",
|
||||
"warning_cannot_delete_account": "Tu es le seul propriétaire de cette organisation. Transfère la propriété à un autre membre d'abord.",
|
||||
"warning_cannot_undo": "Cette opération est irréversible."
|
||||
"warning_cannot_undo": "Cette opération est irréversible.",
|
||||
"wrong_password": "Mot de passe incorrect"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Ajoutez des membres à l'équipe et déterminez leur rôle.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "A kétfaktoros hitelesítés feloldása egy magasabb csomaggal",
|
||||
"update_personal_info": "Személyes információk frissítése",
|
||||
"warning_cannot_delete_account": "Ön az egyetlen tulajdonosa ennek a szervezetnek. Először adja át a tulajdonjogot egy másik tagnak.",
|
||||
"warning_cannot_undo": "Ezt nem lehet visszavonni"
|
||||
"warning_cannot_undo": "Ezt nem lehet visszavonni",
|
||||
"wrong_password": "Hibás jelszó"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Tagok hozzáadása a csapathoz és a szerepük meghatározása.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "上位プランで二段階認証をアンロック",
|
||||
"update_personal_info": "個人情報を更新",
|
||||
"warning_cannot_delete_account": "あなたは、この組織の唯一のオーナーです。まず、別のメンバーにオーナーシップを譲渡してください。",
|
||||
"warning_cannot_undo": "この操作は元に戻せません"
|
||||
"warning_cannot_undo": "この操作は元に戻せません",
|
||||
"wrong_password": "パスワードが間違っています"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "チームにメンバーを追加し、役割を決定します。",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Ontgrendel tweefactorauthenticatie met een hoger abonnement",
|
||||
"update_personal_info": "Update uw persoonlijke gegevens",
|
||||
"warning_cannot_delete_account": "U bent de enige eigenaar van deze organisatie. Draag het eigendom eerst over aan een ander lid.",
|
||||
"warning_cannot_undo": "Dit kan niet ongedaan worden gemaakt"
|
||||
"warning_cannot_undo": "Dit kan niet ongedaan worden gemaakt",
|
||||
"wrong_password": "Verkeerd wachtwoord"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Voeg leden toe aan het team en bepaal hun rol.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Desbloqueia a autenticação de dois fatores com um plano melhor",
|
||||
"update_personal_info": "Atualize suas informações pessoais",
|
||||
"warning_cannot_delete_account": "Você é o único dono desta organização. Transfere a propriedade para outra pessoa primeiro.",
|
||||
"warning_cannot_undo": "Isso não pode ser desfeito"
|
||||
"warning_cannot_undo": "Isso não pode ser desfeito",
|
||||
"wrong_password": "Senha incorreta"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adicione membros à equipe e determine sua função.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Desbloqueie a autenticação de dois fatores com um plano superior",
|
||||
"update_personal_info": "Atualize as suas informações pessoais",
|
||||
"warning_cannot_delete_account": "É o único proprietário desta organização. Transfira a propriedade para outro membro primeiro.",
|
||||
"warning_cannot_undo": "Isto não pode ser desfeito"
|
||||
"warning_cannot_undo": "Isto não pode ser desfeito",
|
||||
"wrong_password": "Palavra-passe incorreta"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adicionar membros à equipa e determinar o seu papel.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Deblocați autentificarea în doi pași cu un plan superior",
|
||||
"update_personal_info": "Actualizează informațiile tale personale",
|
||||
"warning_cannot_delete_account": "Ești singurul proprietar al acestei organizații. Te rugăm să transferi proprietatea către un alt membru mai întâi.",
|
||||
"warning_cannot_undo": "Aceasta nu poate fi anulată"
|
||||
"warning_cannot_undo": "Aceasta nu poate fi anulată",
|
||||
"wrong_password": "Parolă greșită"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Adaugă membri în echipă și stabilește rolul lor.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Откройте двухфакторную аутентификацию с более высоким тарифом",
|
||||
"update_personal_info": "Обновить личную информацию",
|
||||
"warning_cannot_delete_account": "Вы являетесь единственным владельцем этой организации. Пожалуйста, сначала передайте права другому участнику.",
|
||||
"warning_cannot_undo": "Это действие необратимо"
|
||||
"warning_cannot_undo": "Это действие необратимо",
|
||||
"wrong_password": "Неверный пароль"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Добавьте участников в команду и определите их роль.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "Lås upp tvåfaktorsautentisering med en högre plan",
|
||||
"update_personal_info": "Uppdatera din personliga information",
|
||||
"warning_cannot_delete_account": "Du är den enda ägaren av denna organisation. Vänligen överför ägarskapet till en annan medlem först.",
|
||||
"warning_cannot_undo": "Detta kan inte ångras"
|
||||
"warning_cannot_undo": "Detta kan inte ångras",
|
||||
"wrong_password": "Fel lösenord"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "Lägg till medlemmar i teamet och bestäm deras roll.",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "使用 更高 级 方案 解锁 双 重 因素 验证",
|
||||
"update_personal_info": "更新你的个人信息",
|
||||
"warning_cannot_delete_account": "您 是 该 组织 的 唯一 拥有者 。 请 先 将 所有权 转移 给 其他 成员 。",
|
||||
"warning_cannot_undo": "此 无法 撤销。"
|
||||
"warning_cannot_undo": "此 无法 撤销。",
|
||||
"wrong_password": "密码错误"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "将 成员 添加到 团队 ,并 确定 他们 的 角色",
|
||||
|
||||
@@ -1251,7 +1251,8 @@
|
||||
"unlock_two_factor_authentication": "使用更高等級的方案解鎖雙重驗證",
|
||||
"update_personal_info": "更新您的個人資訊",
|
||||
"warning_cannot_delete_account": "您是此組織的唯一擁有者。請先將所有權轉讓給其他成員。",
|
||||
"warning_cannot_undo": "此操作無法復原"
|
||||
"warning_cannot_undo": "此操作無法復原",
|
||||
"wrong_password": "密碼錯誤"
|
||||
},
|
||||
"teams": {
|
||||
"add_members_description": "將成員新增至團隊並確定其角色。",
|
||||
|
||||
@@ -1,24 +1,89 @@
|
||||
"use server";
|
||||
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { z } from "zod";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { AuthorizationError, InvalidInputError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||
import { verifyUserPassword } from "@/lib/user/password";
|
||||
import { deleteUser, getUser } from "@/lib/user/service";
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { applyRateLimit } from "@/modules/core/rate-limit/helpers";
|
||||
import { rateLimitConfigs } from "@/modules/core/rate-limit/rate-limit-configs";
|
||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { DELETE_ACCOUNT_WRONG_PASSWORD_ERROR } from "./constants";
|
||||
|
||||
export const deleteUserAction = authenticatedActionClient.action(
|
||||
withAuditLogging("deleted", "user", async ({ ctx }) => {
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
const organizationsWithSingleOwner = await getOrganizationsWhereUserIsSingleOwner(ctx.user.id);
|
||||
if (!isMultiOrgEnabled && organizationsWithSingleOwner.length > 0) {
|
||||
throw new OperationNotAllowedError(
|
||||
"You are the only owner of this organization. Please transfer ownership to another member first."
|
||||
);
|
||||
}
|
||||
const DELETE_USER_CONFIRMATION_REQUIRED_ERROR =
|
||||
"Password and email confirmation are required to delete your account.";
|
||||
|
||||
const ZDeleteUserConfirmation = z
|
||||
.object({
|
||||
confirmationEmail: z.string().trim().min(1).max(255),
|
||||
password: z.string().max(128).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const parseDeleteUserConfirmation = (input: unknown) => {
|
||||
const parsedInput = ZDeleteUserConfirmation.safeParse(input);
|
||||
|
||||
if (!parsedInput.success) {
|
||||
throw new InvalidInputError(DELETE_USER_CONFIRMATION_REQUIRED_ERROR);
|
||||
}
|
||||
|
||||
return parsedInput.data;
|
||||
};
|
||||
|
||||
const getPasswordOrThrow = (password?: string) => {
|
||||
if (!password) {
|
||||
throw new InvalidInputError(DELETE_USER_CONFIRMATION_REQUIRED_ERROR);
|
||||
}
|
||||
|
||||
return password;
|
||||
};
|
||||
|
||||
const logAccountDeletionError = (userId: string, error: unknown) => {
|
||||
logger.error({ error, userId }, "Account deletion failed");
|
||||
};
|
||||
|
||||
export const deleteUserAction = authenticatedActionClient.inputSchema(z.unknown()).action(
|
||||
withAuditLogging("deleted", "user", async ({ ctx, parsedInput }) => {
|
||||
ctx.auditLoggingCtx.userId = ctx.user.id;
|
||||
ctx.auditLoggingCtx.oldObject = await getUser(ctx.user.id);
|
||||
const result = await deleteUser(ctx.user.id);
|
||||
return result;
|
||||
|
||||
try {
|
||||
await applyRateLimit(rateLimitConfigs.actions.accountDeletion, ctx.user.id);
|
||||
|
||||
const isPasswordBackedAccount = ctx.user.identityProvider === "email";
|
||||
const { confirmationEmail, password } = parseDeleteUserConfirmation(parsedInput);
|
||||
|
||||
if (confirmationEmail.toLowerCase() !== ctx.user.email.toLowerCase()) {
|
||||
throw new AuthorizationError("Email confirmation does not match");
|
||||
}
|
||||
|
||||
if (isPasswordBackedAccount) {
|
||||
const isCorrectPassword = await verifyUserPassword(ctx.user.id, getPasswordOrThrow(password));
|
||||
if (!isCorrectPassword) {
|
||||
throw new AuthorizationError(DELETE_ACCOUNT_WRONG_PASSWORD_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
if (!isMultiOrgEnabled) {
|
||||
const organizationsWithSingleOwner = await getOrganizationsWhereUserIsSingleOwner(ctx.user.id);
|
||||
if (organizationsWithSingleOwner.length > 0) {
|
||||
throw new OperationNotAllowedError(
|
||||
"You are the only owner of this organization. Please transfer ownership to another member first."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.auditLoggingCtx.oldObject = await getUser(ctx.user.id);
|
||||
|
||||
await deleteUser(ctx.user.id);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logAccountDeletionError(ctx.user.id, error);
|
||||
throw error;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const DELETE_ACCOUNT_WRONG_PASSWORD_ERROR = "Wrong password";
|
||||
@@ -3,12 +3,16 @@
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { logger } from "@formbricks/logger";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
import { PasswordInput } from "@/modules/ui/components/password-input";
|
||||
import { deleteUserAction } from "./actions";
|
||||
import { DELETE_ACCOUNT_WRONG_PASSWORD_ERROR } from "./constants";
|
||||
|
||||
interface DeleteAccountModalProps {
|
||||
open: boolean;
|
||||
@@ -28,15 +32,57 @@ export const DeleteAccountModal = ({
|
||||
const { t } = useTranslation();
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
||||
const isPasswordBackedAccount = user.identityProvider === "email";
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) {
|
||||
setInputValue("");
|
||||
setPassword("");
|
||||
}
|
||||
setOpen(nextOpen);
|
||||
};
|
||||
|
||||
const hasValidEmailConfirmation = inputValue.trim().toLowerCase() === user.email.toLowerCase();
|
||||
const hasValidConfirmation = hasValidEmailConfirmation && (!isPasswordBackedAccount || password.length > 0);
|
||||
const isDeleteDisabled = !hasValidConfirmation;
|
||||
|
||||
const deleteAccount = async () => {
|
||||
try {
|
||||
if (!hasValidConfirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeleting(true);
|
||||
await deleteUserAction();
|
||||
const result = await deleteUserAction(
|
||||
isPasswordBackedAccount
|
||||
? {
|
||||
confirmationEmail: inputValue,
|
||||
password,
|
||||
}
|
||||
: {
|
||||
confirmationEmail: inputValue,
|
||||
}
|
||||
);
|
||||
|
||||
if (!result?.data?.success) {
|
||||
const fallbackErrorMessage = t("common.something_went_wrong_please_try_again");
|
||||
let errorMessage = fallbackErrorMessage;
|
||||
|
||||
if (result?.serverError === DELETE_ACCOUNT_WRONG_PASSWORD_ERROR) {
|
||||
errorMessage = t("environments.settings.profile.wrong_password");
|
||||
} else if (result) {
|
||||
errorMessage = getFormattedErrorMessage(result);
|
||||
}
|
||||
|
||||
logger.error({ errorMessage }, "Account deletion action failed");
|
||||
toast.error(errorMessage || fallbackErrorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Sign out with account deletion reason (no automatic redirect)
|
||||
await signOutWithAudit({
|
||||
@@ -52,22 +98,22 @@ export const DeleteAccountModal = ({
|
||||
window.location.replace("/auth/login");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Something went wrong");
|
||||
logger.error({ error }, "Account deletion failed");
|
||||
toast.error(t("common.something_went_wrong_please_try_again"));
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DeleteDialog
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
setOpen={handleOpenChange}
|
||||
deleteWhat={t("common.account")}
|
||||
onDelete={() => deleteAccount()}
|
||||
text={t("environments.settings.profile.account_deletion_consequences_warning")}
|
||||
isDeleting={deleting}
|
||||
disabled={inputValue !== user.email}>
|
||||
disabled={isDeleteDisabled}>
|
||||
<div className="py-5">
|
||||
<ul className="list-disc pb-6 pl-6">
|
||||
<li>
|
||||
@@ -110,11 +156,29 @@ export const DeleteAccountModal = ({
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
placeholder={user.email}
|
||||
className="mt-5"
|
||||
className="mt-2"
|
||||
type="text"
|
||||
id="deleteAccountConfirmation"
|
||||
name="deleteAccountConfirmation"
|
||||
/>
|
||||
{isPasswordBackedAccount && (
|
||||
<>
|
||||
<label htmlFor="deleteAccountPassword" className="mt-4 block">
|
||||
{t("common.password")}
|
||||
</label>
|
||||
<PasswordInput
|
||||
data-testid="deleteAccountPassword"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
className="pr-10"
|
||||
containerClassName="mt-2"
|
||||
id="deleteAccountPassword"
|
||||
name="deleteAccountPassword"
|
||||
required
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</DeleteDialog>
|
||||
|
||||
@@ -75,6 +75,7 @@ describe("rateLimitConfigs", () => {
|
||||
const actionConfigs = Object.keys(rateLimitConfigs.actions);
|
||||
expect(actionConfigs).toEqual([
|
||||
"emailUpdate",
|
||||
"accountDeletion",
|
||||
"surveyFollowUp",
|
||||
"sendLinkSurveyEmail",
|
||||
"licenseRecheck",
|
||||
@@ -139,6 +140,7 @@ describe("rateLimitConfigs", () => {
|
||||
{ config: rateLimitConfigs.api.v3, identifier: "api-v3-key" },
|
||||
{ config: rateLimitConfigs.api.client, identifier: "client-api-key" },
|
||||
{ config: rateLimitConfigs.actions.emailUpdate, identifier: "user-profile" },
|
||||
{ config: rateLimitConfigs.actions.accountDeletion, identifier: "user-account-delete" },
|
||||
{ config: rateLimitConfigs.storage.upload, identifier: "storage-upload" },
|
||||
{ config: rateLimitConfigs.storage.delete, identifier: "storage-delete" },
|
||||
];
|
||||
|
||||
@@ -18,6 +18,7 @@ export const rateLimitConfigs = {
|
||||
// Server actions - varies by action type
|
||||
actions: {
|
||||
emailUpdate: { interval: 3600, allowedPerInterval: 3, namespace: "action:email" }, // 3 per hour
|
||||
accountDeletion: { interval: 3600, allowedPerInterval: 5, namespace: "action:account-delete" }, // 5 per hour
|
||||
surveyFollowUp: { interval: 3600, allowedPerInterval: 50, namespace: "action:followup" }, // 50 per hour
|
||||
sendLinkSurveyEmail: {
|
||||
interval: 3600,
|
||||
|
||||
Reference in New Issue
Block a user