From c09b56b0cd53df8c98609e1142f78501a76f29eb Mon Sep 17 00:00:00 2001 From: Pau Costa Date: Sun, 4 Feb 2024 12:30:11 +0100 Subject: [PATCH] :sparkles: Comments, likes, and notifications Signed-off-by: Pau Costa --- server/src/controller/postController.ts | 235 ++++++++++++++++++++++++ server/src/entity/Comment.ts | 6 +- server/src/entity/Notification.ts | 2 +- server/src/entity/Post.ts | 17 +- server/src/entity/User.ts | 3 + server/src/index.ts | 2 + server/src/routes/postRoutes.ts | 18 ++ 7 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 server/src/controller/postController.ts create mode 100644 server/src/routes/postRoutes.ts diff --git a/server/src/controller/postController.ts b/server/src/controller/postController.ts new file mode 100644 index 0000000..10f18c5 --- /dev/null +++ b/server/src/controller/postController.ts @@ -0,0 +1,235 @@ +import {AppDataSource} from "../data-source"; +import {User} from "../entity/User"; +import {Post} from "../entity/Post"; +import {Comment} from "../entity/Comment"; +import {Notification} from "../entity/Notification"; +import {catchAsync} from "../util/catchAsync"; +import {AppError} from "../util/AppError"; +import {AppRequest} from "../util/AppRequest"; + +export class PostController { + private postRepository = AppDataSource.getRepository(Post) + + private commentRepository = AppDataSource.getRepository(Comment) + + private notificationRepository = AppDataSource.getRepository(Notification) + + private userRepository = AppDataSource.getRepository(User) + + public getAllPosts = catchAsync(async (_req, res, _next) => { + const posts = await this.postRepository.find({relations: {createdBy: true}}) + + // Remove sensitive fields + posts.forEach(post => { + post.deleteSensitiveFields() + }) + + res.status(200).send(posts) + }) + + public getPost = catchAsync(async (req, res, next) => { + const { post, errorMessage} = + await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID'){ + return next(new AppError('No post found with that ID', 404)) + } + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.send(post) + }) + + public getFollowedPosts = catchAsync(async (req : AppRequest, res, _next) => { + const user = await this.userRepository.findOne({ + where: {id: req.user.id}, + relations: {followed: true, posts: true}}) + + const followedPosts = user.followed.map(followedUser => followedUser.posts).flat() + + // Remove sensitive fields + followedPosts.forEach(post => { + post.deleteSensitiveFields() + }) + + res.send(followedPosts) + }) + + public createPost = catchAsync(async (req : AppRequest, res, next) => { + const user = await this.userRepository.findOne({where: {id: req.user.id}}) + const {title, content} = req.body + + if(!title || !content) return next(new AppError('Title and content are required', 400)) + + + const newPost = Object.assign(new Post(), { + title, + content, + createdBy: user, + createdAt: new Date()}) + + const post = await this.postRepository.save(newPost) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(201).send(post) + }) + + public updatePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + const {title, content} = req.body + + if(!title || !content) return next(new AppError('Title and content are required', 400)) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID'){ + return next(new AppError('No post found with that ID', 404)) + } + + if(post.createdBy.id !== user.id){ + return next(new AppError('You are not authorized to update this post', 403)) + } + + post.title = title + post.content = content + await this.postRepository.save(post) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(200).send(post) + }) + + public deletePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404)) + + if(post.createdBy.id !== user.id){ + return next(new AppError('You are not authorized to delete this post', 403)) + } + + await this.postRepository.remove(post) + res.status(204).send() + }) + + public likePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID'){ + return next(new AppError('No post found with that ID', 404)) + } + + // Check if user has already liked the post + if(post.likedBy.some(likedUser => likedUser.id === user.id)){ + return next(new AppError('You have already liked this post', 400)) + } + + post.likedBy.push(user) + await this.postRepository.save(post) + + // Send notification to post creator + const newNotification = Object.assign(new Notification(),{ + message: `${user.firstName} ${user.lastName} liked your post`, + belongsTo: post.createdBy, + timeStamp: new Date() + }) + const notification = await this.notificationRepository.save(newNotification) + post.createdBy.notifications.push(notification) + await this.userRepository.save(post.createdBy) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(200).send(post) + }) + + public unlikePost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID') return next(new AppError('No post found with that ID', 404)) + + // Check if user likes the post + if(!post.likedBy.some(likedUser => likedUser.id === user.id)){ + return next(new AppError('You have not liked this post', 400)) + } + + post.likedBy = post.likedBy.filter(likedUser => likedUser.id !== user.id) + await this.postRepository.save(post) + + // Remove sensitive fields + post.deleteSensitiveFields() + + res.status(200).send(post) + }) + + public commentPost = catchAsync(async (req : AppRequest, res, next) => { + const {user, post, errorMessage} = await this.validateRequestAndGetEntities(req) + + if(errorMessage == 'Invalid ID') return next(new AppError('Invalid ID', 400)) + if(errorMessage == 'No post found with that ID') { + return next(new AppError('No post found with that ID', 404)) + } + + const {content} = req.body + const newComment = Object.assign(new Comment(), { + content, + createdBy: user, + post, + createdAt: new Date() + }) + + const comment = await this.commentRepository.save(newComment) + + // Send notification to post creator + const newNotification = Object.assign(new Notification(),{ + message: `${user.firstName} ${user.lastName} commented on your post`, + belongsTo: post.createdBy, + timeStamp: new Date() + }) + await this.notificationRepository.save(newNotification) + + // Remove sensitive fields + comment.createdBy.deleteSensitiveFields() + post.deleteSensitiveFields() + + res.status(201).send(comment) + }) + + + private validateRequestAndGetEntities = async (req : AppRequest,) : Promise => { + const postId = req.params.id + const parsedId = parseInt(postId) + let errorMessage: 'Invalid ID' | 'No post found with that ID' + + // Check if ID is a number + if(isNaN(parsedId)) errorMessage = 'Invalid ID' + + const user = req.user + const post = await this.postRepository.findOne({ + where: {id: parsedId}, + relations: { + likedBy: true, + comments: { createdBy: true }, + createdBy: {notifications: true} + } + }) + + // Check if post exists + if(!post) errorMessage = 'No post found with that ID' + + return {user, post, errorMessage} + + } +} +interface validatedEntities { + user: User + post: Post + errorMessage?: 'Invalid ID' | 'No post found with that ID' +} diff --git a/server/src/entity/Comment.ts b/server/src/entity/Comment.ts index c7bd23f..2e6ccf0 100644 --- a/server/src/entity/Comment.ts +++ b/server/src/entity/Comment.ts @@ -1,5 +1,6 @@ import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; import {User} from "./User"; +import {Post} from "./Post"; @Entity() export class Comment { @@ -12,9 +13,12 @@ export class Comment { @Column() createdAt: Date - @ManyToOne(() => User, user => user.posts) + @ManyToOne(() => User, user => user.comments) createdBy: User + @ManyToOne(() => Post, post => post.comments) + post: Post + @ManyToMany(()=> User) @JoinTable() likedBy: User[] diff --git a/server/src/entity/Notification.ts b/server/src/entity/Notification.ts index 8308d1a..8e06d47 100644 --- a/server/src/entity/Notification.ts +++ b/server/src/entity/Notification.ts @@ -13,7 +13,7 @@ export class Notification { @Column() timeStamp: Date - @Column() + @Column({default: false}) seen: boolean @ManyToOne(type => User, user => user.notifications) diff --git a/server/src/entity/Post.ts b/server/src/entity/Post.ts index f9a9847..d7e20d2 100644 --- a/server/src/entity/Post.ts +++ b/server/src/entity/Post.ts @@ -1,5 +1,6 @@ -import {Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; +import {Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn} from "typeorm"; import {User} from "./User"; +import {Comment} from "./Comment"; @Entity() export class Post { @@ -18,8 +19,22 @@ export class Post { @ManyToOne(() => User, user => user.posts) createdBy: User + @OneToMany(() => Comment, comment => comment.post) + comments: Comment[] + @ManyToMany(()=> User) @JoinTable() likedBy: User[] + public deleteSensitiveFields(){ + this.createdBy.deleteSensitiveFields() + if(this.likedBy){ + this.likedBy.forEach(user => user.deleteSensitiveFields()) + } + if(this.comments){ + this.comments.forEach(comment => comment.createdBy.deleteSensitiveFields()) + } + + } + } \ No newline at end of file diff --git a/server/src/entity/User.ts b/server/src/entity/User.ts index f6b6a1c..d2b9705 100644 --- a/server/src/entity/User.ts +++ b/server/src/entity/User.ts @@ -51,5 +51,8 @@ export class User { public deleteSensitiveFields(){ delete this.password delete this.email + delete this.notifications + delete this.followed + delete this.followers } } diff --git a/server/src/index.ts b/server/src/index.ts index d5eb5ec..4882f4d 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -5,6 +5,7 @@ import {errorHandler} from "./controller/errorController"; import {AuthRoutes} from "./routes/authRoutes"; import {UserRoutes} from "./routes/userRoutes"; import {AuthController} from "./controller/authController"; +import {PostRoutes} from "./routes/postRoutes"; AppDataSource.initialize().then(async () => { @@ -20,6 +21,7 @@ AppDataSource.initialize().then(async () => { app.use(authController.protect) app.use('/users', UserRoutes) + app.use('/posts', PostRoutes) // setup express app here app.use(errorHandler) diff --git a/server/src/routes/postRoutes.ts b/server/src/routes/postRoutes.ts new file mode 100644 index 0000000..4ea76f8 --- /dev/null +++ b/server/src/routes/postRoutes.ts @@ -0,0 +1,18 @@ +import {PostController} from "../controller/postController"; +import {Router} from "express"; + +const postController = new PostController() + +export const PostRoutes = Router(); + +PostRoutes.route("/") + .get(postController.getAllPosts) + .post(postController.createPost) +PostRoutes.route("/followed").get(postController.getFollowedPosts) +PostRoutes.route("/:id") + .get(postController.getPost) + .patch(postController.updatePost) + .delete(postController.deletePost) +PostRoutes.route("/:id/like").post(postController.likePost) +PostRoutes.route("/:id/unlike").post(postController.unlikePost) +PostRoutes.route("/:id/comment").post(postController.commentPost) \ No newline at end of file