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

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):

---------

Co-authored-by: Stan Girard <girard.stanislas@gmail.com>
This commit is contained in:
Antoine Dewez 2024-05-02 11:31:58 +02:00 committed by GitHub
parent 699097f6ac
commit 8d54187713
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 127 additions and 18 deletions

View File

@ -79,6 +79,26 @@ def get_user_identity_route(
"""
return user_repository.get_user_identity(current_user.id)
@user_router.delete(
"/user_data",
dependencies=[Depends(AuthBearer())],
tags=["User"],
)
async def delete_user_data_route(
current_user: UserIdentity = Depends(get_current_user),
):
"""
Delete a user.
- `user_id`: The ID of the user to delete.
This endpoint deletes a user from the system.
"""
user_repository.delete_user_data(current_user.id)
return {"message": "User deleted successfully"}
@user_router.get(
"/user/credits",
dependencies=[Depends(AuthBearer())],

View File

@ -76,6 +76,30 @@ class Users(UsersInterface):
).execute()
return response.data[0]["email"]
def delete_user_data(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)

View File

@ -46,6 +46,15 @@ class UsersInterface(ABC):
pass
@abstractmethod
def delete_user_data(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

View File

@ -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;
}
}

View File

@ -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";
@ -18,7 +20,10 @@ import { useLogoutModal } from "../../lib/hooks/useLogoutModal";
const UserPage = (): JSX.Element => {
const { session } = useSupabase();
const { userData } = useUserData();
const { deleteUserData } = useUserApi();
const { t } = useTranslation(["translation", "logout"]);
const [deleteAccountModalOpened, setDeleteAccountModalOpened] =
useState(false);
const {
handleLogout,
isLoggingOut,
@ -26,14 +31,24 @@ const UserPage = (): JSX.Element => {
setIsLogoutModalOpened,
} = useLogoutModal();
const button: ButtonType = {
const buttons: ButtonType[] = [
{
label: "Logout",
color: "dangerous",
onClick: () => {
setIsLogoutModalOpened(true);
},
iconName: "logout",
};
},
{
label: "Delete Account",
color: "dangerous",
onClick: () => {
setDeleteAccountModalOpened(true);
},
iconName: "delete",
},
];
if (!session || !userData) {
redirectToLogin();
@ -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}>
@ -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"
@ -76,6 +89,34 @@ 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={() => {
void deleteUserData();
void handleLogout();
}}
label="Delete Account"
iconName="logout"
></QuivrButton>
</div>
</div>
</Modal>
</>
);
};

View File

@ -1,6 +1,7 @@
import { useAxios } from "@/lib/hooks";
import {
deleteUserData,
getUser,
getUserCredits,
getUserIdentity,
@ -18,6 +19,7 @@ export const useUserApi = () => {
) => updateUserIdentity(userIdentityUpdatableProperties, axiosInstance),
getUserIdentity: async () => getUserIdentity(axiosInstance),
getUser: async () => getUser(axiosInstance),
deleteUserData: async () => deleteUserData(axiosInstance),
getUserCredits: async () => getUserCredits(axiosInstance),
};
};

View File

@ -31,7 +31,7 @@ export type UserIdentityUpdatableProperties = {
};
export type UserIdentity = {
user_id: UUID;
id: UUID;
onboarded: boolean;
username: string;
};
@ -54,6 +54,12 @@ export const getUser = async (
axiosInstance: AxiosInstance
): Promise<UserStats> => (await axiosInstance.get<UserStats>("/user")).data;
export const deleteUserData = async (
axiosInstance: AxiosInstance
): Promise<void> => {
await axiosInstance.delete(`/user_data`);
};
export const getUserCredits = async (
axiosInstance: AxiosInstance
): Promise<number> => (await axiosInstance.get<number>("/user/credits")).data;

View File

@ -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);