Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
"clean": "rimraf .turbo node_modules dist storybook-static"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@chromatic-com/storybook": "5.0.2",
"@storybook/addon-a11y": "10.3.5",
"@storybook/addon-docs": "10.3.5",
"@storybook/addon-links": "10.3.5",
"@storybook/addon-onboarding": "10.3.5",
"@storybook/react-vite": "10.3.5",
"@tailwindcss/vite": "4.2.1",
"@typescript-eslint/eslint-plugin": "8.57.0",
"@typescript-eslint/parser": "8.57.0",
"@tailwindcss/vite": "4.2.4",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"@vitejs/plugin-react": "5.1.4",
"eslint-plugin-react-refresh": "0.4.26",
"eslint-plugin-storybook": "10.3.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
36 changes: 36 additions & 0 deletions apps/web/lib/user/password.ts
Original file line number Diff line number Diff line change
@@ -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);
};
3 changes: 2 additions & 1 deletion apps/web/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/es-ES.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/hu-HU.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,8 @@
"unlock_two_factor_authentication": "上位プランで二段階認証をアンロック",
"update_personal_info": "個人情報を更新",
"warning_cannot_delete_account": "あなたは、この組織の唯一のオーナーです。まず、別のメンバーにオーナーシップを譲渡してください。",
"warning_cannot_undo": "この操作は元に戻せません"
"warning_cannot_undo": "この操作は元に戻せません",
"wrong_password": "パスワードが間違っています"
},
"teams": {
"add_members_description": "チームにメンバーを追加し、役割を決定します。",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/nl-NL.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/pt-PT.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/ro-RO.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/ru-RU.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,8 @@
"unlock_two_factor_authentication": "Откройте двухфакторную аутентификацию с более высоким тарифом",
"update_personal_info": "Обновить личную информацию",
"warning_cannot_delete_account": "Вы являетесь единственным владельцем этой организации. Пожалуйста, сначала передайте права другому участнику.",
"warning_cannot_undo": "Это действие необратимо"
"warning_cannot_undo": "Это действие необратимо",
"wrong_password": "Неверный пароль"
},
"teams": {
"add_members_description": "Добавьте участников в команду и определите их роль.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/sv-SE.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,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.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/tr-TR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,8 @@
"unlock_two_factor_authentication": "Daha üst bir planla iki faktörlü kimlik doğrulamayı açın",
"update_personal_info": "Kişisel bilgilerinizi güncelleyin",
"warning_cannot_delete_account": "Bu organizasyonun tek sahibi sizsiniz. Lütfen önce sahipliği başka bir üyeye devredin.",
"warning_cannot_undo": "Bu işlem geri alınamaz"
"warning_cannot_undo": "Bu işlem geri alınamaz",
"wrong_password": "Yanlış parola"
},
"teams": {
"add_members_description": "Takıma üyeler ekleyin ve rollerini belirleyin.",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/zh-Hans-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,8 @@
"unlock_two_factor_authentication": "使用 更高 级 方案 解锁 双 重 因素 验证",
"update_personal_info": "更新你的个人信息",
"warning_cannot_delete_account": "您 是 该 组织 的 唯一 拥有者 。 请 先 将 所有权 转移 给 其他 成员 。",
"warning_cannot_undo": "此 无法 撤销。"
"warning_cannot_undo": "此 无法 撤销。",
"wrong_password": "密码错误"
},
"teams": {
"add_members_description": "将 成员 添加到 团队 ,并 确定 他们 的 角色",
Expand Down
3 changes: 2 additions & 1 deletion apps/web/locales/zh-Hant-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,8 @@
"unlock_two_factor_authentication": "使用更高等級的方案解鎖雙重驗證",
"update_personal_info": "更新您的個人資訊",
"warning_cannot_delete_account": "您是此組織的唯一擁有者。請先將所有權轉讓給其他成員。",
"warning_cannot_undo": "此操作無法復原"
"warning_cannot_undo": "此操作無法復原",
"wrong_password": "密碼錯誤"
},
"teams": {
"add_members_description": "將成員新增至團隊並確定其角色。",
Expand Down
91 changes: 78 additions & 13 deletions apps/web/modules/account/components/DeleteAccountModal/actions.ts
Original file line number Diff line number Diff line change
@@ -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;
}
})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DELETE_ACCOUNT_WRONG_PASSWORD_ERROR = "Wrong password";
Loading
Loading