-
+
Delete your account with all personal data. This cannot be undone!
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditAvatar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditAvatar.tsx
new file mode 100644
index 00000000000..cfe31eade38
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditAvatar.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import AvatarPlaceholder from "@/images/avatar-placeholder.png";
+import { Button, ProfileAvatar } from "@formbricks/ui";
+import Image from "next/image";
+import { Session } from "next-auth";
+
+export function EditAvatar({ session }:{session: Session | null}) {
+ return (
+
+ {session?.user?.image ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditName.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditName.tsx
new file mode 100644
index 00000000000..2db1dc360cc
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/EditName.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { Button, Input, Label } from "@formbricks/ui";
+import { useForm } from "react-hook-form";
+import toast from "react-hot-toast";
+import { profileEditAction } from "./actions";
+import { TProfile } from "@formbricks/types/v1/profile";
+
+export function EditName({ profile }: { profile: TProfile }) {
+
+ const {
+ register,
+ handleSubmit,
+ formState: { isSubmitting },
+ } = useForm<{name:string}>()
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/profile/actions.ts
new file mode 100644
index 00000000000..bc067a2fcf1
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/actions.ts
@@ -0,0 +1,12 @@
+"use server";
+
+import { updateProfile, deleteProfile } from "@formbricks/lib/services/profile";
+import { Prisma } from "@prisma/client";
+
+export async function profileEditAction(userId: string, data: Prisma.UserUpdateInput) {
+ return await updateProfile(userId, data);
+}
+
+export async function profileDeleteAction(userId: string) {
+ return await deleteProfile(userId);
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/loading.tsx
new file mode 100644
index 00000000000..e4aa56c89ef
--- /dev/null
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/loading.tsx
@@ -0,0 +1,54 @@
+function LoadingCard({ title, description, skeletonLines }) {
+ return (
+
+
+
{title}
+
{description}
+
+
+
+ {skeletonLines.map((line, index) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default function Loading() {
+ const cards = [
+ {
+ title: "Personal Information",
+ description: "Update your personal information",
+ skeletonLines: [
+ { classes: "h-4 w-28" },
+ { classes: "h-6 w-64" },
+ { classes: "h-4 w-28" },
+ { classes: "h-6 w-64" },
+ { classes: "h-8 w-24" },
+ ],
+ },
+ {
+ title: "Avatar",
+ description: "Assist your team in identifying you on Formbricks.",
+ skeletonLines: [{ classes: "h-10 w-10" }, { classes: "h-8 w-24" }],
+ },
+ {
+ title: "Delete account",
+ description: "Delete your account with all of your personal information and data.",
+ skeletonLines: [{ classes: "h-4 w-60" }, { classes: "h-8 w-24" }],
+ },
+ ];
+
+ return (
+
+
Profile
+ {cards.map((card, index) => (
+
+ ))}
+
+ );
+}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx
index 492c55d5b51..6c54272d4f9 100644
--- a/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx
+++ b/apps/web/app/(app)/environments/[environmentId]/settings/profile/page.tsx
@@ -1,25 +1,37 @@
-import SettingsCard from "../SettingsCard";
-import SettingsTitle from "../SettingsTitle";
+export const revalidate = REVALIDATION_INTERVAL;
+
+import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { getServerSession } from "next-auth";
-import { EditName, EditAvatar, DeleteAccount } from "./editProfile";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
+import SettingsCard from "../SettingsCard";
+import SettingsTitle from "../SettingsTitle";
+import { DeleteAccount } from "./DeleteAccount";
+import { EditName } from "./EditName";
+import { EditAvatar } from "./EditAvatar";
+import { getProfile } from "@formbricks/lib/services/profile";
export default async function ProfileSettingsPage() {
const session = await getServerSession(authOptions);
+ const profile = session ? await getProfile(session.user.id) : null;
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
+ <>
+ {profile && (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+ >
);
}
diff --git a/packages/lib/services/profile.ts b/packages/lib/services/profile.ts
new file mode 100644
index 00000000000..03642682456
--- /dev/null
+++ b/packages/lib/services/profile.ts
@@ -0,0 +1,141 @@
+import { prisma } from "@formbricks/database";
+import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors";
+import { Prisma } from "@prisma/client";
+import { TProfile } from "@formbricks/types/v1/profile";
+import { deleteTeam } from "./team";
+import { MembershipRole } from "@prisma/client";
+import { cache } from "react";
+
+const responseSelection = {
+ id: true,
+ name: true,
+ email: true,
+ createdAt: true,
+ updatedAt: true,
+};
+
+interface Membership {
+ role: MembershipRole;
+ userId: string;
+}
+
+// function to retrive basic information about a user's profile
+export const getProfile = cache(async (userId: string): Promise
=> {
+ try {
+ const profile = await prisma.user.findUnique({
+ where: {
+ id: userId,
+ },
+ select: responseSelection,
+ });
+
+ if (!profile) {
+ return null;
+ }
+
+ return profile;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+});
+
+const updateUserMembership = async (teamId: string, userId: string, role: MembershipRole) => {
+ await prisma.membership.update({
+ where: {
+ userId_teamId: {
+ userId,
+ teamId,
+ },
+ },
+ data: {
+ role,
+ },
+ });
+};
+
+const getAdminMemberships = (memberships: Membership[]) =>
+ memberships.filter((membership) => membership.role === MembershipRole.admin);
+
+// function to update a user's profile
+export const updateProfile = async (personId: string, data: Prisma.UserUpdateInput): Promise => {
+ try {
+ const updatedProfile = await prisma.user.update({
+ where: {
+ id: personId,
+ },
+ data: data,
+ });
+
+ return updatedProfile;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2016") {
+ throw new ResourceNotFoundError("Profile", personId);
+ } else {
+ throw error; // Re-throw any other errors
+ }
+ }
+};
+const deleteUser = async (userId: string) => {
+ await prisma.user.delete({
+ where: {
+ id: userId,
+ },
+ });
+};
+
+// function to delete a user's profile including teams
+export const deleteProfile = async (personId: string): Promise => {
+ try {
+ const currentUserMemberships = await prisma.membership.findMany({
+ where: {
+ userId: personId,
+ },
+ include: {
+ team: {
+ select: {
+ id: true,
+ name: true,
+ memberships: {
+ select: {
+ userId: true,
+ role: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ for (const currentUserMembership of currentUserMemberships) {
+ const teamMemberships = currentUserMembership.team.memberships;
+ const role = currentUserMembership.role;
+ const teamId = currentUserMembership.teamId;
+
+ const teamAdminMemberships = getAdminMemberships(teamMemberships);
+ const teamHasAtLeastOneAdmin = teamAdminMemberships.length > 0;
+ const teamHasOnlyOneMember = teamMemberships.length === 1;
+ const currentUserIsTeamOwner = role === MembershipRole.owner;
+
+ if (teamHasOnlyOneMember) {
+ await deleteTeam(teamId);
+ } else if (currentUserIsTeamOwner && teamHasAtLeastOneAdmin) {
+ const firstAdmin = teamAdminMemberships[0];
+ await updateUserMembership(teamId, firstAdmin.userId, MembershipRole.owner);
+ } else if (currentUserIsTeamOwner) {
+ await deleteTeam(teamId);
+ }
+ }
+
+ await deleteUser(personId);
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+};
diff --git a/packages/lib/services/team.ts b/packages/lib/services/team.ts
index e08de09575e..6babcebaafa 100644
--- a/packages/lib/services/team.ts
+++ b/packages/lib/services/team.ts
@@ -1,9 +1,9 @@
-import { cache } from "react";
import { prisma } from "@formbricks/database";
-import { Prisma } from "@prisma/client";
import { DatabaseError } from "@formbricks/errors";
import { TTeam } from "@formbricks/types/v1/teams";
import { createId } from "@paralleldrive/cuid2";
+import { Prisma } from "@prisma/client";
+import { cache } from "react";
import {
ChurnResponses,
ChurnSurvey,
@@ -58,6 +58,22 @@ export const getTeamByEnvironmentId = cache(async (environmentId: string): Promi
}
});
+export const deleteTeam = async (teamId: string) => {
+ try {
+ await prisma.team.delete({
+ where: {
+ id: teamId,
+ },
+ });
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ throw new DatabaseError("Database operation failed");
+ }
+
+ throw error;
+ }
+};
+
export const createDemoProduct = cache(async (teamId: string) => {
const productWithEnvironment = Prisma.validator()({
include: {
diff --git a/packages/types/v1/profile.ts b/packages/types/v1/profile.ts
new file mode 100644
index 00000000000..0cbee578db6
--- /dev/null
+++ b/packages/types/v1/profile.ts
@@ -0,0 +1,11 @@
+import z from "zod";
+
+export const ZProfile = z.object({
+ id: z.string(),
+ name: z.string().nullish(),
+ email: z.string(),
+ createdAt: z.date(),
+ updatedAt: z.date(),
+});
+
+export type TProfile = z.infer;