Skip to content

Commit

Permalink
Merge pull request #81 from cuappdev/softdelete
Browse files Browse the repository at this point in the history
Implement Soft Delete + Fix blocking users routes
  • Loading branch information
akmatchev authored Apr 15, 2024
2 parents 0a3fdc4 + 5bf9421 commit 210b976
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 41 deletions.
7 changes: 6 additions & 1 deletion src/api/controllers/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class UserController {
return { user: await this.userService.unblockUser(user, unblockUserRequest) }
}

@Post('id/:id/blocked/')
@Get('blocked/id/:id/')
async getBlockedUsersById(@Params() params: UuidParam): Promise<GetUsersResponse> {
return { users: await this.userService.getBlockedUsersById(params) };
}
Expand All @@ -68,4 +68,9 @@ export class UserController {
async deleteUser(@Params() params: UuidParam, @CurrentUser() user: UserModel): Promise<GetUserResponse> {
return { user: await this.userService.deleteUser(user, params) };
}

@Post('softDelete/id/:id/')
async softDeleteUser(@Params() params: UuidParam): Promise<GetUserResponse> {
return { user: await this.userService.softDeleteUser(params) };
}
}
18 changes: 18 additions & 0 deletions src/migrations/1713139721037-softdelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class softdelete1713139721037 implements MigrationInterface {
name = 'softdelete1713139721037'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" ADD "isActive" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`ALTER TABLE "Post" ALTER COLUMN "original_price" TYPE numeric`);
await queryRunner.query(`ALTER TABLE "Post" ALTER COLUMN "altered_price" TYPE numeric`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "Post" ALTER COLUMN "altered_price" TYPE numeric`);
await queryRunner.query(`ALTER TABLE "Post" ALTER COLUMN "original_price" TYPE numeric`);
await queryRunner.query(`ALTER TABLE "User" DROP COLUMN "isActive"`);
}

}
4 changes: 4 additions & 0 deletions src/models/UserModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export class UserModel {
@Column()
admin: boolean;

@Column({ default: true })
isActive: boolean;

@Column({ type: "numeric", default: 0 })
stars: number;

Expand Down Expand Up @@ -102,6 +105,7 @@ export class UserModel {
email: this.email,
googleId: this.googleId,
bio: this.bio,
isActive: this.isActive,
blocking: this.blocking,
blockers: this.blockers,
posts: this.posts,
Expand Down
48 changes: 32 additions & 16 deletions src/repositories/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ export class UserRepository extends AbstractRepository<UserModel> {
.getOne();
}

public async getBlockedUsersById(id: Uuid): Promise<UserModel | undefined> {
return await this.repository
public async getUserWithBlockedInfo(id: Uuid): Promise<UserModel | undefined> {
return this.repository
.createQueryBuilder("user")
.leftJoinAndSelect("user.blocking", "user_blocking_users.blocking")
.leftJoinAndSelect("user.blockers", "user_blocking_users.blockers")
.where("user.id = :id", { id })
.getOne();
}
Expand Down Expand Up @@ -79,18 +80,28 @@ export class UserRepository extends AbstractRepository<UserModel> {
email: string,
googleId: string,
): Promise<UserModel> {
let existingUser = this.repository
.createQueryBuilder("user")
.where("user.email = :email", { email })
.getOne();
if (await existingUser) throw new ConflictError('UserModel with same email already exists!');

existingUser = this.repository
.createQueryBuilder("user")
.where("user.googleId = :googleId", { googleId })
.getOne();
if (await existingUser) throw new ConflictError('UserModel with same google ID already exists!');

let existingUser = await this.repository
.createQueryBuilder("user")
.where("user.username = :username", { username })
.orWhere("user.netid = :netid", { netid })
.orWhere("user.email = :email", { email })
.orWhere("user.googleId = :googleId", { googleId })
.getOne();
if (existingUser) {
if (existingUser.username === username) {
throw new ConflictError('UserModel with same username already exists!');
}
else if (existingUser.netid === netid)
{
throw new ConflictError('UserModel with same netid already exists!');
}
else if (existingUser.email === email) {
throw new ConflictError('UserModel with same email already exists!');
}
else {
throw new ConflictError('UserModel with same google ID already exists!');
}
}
const adminEmails = process.env.ADMIN_EMAILS?.split(",");
const adminStatus = adminEmails?.includes(email);

Expand Down Expand Up @@ -159,9 +170,14 @@ export class UserRepository extends AbstractRepository<UserModel> {
if (!blocker.blocking.find((user) => user.id === blocked.id)) {
throw new NotFoundError("User has not been blocked!")
}
blocker.blocking.splice(blocker.blocking.indexOf(blocked), 1);
if (blocker.blocking.length === 0) { blocker.blocking = undefined; }
// remove blocked user from blocking list
blocker.blocking = blocker.blocking.filter((user) => user.id !== blocked.id);
}
return this.repository.save(blocker);
}

public async softDeleteUser(user: UserModel): Promise<UserModel> {
user.isActive = false;
return this.repository.save(user);
}
}
6 changes: 4 additions & 2 deletions src/services/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,11 @@ export class AuthService {
if (emailIndex === -1 && !adminEmails?.includes(authRequest.user.email)) {
throw new UnauthorizedError('Non-Cornell email used!');
}

if (process.env.OAUTH_ANDROID_CLIENT && process.env.OAUTH_IOS_ID) {
// verifies info using id token
const ticket = await client.verifyIdToken({
idToken: authRequest.idToken,
});

const payload = ticket.getPayload();

if (payload) {
Expand All @@ -68,6 +66,10 @@ export class AuthService {
user = await userRepository.createUser(netid, netid, newUser.givenName, newUser.familyName,
newUser.photoUrl, newUser.email, userId);
}
//check if the user is inactive/soft deleted
if (!user.isActive) {
throw new ForbiddenError("User is soft deleted");
}
//add device token
const session = await sessionsRepository.createSession(user);
sessionsRepository.updateSessionDeviceToken(session, authRequest.deviceToken)
Expand Down
20 changes: 14 additions & 6 deletions src/services/PostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export class PostService {
public async getAllPosts(): Promise<PostModel[]> {
return this.transactions.readOnly(async (transactionalEntityManager) => {
const postRepository = Repositories.post(transactionalEntityManager);
return await postRepository.getAllPosts();
// filter out posts from inactive users
return (await postRepository.getAllPosts()).filter((post) => post.user?.isActive);
});
}

Expand All @@ -33,6 +34,7 @@ export class PostService {
const postRepository = Repositories.post(transactionalEntityManager);
const post = await postRepository.getPostById(params.id);
if (!post) throw new NotFoundError('Post not found!');
if (!post.user?.isActive) throw new NotFoundError('User is not active!');
return post;
});
}
Expand All @@ -42,6 +44,7 @@ export class PostService {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getUserById(params.id)
if (!user) throw new NotFoundError('User not found!');
if (!user.isActive) throw new NotFoundError('User is not active!');
const postRepository = Repositories.post(transactionalEntityManager);
const posts = await postRepository.getPostsByUserId(params.id);
return posts;
Expand All @@ -53,6 +56,7 @@ export class PostService {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getUserById(post.userId);
if (!user) throw new NotFoundError('User not found!');
if (!user.isActive) throw new NotFoundError('User is not active!');
const postRepository = Repositories.post(transactionalEntityManager);
const images: string[] = [];
for (const imageBase64 of post.imagesBase64) {
Expand Down Expand Up @@ -112,30 +116,30 @@ export class PostService {
posts.push(pd);
}
});
return posts;
return posts.filter((post) => post.user?.isActive);
});
}

public async filterPosts(filterPostsRequest: FilterPostsRequest): Promise<PostModel[]> {
return this.transactions.readOnly(async (transactionalEntityManager) => {
const postRepository = Repositories.post(transactionalEntityManager);
const posts = await postRepository.filterPosts(filterPostsRequest.category);
return posts;
return posts.filter((post) => post.user?.isActive);
});
}

public async filterPostsByPrice(filterPostsByPriceRequest: FilterPostsByPriceRequest): Promise<PostModel[]> {
return this.transactions.readOnly(async (transactionalEntityManager) => {
const postRepository = Repositories.post(transactionalEntityManager);
const posts = await postRepository.filterPostsByPrice(filterPostsByPriceRequest.lowerBound, filterPostsByPriceRequest.upperBound)
return posts;
return posts.filter((post) => post.user?.isActive);
})
}

public async getArchivedPosts(): Promise<PostModel[]> {
return this.transactions.readOnly(async (transactionalEntityManager) => {
const postRepository = Repositories.post(transactionalEntityManager);
return await postRepository.getArchivedPosts();
return (await postRepository.getArchivedPosts()).filter((post) => post.user?.isActive);
});
}

Expand All @@ -144,6 +148,7 @@ export class PostService {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getUserById(params.id)
if (!user) throw new NotFoundError('User not found!');
if (!user.isActive) throw new NotFoundError('User is not active!');
const postRepository = Repositories.post(transactionalEntityManager);
const posts = await postRepository.getArchivedPostsByUserId(params.id);
return posts;
Expand All @@ -155,6 +160,7 @@ export class PostService {
const postRepository = Repositories.post(transactionalEntityManager);
const post = await postRepository.getPostById(params.id);
if (!post) throw new NotFoundError('Post not found!');
if (post.user.isActive == false) throw new NotFoundError('User is not active!');
if (user.id != post.user?.id) throw new ForbiddenError('User is not poster!');
return await postRepository.archivePost(post);
});
Expand All @@ -174,6 +180,7 @@ export class PostService {
const postRepository = Repositories.post(transactionalEntityManager);
const post = await postRepository.getPostById(params.id);
if (!post) throw new NotFoundError('Post not found!');
if (post.user.isActive == false) throw new NotFoundError('User is not active!');
const userRepository = Repositories.user(transactionalEntityManager);
return await userRepository.savePost(user, post);
});
Expand All @@ -184,6 +191,7 @@ export class PostService {
const postRepository = Repositories.post(transactionalEntityManager);
const post = await postRepository.getPostById(params.id);
if (!post) throw new NotFoundError('Post not found!');
if (post.user.isActive == false) throw new NotFoundError('User is not active!');
const userRepository = Repositories.user(transactionalEntityManager);
return await userRepository.unsavePost(user, post);
});
Expand Down Expand Up @@ -248,7 +256,7 @@ export class PostService {
});
}
}
return posts
return posts.filter((post) => post.user?.isActive);
});
}
}
Expand Down
41 changes: 32 additions & 9 deletions src/services/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class UserService {
if (!user.admin) throw new UnauthorizedError('User does not have permission to get all users')
return this.transactions.readOnly(async (transactionalEntityManager) => {
const userRepository = Repositories.user(transactionalEntityManager);
return userRepository.getAllUsers();
return (await userRepository.getAllUsers()).filter((user) => user.isActive);
});
}

Expand All @@ -30,6 +30,7 @@ export class UserService {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getUserById(params.id);
if (!user) throw new NotFoundError('User not found!');
if (!user.isActive) throw new NotFoundError('User is not active!');
return user;
});
}
Expand All @@ -39,6 +40,7 @@ export class UserService {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getUserByGoogleId(id);
if (!user) throw new NotFoundError('User not found!');
if (!user.isActive) throw new NotFoundError('User is not active!');
return user;
});
}
Expand All @@ -48,6 +50,7 @@ export class UserService {
const postRepository = Repositories.post(transactionalEntityManager);
const user = await postRepository.getUserByPostId(params.id);
if (!user) throw new NotFoundError('Post not found!');
if (!user.isActive) throw new NotFoundError('User is not active!');
return user;
});
}
Expand All @@ -57,6 +60,7 @@ export class UserService {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getUserByEmail(email);
if (!user) throw new NotFoundError('User not found!');
if (!user.isActive) throw new NotFoundError('User is not active!');
return user;
});
}
Expand Down Expand Up @@ -93,33 +97,43 @@ export class UserService {
if (user.id === blockUserRequest.blocked) {
throw new UnauthorizedError('User cannot block themselves!');
}
if (user.blocking?.find((blockedUser) => blockedUser.id === blockUserRequest.blocked)) {
if (!user.isActive) throw new UnauthorizedError('User is not active!');
const joinedUser = await userRepository.getUserWithBlockedInfo(user.id);
if (joinedUser?.blocking?.find((blockedUser) => blockedUser.id === blockUserRequest.blocked)) {
throw new UnauthorizedError('User is already blocked!');
}
const blocked = await userRepository.getUserById(blockUserRequest.blocked);
if (!blocked) throw new NotFoundError('Blocked user not found!');
return userRepository.blockUser(user, blocked);
if (!joinedUser) throw new NotFoundError('Joined user not found!');
return userRepository.blockUser(joinedUser, blocked);
});
}

public async unblockUser(user: UserModel, blockUserRequest: UnblockUserRequest): Promise<UserModel> {
public async unblockUser(user: UserModel, unblockUserRequest: UnblockUserRequest): Promise<UserModel> {
return this.transactions.readWrite(async (transactionalEntityManager) => {
const userRepository = Repositories.user(transactionalEntityManager);
const blocked = await userRepository.getUserById(blockUserRequest.unblocked);
const blocked = await userRepository.getUserById(unblockUserRequest.unblocked);
if (!blocked) throw new NotFoundError('Blocked user not found!');
if (!user.blocking?.find((blockedUser) => blockedUser.id === blockUserRequest.unblocked)) {
if (user.id === unblockUserRequest.unblocked) {
throw new UnauthorizedError('User cannot unblock themselves!');
}
if (!user.isActive) throw new UnauthorizedError('User is not active!');
const joinedUser = await userRepository.getUserWithBlockedInfo(user.id);
if (!joinedUser) throw new NotFoundError('Joined user not found!');
if (!joinedUser.blocking?.find((blockedUser) => blockedUser.id === unblockUserRequest.unblocked)) {
throw new UnauthorizedError('User is not blocked!');
}
return userRepository.unblockUser(user, blocked);
return userRepository.unblockUser(joinedUser, blocked);
});
}

public async getBlockedUsersById(params: UuidParam): Promise<UserModel[]> {
return this.transactions.readOnly(async (transactionalEntityManager) => {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getBlockedUsersById(params.id);
const user = await userRepository.getUserWithBlockedInfo(params.id);
if (!user) throw new NotFoundError('User not found!');
return user.blocking ?? [];
// get user.blocking and filter out inactive users, else return empty array
return user.blocking?.filter((blockedUser) => blockedUser.isActive) ?? [];
});
}

Expand All @@ -134,4 +148,13 @@ export class UserService {
return userRepository.deleteUser(userToDelete);
});
}

public async softDeleteUser(params: UuidParam): Promise<UserModel> {
return this.transactions.readWrite(async (transactionalEntityManager) => {
const userRepository = Repositories.user(transactionalEntityManager);
const user = await userRepository.getUserById(params.id);
if (!user) throw new NotFoundError('User not found!');
return userRepository.softDeleteUser(user);
});
}
}
Loading

0 comments on commit 210b976

Please sign in to comment.