diff --git a/server/src/controller/UserController.ts b/server/src/controller/UserController.ts index 339a75b..fec29b0 100644 --- a/server/src/controller/UserController.ts +++ b/server/src/controller/UserController.ts @@ -1,53 +1,137 @@ import { AppDataSource } from "../data-source" import { NextFunction, Request, Response } from "express" import { User } from "../entity/User" +import {AppError} from "../util/AppError"; +import {AppRequest} from "../util/AppRequest"; +import {Notification} from "../entity/Notification"; +import {catchAsync} from "../util/catchAsync"; export class UserController { private userRepository = AppDataSource.getRepository(User) - async all(request: Request, response: Response, next: NextFunction) { - return this.userRepository.find() - } + private notificationRepository = AppDataSource.getRepository(Notification) + public getAllUsers = catchAsync(async (req: Request, res: Response, next: NextFunction) => { + const users = await this.userRepository.find() - async one(request: Request, response: Response, next: NextFunction) { - const id = parseInt(request.params.id) + // remove sensitive fields + users.forEach(user => { + user.deleteSensitiveFields() + }) + + res.status(200).send(users) + }) + + public getUser = catchAsync( async (req: Request, res: Response, next: NextFunction) => { + const id = req.params.id + const parsedId = parseInt(id) + // Check if ID is a number + if(isNaN(parsedId)) return next(new AppError('Invalid ID', 400)) + + const user = await this.userRepository.findOne({where: {id: parsedId}, relations:{ + followed: true, + followers: true, + posts: true, + comments: true, + }}) + if(!user) return next(new AppError('No user found with that ID', 404)) + + // remove sensitive fields + user.deleteSensitiveFields() + user.followed.forEach(followedUser => followedUser.deleteSensitiveFields()) + user.followers.forEach(follower => follower.deleteSensitiveFields()) + + return res.send(user) + }) + + public getMe = catchAsync(async (req: AppRequest, res: Response, next: NextFunction) => { const user = await this.userRepository.findOne({ - where: { id } + where: {id: req.user.id}, + relations:{ + followed: true, + followers: true, + posts: true, + comments: true, + } }) - if (!user) { - return "unregistered user" + user.followed.forEach(followedUser => followedUser.deleteSensitiveFields()) + user.followers.forEach(follower => follower.deleteSensitiveFields()) + + return res.status(200).send(user) + }) + + public followUser = catchAsync(async (req: AppRequest, res: Response, next: NextFunction) => { + const userToFollowId = req.params.id + const parsedId = parseInt(userToFollowId) + // Check if ID is a number + if(isNaN(parsedId)) return next(new AppError('Invalid ID', 400)) + + const user = req.user + const userToFollow = await this.userRepository.findOne({ + where: {id: parsedId}, + relations:{followed: true, followers: true, notifications: true}} + ) + + if(!userToFollow) return next(new AppError('No user found with that ID', 404)) + + // Check if user is already following + if(user.followed.some(followedUser => followedUser.id === userToFollow.id)){ + return next(new AppError('You are already following this user', 400)) } - return user - } + // Follow the user + user.followed.push(userToFollow) + await this.userRepository.save(user) + // Add the requesting user to the followers of the user being followed + userToFollow.followers.push(user) + // Create a notification for the user being followed + const followNotification = Object.assign(new Notification(),{ + seen: false, + message: `${user.firstName} is now following you`, + timeStamp: new Date() + }) + userToFollow.notifications.push( + await this.notificationRepository.save(followNotification) + ) + await this.userRepository.save(userToFollow) - async save(request: Request, response: Response, next: NextFunction) { - const { firstName, lastName, age } = request.body; - const user = Object.assign(new User(), { - firstName, - lastName, - age + return res.status(200).send({ + status: 'success', + message: `You are now following ${userToFollow.firstName}` + }) + }) + + public unfollowUser = catchAsync(async (req: AppRequest, res: Response, next: NextFunction) => { + const userToUnfollowId = req.params.id + const parsedId = parseInt(userToUnfollowId) + // Check if ID is a number + if(isNaN(parsedId)) return next(new AppError('Invalid ID', 400)) + + const user = req.user + const userToUnfollow = await this.userRepository.findOne({ + where: {id: parsedId}, + relations: {followed: true, followers: true} }) - return this.userRepository.save(user) - } - - async remove(request: Request, response: Response, next: NextFunction) { - const id = parseInt(request.params.id) - - let userToRemove = await this.userRepository.findOneBy({ id }) - - if (!userToRemove) { - return "this user not exist" + if(!userToUnfollow) return next(new AppError('No user found with that ID', 404)) + // Check if user is following + if(!user.followed.some(followedUser => followedUser.id === userToUnfollow.id)){ + return next(new AppError('You are not following this user', 400)) } + // Unfollow the user + user.followed = user.followed.filter(followedUser => followedUser.id !== userToUnfollow.id) + await this.userRepository.save(user) - await this.userRepository.remove(userToRemove) + userToUnfollow.followers = userToUnfollow.followers.filter(follower => follower.id !== user.id) + await this.userRepository.save(userToUnfollow) - return "user has been removed" - } + return res.status(200).send({ + status: 'success', + message: `You are no longer following ${userToUnfollow.firstName}` + }) + }) } \ No newline at end of file diff --git a/server/src/controller/authController.ts b/server/src/controller/authController.ts index 50af6dd..2f5f13f 100644 --- a/server/src/controller/authController.ts +++ b/server/src/controller/authController.ts @@ -106,7 +106,10 @@ export class AuthController { // Verify the token const decoded = jwt.verify(token, this.jwt_secret) as JwtPayload // Check if the user still exists - const candidateUser = await this.userRepository.findOne({where: {id: decoded.id}}) + const candidateUser = await this.userRepository.findOne({ + where: {id: decoded.id}, + relations: {followed: true, followers: true, notifications: true} + }) if(!candidateUser){ return next(new AppError('The user belonging to this token no longer exists', 401)) } diff --git a/server/src/data-source.ts b/server/src/data-source.ts index 879c428..6d37e96 100644 --- a/server/src/data-source.ts +++ b/server/src/data-source.ts @@ -3,6 +3,7 @@ import { DataSource } from "typeorm" import { User } from "./entity/User" import {Comment} from "./entity/Comment"; import {Post} from "./entity/Post"; +import {Notification} from "./entity/Notification"; export const AppDataSource = new DataSource({ type: "mysql", @@ -13,7 +14,7 @@ export const AppDataSource = new DataSource({ database: process.env.MYSQL_DATABASE, synchronize: true, logging: false, - entities: [User, Comment, Post], + entities: [User, Comment, Post, Notification], migrations: [], subscribers: [], }) diff --git a/server/src/entity/Notification.ts b/server/src/entity/Notification.ts new file mode 100644 index 0000000..8308d1a --- /dev/null +++ b/server/src/entity/Notification.ts @@ -0,0 +1,21 @@ +import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from "typeorm"; +import {User} from "./User"; + +@Entity() +export class Notification { + + @PrimaryGeneratedColumn() + id: number + + @Column() + message: string + + @Column() + timeStamp: Date + + @Column() + seen: boolean + + @ManyToOne(type => User, user => user.notifications) + belongsTo: User +} \ No newline at end of file diff --git a/server/src/entity/User.ts b/server/src/entity/User.ts index 2927f71..f6b6a1c 100644 --- a/server/src/entity/User.ts +++ b/server/src/entity/User.ts @@ -2,6 +2,7 @@ import {Entity, PrimaryGeneratedColumn, Column, ManyToMany, OneToMany, JoinTable import * as bcrypt from "bcrypt" import {Post} from "./Post"; import {Comment} from "./Comment"; +import {Notification} from "./Notification"; @Entity() export class User { @@ -25,12 +26,19 @@ export class User { @JoinTable() followed: User[] + @ManyToMany(type => User) + @JoinTable() + followers: User[] + @OneToMany(type => Post, post=> post.createdBy) posts: Post[] @OneToMany(type => Comment, comment=> comment.createdBy) comments: Comment[] + @OneToMany(type => Notification, notification => notification.belongsTo) + notifications: Notification[] + static async hashPassword(password: string){ return await bcrypt.hash(password, parseInt(process.env.SALT_ROUNDS)) @@ -39,4 +47,9 @@ export class User { async comparePassword(password: string){ return await bcrypt.compare(password, this.password) } + + public deleteSensitiveFields(){ + delete this.password + delete this.email + } } diff --git a/server/src/index.ts b/server/src/index.ts index 71bccab..d5eb5ec 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,6 +3,8 @@ import * as bodyParser from "body-parser" import { AppDataSource } from "./data-source" import {errorHandler} from "./controller/errorController"; import {AuthRoutes} from "./routes/authRoutes"; +import {UserRoutes} from "./routes/userRoutes"; +import {AuthController} from "./controller/authController"; AppDataSource.initialize().then(async () => { @@ -13,6 +15,11 @@ AppDataSource.initialize().then(async () => { // register express routes from defined application routes // Auth Routes app.use('/auth', AuthRoutes) + // All routes after this one require authentication + const authController = new AuthController(); + app.use(authController.protect) + + app.use('/users', UserRoutes) // setup express app here app.use(errorHandler) diff --git a/server/src/routes/authRoutes.ts b/server/src/routes/authRoutes.ts index 7762739..2731ad7 100644 --- a/server/src/routes/authRoutes.ts +++ b/server/src/routes/authRoutes.ts @@ -7,6 +7,7 @@ export const AuthRoutes = Router(); AuthRoutes.route("/signup").post(authController.handleSignUp) AuthRoutes.route("/login").post(authController.handleLogin) +AuthRoutes.route("/logout").get(authController.handleLogout) diff --git a/server/src/routes/userRoutes.ts b/server/src/routes/userRoutes.ts new file mode 100644 index 0000000..cd268dc --- /dev/null +++ b/server/src/routes/userRoutes.ts @@ -0,0 +1,12 @@ +import {Router} from "express"; +import {UserController} from "../controller/UserController"; + +export const UserRoutes = Router(); + +const userController = new UserController() + +UserRoutes.route("/").get(userController.getAllUsers) +UserRoutes.route("/me").get(userController.getMe) +UserRoutes.route("/:id").get(userController.getUser) +UserRoutes.route("/follow/:id").post(userController.followUser) +UserRoutes.route("/unfollow/:id").post(userController.unfollowUser) \ No newline at end of file