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

Merge to prod #84

Merged
merged 10 commits into from
Apr 16, 2024
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"`);
}

}
14 changes: 14 additions & 0 deletions src/migrations/1713218553306-makenetidnullable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class makenetidnullable1713218553306 implements MigrationInterface {
name = 'makenetidnullable1713218553306'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" ALTER COLUMN "netid" DROP NOT NULL`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "User" ALTER COLUMN "netid" SET NOT NULL`);
}

}
6 changes: 5 additions & 1 deletion src/models/UserModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class UserModel {
@Column({ unique: true })
username: string;

@Column({ unique: true })
@Column({ unique: true, nullable: true })
netid: string;

@Column()
Expand All @@ -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
Loading