diff --git a/api/controllers/UserController.ts b/api/controllers/UserController.ts index 921e790d..94ff413d 100644 --- a/api/controllers/UserController.ts +++ b/api/controllers/UserController.ts @@ -1,5 +1,5 @@ import { - JsonController, Params, Get, Post, Patch, UseBefore, UploadedFile, Body, + JsonController, Params, Get, Post, Patch, UseBefore, UploadedFile, Body, Delete, } from 'routing-controllers'; import { UserModel } from '../../models/UserModel'; import UserAccountService from '../../services/UserAccountService'; @@ -14,6 +14,7 @@ import { GetUserResponse, GetCurrentUserResponse, PatchUserResponse, + DeleteUserResponse, } from '../../types'; import { UuidParam } from '../validators/GenericRequests'; import { PatchUserRequest } from '../validators/UserControllerRequests'; @@ -77,4 +78,10 @@ export class UserController { const patchedUser = await this.userAccountService.update(user, patchUserRequest.user); return { error: null, user: patchedUser.getFullUserProfile() }; } + + @Delete() + async deleteAccount(@AuthenticatedUser() user: UserModel): Promise { + await this.userAccountService.delete(user); + return { error: null }; + } } diff --git a/repositories/UserRepository.ts b/repositories/UserRepository.ts index 77fe6bb9..f6104df0 100644 --- a/repositories/UserRepository.ts +++ b/repositories/UserRepository.ts @@ -1,8 +1,9 @@ import { EntityRepository, In } from 'typeorm'; import * as bcrypt from 'bcrypt'; +import FactoryUtils from 'tests/data/FactoryUtils'; import { Activity } from '../types/internal'; import { UserModel } from '../models/UserModel'; -import { Uuid } from '../types'; +import { UserAccessType, UserState, Uuid } from '../types'; import { BaseRepository } from './BaseRepository'; @EntityRepository(UserModel) @@ -82,4 +83,23 @@ export class UserRepository extends BaseRepository { }) .execute(); } + + public async deleteUser(user: UserModel) { + const clearedSensitiveFields: Partial = { + email: `deleted-user-${FactoryUtils.randomHexString()}@ucsd.edu`, + profilePicture: null, + firstName: 'Deleted', + lastName: 'User', + graduationYear: 0, + major: 'Deleted', + points: 0, + credits: 0, + lastLogin: new Date(0), + bio: null, + accessType: UserAccessType.RESTRICTED, + state: UserState.BLOCKED, + }; + const deletedUser = { ...user, ...clearedSensitiveFields }; + return this.repository.save(deletedUser); + } } diff --git a/services/UserAccountService.ts b/services/UserAccountService.ts index a05aefea..b4b9c30b 100644 --- a/services/UserAccountService.ts +++ b/services/UserAccountService.ts @@ -97,6 +97,10 @@ export default class UserAccountService { }); } + public async delete(user: UserModel): Promise { + return this.transactions.readWrite(async (txn) => Repositories.user(txn).deleteUser(user)); + } + public async updateProfilePicture(user: UserModel, profilePicture: string): Promise { return this.transactions.readWrite(async (txn) => Repositories .user(txn) diff --git a/tests/user.test.ts b/tests/user.test.ts new file mode 100644 index 00000000..ecf99a06 --- /dev/null +++ b/tests/user.test.ts @@ -0,0 +1,33 @@ +import { ControllerFactory } from './controllers'; +import { DatabaseConnection, PortalState, UserFactory } from './data'; + +beforeAll(async () => { + await DatabaseConnection.connect(); +}); + +beforeEach(async () => { + await DatabaseConnection.clear(); +}); + +afterAll(async () => { + await DatabaseConnection.clear(); + await DatabaseConnection.close(); +}); + +describe('Delete user account', () => { + test('Deleted user account cannot log in', async () => { + const conn = await DatabaseConnection.get(); + const account = UserFactory.fake(); + + await new PortalState().createUsers(account).write(); + const userController = await ControllerFactory.user(conn); + + const deletedUserResponse = await userController.deleteAccount(account); + + expect(deletedUserResponse).toStrictEqual({ error: null }); + }); + test('Deleted user account is not viewable to other members', async () => { + }); + test('Deleted user account is still counted for event attendance', async () => { + }); +}); diff --git a/types/ApiResponses.ts b/types/ApiResponses.ts index 9d80e861..07235645 100644 --- a/types/ApiResponses.ts +++ b/types/ApiResponses.ts @@ -346,6 +346,8 @@ export interface PatchUserResponse extends ApiResponse { user: PrivateProfile; } +export interface DeleteUserResponse extends ApiResponse {} + export interface GetFeedbackResponse extends ApiResponse { feedback: PublicFeedback[]; }