diff --git a/apps/backend/src/api/routes/users.controller.ts b/apps/backend/src/api/routes/users.controller.ts index 601ba6556..55195c7b4 100644 --- a/apps/backend/src/api/routes/users.controller.ts +++ b/apps/backend/src/api/routes/users.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, HttpException, Post, @@ -280,4 +281,9 @@ export class UsersController { track: uniqueId, }); } + + @Delete('/delete') + async deleteUser(@GetUserFromRequest() user: User) { + return this._userService.deleteUser(user.id); + } } diff --git a/apps/frontend/src/components/layout/settings.component.tsx b/apps/frontend/src/components/layout/settings.component.tsx index ac43dc8b5..b5c31812b 100644 --- a/apps/frontend/src/components/layout/settings.component.tsx +++ b/apps/frontend/src/components/layout/settings.component.tsx @@ -30,6 +30,7 @@ import { SignaturesComponent } from '@gitroom/frontend/components/settings/signa import { Autopost } from '@gitroom/frontend/components/autopost/autopost'; import { useT } from '@gitroom/react/translation/get.transation.service.client'; import { SVGLine } from '@gitroom/frontend/components/launches/launches.component'; +import { UserComponent } from '../settings/user.component'; export const SettingsPopup: FC<{ getRef?: Ref; }> = (props) => { @@ -115,6 +116,7 @@ export const SettingsPopup: FC<{ if (user?.tier?.public_api && isGeneral && showLogout) { arr.push({ tab: 'api', label: t('public_api', 'Public API') }); } + arr.push({ tab: 'user', label: t('user', 'User') }); return arr; }, [user, isGeneral, showLogout, t]); @@ -198,6 +200,12 @@ export const SettingsPopup: FC<{ )} + {tab === 'user' && user?.tier.current !== 'FREE' && ( +
+ +
+ )} + {tab === 'api' && !!user?.tier?.public_api && isGeneral && diff --git a/apps/frontend/src/components/settings/user.component.tsx b/apps/frontend/src/components/settings/user.component.tsx new file mode 100644 index 000000000..00c2d4b4d --- /dev/null +++ b/apps/frontend/src/components/settings/user.component.tsx @@ -0,0 +1,77 @@ +'use client'; + +import React, { FC, useCallback } from 'react'; +import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { Button } from '@gitroom/react/form/button'; +import { useToaster } from '@gitroom/react/toaster/toaster'; +import { useT } from '@gitroom/react/translation/get.transation.service.client'; +import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; + +export const UserComponent: FC = () => { + const fetch = useFetch(); + const toast = useToaster(); + const t = useT(); + + const deleteAccount = useCallback(async () => { + try { + const res = await fetch('/user/delete', { + method: 'DELETE', + }); + if (!res.ok) throw new Error('Failed to delete account'); + + toast.show( + t('account_deleted', 'Account deleted successfully.'), + 'success' + ); + window.location.href = '/'; + } catch (err) { + toast.show( + t( + 'delete_account_failed', + 'Failed to delete account. Please try again.' + ), + 'warning' + ); + } + }, []); + + const confirmDelete = useCallback(async () => { + const confirmed = await deleteDialog( + t( + 'delete_account_confirmation', + 'Are you sure you want to delete your account? This action cannot be undone.' + ), + t('delete', 'Delete') + ); + + if (confirmed) deleteAccount(); + }, [deleteAccount]); + + return ( +
+

{t('delete_account', 'Delete Account')}

+
+ {t( + 'delete_account_description', + 'Permanently delete your account and all associated data. This cannot be undone.' + )} +
+
+
+
+ {t('are_you_sure', 'Are you sure?')} +
+
+ {t( + 'delete_warning', + 'Deleting your account will remove all data permanently. This cannot be undone.' + )} +
+
+ +
+
+ ); +}; diff --git a/libraries/nestjs-libraries/src/database/prisma/agencies/agencies.repository.ts b/libraries/nestjs-libraries/src/database/prisma/agencies/agencies.repository.ts index 869d72c44..a73003a33 100644 --- a/libraries/nestjs-libraries/src/database/prisma/agencies/agencies.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/agencies/agencies.repository.ts @@ -180,4 +180,11 @@ export class AgenciesRepository { return insertAgency; } + deleteByUserId(userId: string) { + return this._socialMediaAgencies.model.socialMediaAgency.deleteMany({ + where: { + userId, + }, + }); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/marketplace/item.user.repository.ts b/libraries/nestjs-libraries/src/database/prisma/marketplace/item.user.repository.ts index 7381d7c8b..3b843c0d3 100644 --- a/libraries/nestjs-libraries/src/database/prisma/marketplace/item.user.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/marketplace/item.user.repository.ts @@ -38,4 +38,11 @@ export class ItemUserRepository { }, }); } + deleteAllItemsByUser(userId: string) { + return this._itemUser.model.itemUser.deleteMany({ + where: { + userId, + }, + }); +} } diff --git a/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts b/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts index 17873550d..a49bb63ee 100644 --- a/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/marketplace/messages.repository.ts @@ -912,4 +912,10 @@ export class MessagesRepository { }, }); } + + async deletePayoutProblemsByUser(userId: string) { + await this._payoutProblems.model.payoutProblems.deleteMany({ + where: { userId }, + }); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts index 919c7b7af..af9aed15d 100644 --- a/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/organizations/organization.repository.ts @@ -325,4 +325,10 @@ export class OrganizationRepository { }, }); } + + async deleteUserOrganizations(userId: string) { + return this._userOrg.model.userOrganization.deleteMany({ + where: { userId }, + }); +} } diff --git a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts index 8598b8ac1..c4ae3885b 100644 --- a/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/posts/posts.repository.ts @@ -694,4 +694,12 @@ export class PostsRepository { }, }); } + + async deleteCommentsByUser(userId: string) { + return this._comments.model.comments.deleteMany({ + where: { + userId, + }, + }); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts index 2df553384..449c474bc 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.repository.ts @@ -239,4 +239,10 @@ export class UsersRepository { count, }; } + + deleteUser(userId: string) { + return this._user.model.user.delete({ + where: { id: userId }, + }); + } } diff --git a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts index e7580632e..a5cfdf6f8 100644 --- a/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/users/users.service.ts @@ -4,12 +4,20 @@ import { Provider } from '@prisma/client'; import { ItemsDto } from '@gitroom/nestjs-libraries/dtos/marketplace/items.dto'; import { UserDetailDto } from '@gitroom/nestjs-libraries/dtos/users/user.details.dto'; import { OrganizationRepository } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.repository'; +import { ItemUserRepository } from '../marketplace/item.user.repository'; +import { AgenciesRepository } from '../agencies/agencies.repository'; +import { PostsRepository } from '../posts/posts.repository'; +import { MessagesRepository } from '../marketplace/messages.repository'; @Injectable() export class UsersService { constructor( private _usersRepository: UsersRepository, - private _organizationRepository: OrganizationRepository + private _organizationRepository: OrganizationRepository, + private _itemUserRepository: ItemUserRepository, + private _agenciesRepository: AgenciesRepository, + private _postsRepository: PostsRepository, + private _messagesRepository: MessagesRepository ) {} getUserByEmail(email: string) { @@ -55,4 +63,14 @@ export class UsersService { changePersonal(userId: string, body: UserDetailDto) { return this._usersRepository.changePersonal(userId, body); } + + async deleteUser(userId: string){ + //Deleting all models the user has first + await this._organizationRepository.deleteUserOrganizations(userId); + await this._itemUserRepository.deleteAllItemsByUser(userId) + await this._agenciesRepository.deleteByUserId(userId) + await this._postsRepository.deleteCommentsByUser(userId) + await this._messagesRepository.deletePayoutProblemsByUser(userId) + return this._usersRepository.deleteUser(userId) + } }