Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(user): Delete User Data from frontend #2476

Merged
merged 15 commits into from May 2, 2024
20 changes: 20 additions & 0 deletions backend/modules/user/controller/user_controller.py
Expand Up @@ -79,6 +79,26 @@ def get_user_identity_route(
"""
return user_repository.get_user_identity(current_user.id)

@user_router.delete(
"/user/{user_id}",
dependencies=[Depends(AuthBearer())],
tags=["User"],
)
async def delete_user_route(
user_id: str
):
"""
Delete a user.

- `user_id`: The ID of the user to delete.

This endpoint deletes a user from the system.
"""

user_repository.delete_user(user_id)
Zewed marked this conversation as resolved.
Show resolved Hide resolved

return {"message": "User deleted successfully"}

@user_router.get(
"/user/credits",
dependencies=[Depends(AuthBearer())],
Expand Down
24 changes: 24 additions & 0 deletions backend/modules/user/repository/users.py
Expand Up @@ -76,6 +76,30 @@ def get_user_email_by_user_id(self, user_id):
).execute()
return response.data[0]["email"]

def delete_user(self, user_id):
response = (
self.db.from_("brains_users")
.select("brain_id")
.filter("rights", "eq", "Owner")
.filter("user_id", "eq", str(user_id))
.execute()
)
brain_ids = [row["brain_id"] for row in response.data]

for brain_id in brain_ids:
self.db.table("brains").delete().filter("brain_id", "eq", brain_id).execute()

for brain_id in brain_ids:
self.db.table("brains_vectors").delete().filter("brain_id", "eq", brain_id).execute()

for brain_id in brain_ids:
self.db.table("chat_history").delete().filter("brain_id", "eq", brain_id).execute()

self.db.table("user_settings").delete().filter("user_id", "eq", str(user_id)).execute()
self.db.table("user_identity").delete().filter("user_id", "eq", str(user_id)).execute()
self.db.table("users").delete().filter("id", "eq", str(user_id)).execute()


def get_user_credits(self, user_id):
user_usage_instance = user_usage.UserUsage(id=user_id)

Expand Down
9 changes: 9 additions & 0 deletions backend/modules/user/repository/users_interface.py
Expand Up @@ -46,6 +46,15 @@ def get_user_email_by_user_id(self, user_id: UUID) -> str:
pass

@abstractmethod
def delete_user(self, user_id: str):
"""
Delete a user.

- `user_id`: The ID of the user to delete.

This endpoint deletes a user from the system.
"""
@abstractmethod
def get_user_credits(self, user_id: UUID) -> int:
"""
Get user remaining credits
Expand Down
11 changes: 11 additions & 0 deletions frontend/app/user/page.module.scss
Expand Up @@ -7,3 +7,14 @@
flex-direction: column;
gap: Spacings.$spacing05;
}

.modal_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;

.buttons {
display: flex;
justify-content: space-between;
}
}
71 changes: 57 additions & 14 deletions frontend/app/user/page.tsx
@@ -1,7 +1,9 @@
"use client";

import { useState } from "react";
import { useTranslation } from "react-i18next";

import { useUserApi } from "@/lib/api/user/useUserApi";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { Modal } from "@/lib/components/ui/Modal/Modal";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
Expand All @@ -17,23 +19,36 @@ import { useLogoutModal } from "../../lib/hooks/useLogoutModal";

const UserPage = (): JSX.Element => {
const { session } = useSupabase();
const { userData } = useUserData();
const { userData, userIdentityData } = useUserData();
const { deleteUser } = useUserApi();
const { t } = useTranslation(["translation", "logout"]);
const [deleteAccountModalOpened, setDeleteAccountModalOpened] =
useState(false);
const {
handleLogout,
isLoggingOut,
isLogoutModalOpened,
setIsLogoutModalOpened,
} = useLogoutModal();

const button: ButtonType = {
label: "Logout",
color: "dangerous",
onClick: () => {
setIsLogoutModalOpened(true);
const buttons: ButtonType[] = [
{
label: "Logout",
color: "dangerous",
onClick: () => {
setIsLogoutModalOpened(true);
},
iconName: "logout",
},
iconName: "logout",
};
{
label: "Delete Account",
color: "dangerous",
onClick: () => {
setDeleteAccountModalOpened(true);
},
iconName: "delete",
},
];

if (!session || !userData) {
redirectToLogin();
Expand All @@ -42,7 +57,7 @@ const UserPage = (): JSX.Element => {
return (
<>
<div className={styles.page_header}>
<PageHeader iconName="user" label="Profile" buttons={[button]} />
<PageHeader iconName="user" label="Profile" buttons={buttons} />
</div>
<div className={styles.user_page_container}>
<div className={styles.content_wrapper}>
Expand All @@ -55,11 +70,9 @@ const UserPage = (): JSX.Element => {
size="auto"
CloseTrigger={<div />}
>
<div className="text-center flex flex-col items-center gap-5">
<h2 className="text-lg font-medium mb-5">
{t("areYouSure", { ns: "logout" })}
</h2>
<div className="flex gap-5 items-center justify-center">
<div className={styles.modal_wrapper}>
<h2>{t("areYouSure", { ns: "logout" })}</h2>
<div className={styles.buttons}>
<QuivrButton
onClick={() => setIsLogoutModalOpened(false)}
color="primary"
Expand All @@ -76,6 +89,36 @@ const UserPage = (): JSX.Element => {
</div>
</div>
</Modal>
<Modal
isOpen={deleteAccountModalOpened}
setOpen={setDeleteAccountModalOpened}
size="auto"
CloseTrigger={<div />}
>
<div className={styles.modal_wrapper}>
<h2>Are you sure you want to delete your account ?</h2>
<div className={styles.buttons}>
<QuivrButton
onClick={() => setDeleteAccountModalOpened(false)}
color="primary"
label={t("cancel", { ns: "logout" })}
iconName="close"
></QuivrButton>
<QuivrButton
isLoading={isLoggingOut}
color="dangerous"
onClick={() => {
if (userIdentityData) {
void deleteUser(userIdentityData.id, session);
}
void handleLogout();
}}
label="Delete Account"
iconName="logout"
></QuivrButton>
</div>
</div>
</Modal>
</>
);
};
Expand Down
5 changes: 5 additions & 0 deletions frontend/lib/api/user/useUserApi.ts
@@ -1,6 +1,9 @@
import { Session } from "@supabase/supabase-js";

import { useAxios } from "@/lib/hooks";

import {
deleteUser,
getUser,
getUserCredits,
getUserIdentity,
Expand All @@ -18,6 +21,8 @@ export const useUserApi = () => {
) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance),
getUserIdentity: async () => getUserIdentity(axiosInstance),
getUser: async () => getUser(axiosInstance),
deleteUser: async (userId: string, session: Session) =>
deleteUser(axiosInstance, userId, session),
getUserCredits: async () => getUserCredits(axiosInstance),
};
};
13 changes: 12 additions & 1 deletion frontend/lib/api/user/user.ts
@@ -1,3 +1,4 @@
import { Session } from "@supabase/auth-helpers-nextjs";
import { AxiosInstance } from "axios";
import { UUID } from "crypto";

Expand Down Expand Up @@ -31,7 +32,7 @@ export type UserIdentityUpdatableProperties = {
};

export type UserIdentity = {
user_id: UUID;
id: UUID;
onboarded: boolean;
username: string;
};
Expand All @@ -54,6 +55,16 @@ export const getUser = async (
axiosInstance: AxiosInstance
): Promise<UserStats> => (await axiosInstance.get<UserStats>("/user")).data;

export const deleteUser = async (
axiosInstance: AxiosInstance,
userId: string,
session: Session
): Promise<void> => {
if (session.user.id === userId) {
await axiosInstance.delete(`/user/${userId}`);
}
};

export const getUserCredits = async (
axiosInstance: AxiosInstance
): Promise<number> => (await axiosInstance.get<number>("/user/credits")).data;
4 changes: 0 additions & 4 deletions frontend/lib/hooks/useLogoutModal.ts
Expand Up @@ -30,10 +30,6 @@ export const useLogoutModal = () => {
text: t("error", { errorMessage: error.message, ns: "logout" }),
});
} else {
publish({
variant: "success",
text: t("loggedOut", { ns: "logout" }),
});
window.location.href = "/";
}
setIsLoggingOut(false);
Expand Down