├── logs └── error.log ├── .gitignore ├── .DS_Store ├── src ├── interfaces │ ├── story.interface.ts │ ├── chat.interface.ts │ ├── user.interface.ts │ └── post.interface.ts ├── services │ ├── story.service.ts │ ├── chat.service.ts │ ├── user.service.ts │ └── post.service.ts ├── models │ ├── conversation.model.ts │ ├── follower.model.ts │ ├── following.model.ts │ ├── savedpost.model.ts │ ├── message.model.ts │ ├── comment.model.ts │ ├── post.model.ts │ ├── stories.model.ts │ └── user.model.ts ├── routes │ ├── chat.routes.ts │ ├── story.routes.ts │ ├── message.routes.ts │ ├── auth.routes.ts │ ├── user.routes.ts │ └── post.routes.ts ├── database │ └── db.ts ├── environments │ └── endpoints.config.ts ├── middleware │ ├── errorhandler.ts │ ├── debughandler.ts │ └── isAuthorized.ts ├── jobs │ └── story.jobs.ts ├── controllers │ ├── socket.ts │ ├── auth.ts │ ├── chats.ts │ ├── message.ts │ ├── story.ts │ ├── user.ts │ └── post.ts ├── lib │ └── logger.ts └── app.ts ├── vercel.json ├── package.json ├── README.md └── tsconfig.json /logs/error.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | .DS_Store 4 | build/ -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LawrenceY-B/Friendwave/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/interfaces/story.interface.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "mongoose"; 2 | 3 | export interface IStories { 4 | storyId: string; 5 | userId: Schema.Types.ObjectId; 6 | postId: Schema.Types.ObjectId; 7 | expireAt: Date; 8 | } -------------------------------------------------------------------------------- /src/services/story.service.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request } from "express"; 2 | import joi from "joi"; 3 | 4 | export const validateStory = (details: Request) => { 5 | const schema = joi.object({ 6 | postID: joi.string().min(6), 7 | }); 8 | return schema.validate(details); 9 | }; -------------------------------------------------------------------------------- /src/services/chat.service.ts: -------------------------------------------------------------------------------- 1 | import joi from "joi"; 2 | import { Request } from "express"; 3 | 4 | export const validateChat = (data: Request) => { 5 | const schema = joi.object({ 6 | receiverID: joi.string().min(6).required(), 7 | }); 8 | return schema.validate(data); 9 | } -------------------------------------------------------------------------------- /src/interfaces/chat.interface.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "mongoose"; 2 | 3 | export interface IChats{ 4 | members:Schema.Types.ObjectId[]; 5 | } 6 | export interface IMessage{ 7 | chatId:any; 8 | senderId:Schema.Types.ObjectId; 9 | message:string; 10 | dateTime:Date; 11 | } -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "friendwave-backend", 4 | "builds": [ 5 | { 6 | "src": "/build/app.js", 7 | "use": "@vercel/node" 8 | } 9 | ], 10 | "routes": [ 11 | { 12 | "src": "/(.*)", 13 | "dest": "/build/app.js" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/models/conversation.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IChats } from "../interfaces/chat.interface"; 3 | 4 | const ChatsSchema = new Schema( 5 | { 6 | members: [{ type: Schema.Types.ObjectId }], 7 | }, 8 | { timestamps: true } 9 | ); 10 | 11 | const Chat = model("Chats", ChatsSchema); 12 | 13 | export default Chat; -------------------------------------------------------------------------------- /src/models/follower.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IFollower } from "../interfaces/user.interface"; 3 | 4 | const FollowerSchema = new Schema({ 5 | userID: { type: Schema.Types.ObjectId, required: true }, 6 | followerID: { type: Schema.Types.ObjectId,ref:"User", required:true} 7 | }); 8 | 9 | const Follower = model("Follower", FollowerSchema); 10 | 11 | export default Follower -------------------------------------------------------------------------------- /src/models/following.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IFollowing } from "../interfaces/user.interface"; 3 | 4 | const FollowingSchema = new Schema({ 5 | userID: { type: Schema.Types.ObjectId, required: true }, 6 | followingID: { type: Schema.Types.ObjectId, ref:"User", required:true} 7 | }); 8 | 9 | const Following = model("Following", FollowingSchema); 10 | 11 | export default Following -------------------------------------------------------------------------------- /src/routes/chat.routes.ts: -------------------------------------------------------------------------------- 1 | import {Router} from "express"; 2 | import { verifyToken } from "../middleware/isAuthorized"; 3 | import { deleteChat, getAllChats, newChat } from "../controllers/chats"; 4 | 5 | const ChatRoutes = Router(); 6 | 7 | ChatRoutes.post("/", verifyToken, newChat); 8 | ChatRoutes.get("/allchats", verifyToken, getAllChats); 9 | ChatRoutes.delete("/deletechat/:id", verifyToken, deleteChat) 10 | 11 | export default ChatRoutes; -------------------------------------------------------------------------------- /src/routes/story.routes.ts: -------------------------------------------------------------------------------- 1 | import {Router} from "express"; 2 | import { verifyToken } from "../middleware/isAuthorized"; 3 | import { addtoStory, deleteStory, getStory } from "../controllers/story"; 4 | 5 | const StoryRoutes = Router(); 6 | 7 | StoryRoutes.post('/addstory',verifyToken, addtoStory) 8 | StoryRoutes.delete('/deletestory',verifyToken, deleteStory) 9 | StoryRoutes.get('/getstory',verifyToken, getStory) 10 | 11 | 12 | export default StoryRoutes; -------------------------------------------------------------------------------- /src/models/savedpost.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { ISavedPost } from "../interfaces/post.interface"; 3 | 4 | const SavedPostSchema = new Schema({ 5 | postId: { type: Schema.Types.ObjectId, ref: "Posts", required: true }, 6 | userId: { type: Schema.Types.ObjectId, ref: "User", required: true }, 7 | }); 8 | 9 | const SavedPost = model("SavedPosts", SavedPostSchema); 10 | 11 | export default SavedPost; 12 | -------------------------------------------------------------------------------- /src/database/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { environment } from "../environments/endpoints.config"; 3 | import Logger from "../lib/logger"; 4 | 5 | 6 | export const DB_Connection = async () => { 7 | mongoose 8 | .connect(`${environment.DBUrl}`, {}) 9 | .then(() => { 10 | Logger.debug("MongoDB connected!!"); 11 | }) 12 | .catch((err: Error) => { 13 | Logger.error("Failed to connect to MongoDB", err); 14 | }); 15 | }; 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/models/message.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IMessage } from "../interfaces/chat.interface"; 3 | 4 | const MessageSchema = new Schema( 5 | { 6 | chatId: { type: Schema.Types.ObjectId }, 7 | senderId: { type: Schema.Types.ObjectId }, 8 | message: { type: String }, 9 | dateTime: { type: Date }, 10 | // saved:{type:Boolean} 11 | }, 12 | { timestamps: true } 13 | ); 14 | 15 | const Message = model("Messages", MessageSchema); 16 | 17 | export default Message; 18 | -------------------------------------------------------------------------------- /src/routes/message.routes.ts: -------------------------------------------------------------------------------- 1 | import {Router} from "express"; 2 | import { verifyToken } from "../middleware/isAuthorized"; 3 | import { deleteMessage, editMessage, getMessages, newMessage } from "../controllers/message"; 4 | 5 | const MessageRoutes = Router(); 6 | 7 | MessageRoutes.post("/:id", verifyToken, newMessage); 8 | MessageRoutes.get("/:id", verifyToken, getMessages); 9 | MessageRoutes.delete("/:messageId", verifyToken, deleteMessage); 10 | MessageRoutes.put("/:messageId", verifyToken, editMessage); 11 | 12 | export default MessageRoutes; -------------------------------------------------------------------------------- /src/models/comment.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IFollowing } from "../interfaces/user.interface"; 3 | import { IComment } from "../interfaces/post.interface"; 4 | 5 | const CommentSchema = new Schema({ 6 | userId: { type: Schema.Types.ObjectId,ref:"User", required: true }, 7 | postId: { type: Schema.Types.ObjectId, ref:"Posts", required:true}, 8 | replies: [], 9 | comment: { type: String, required: true }, 10 | dateTime: { type: Date, required: true}, 11 | }); 12 | 13 | const Comment = model("Comment", CommentSchema); 14 | 15 | export default Comment -------------------------------------------------------------------------------- /src/routes/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | import express,{Router} from "express"; 3 | import {requiresAuth } from "express-openid-connect"; 4 | import { Login,Logout} from "../controllers/auth"; 5 | import { verifyToken } from "../middleware/isAuthorized"; 6 | 7 | 8 | // const storage= multer.memoryStorage(); 9 | // const uploaded = multer({storage}) 10 | const app=express(); 11 | const Authroutes= Router(); 12 | 13 | Authroutes.get('/login',requiresAuth(),Login) 14 | Authroutes.get('/logout', Logout) 15 | // Authroutes.get('/register', verifyToken) 16 | 17 | 18 | 19 | 20 | export default Authroutes -------------------------------------------------------------------------------- /src/models/post.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IPost } from "../interfaces/post.interface"; 3 | 4 | const PostSchema = new Schema({ 5 | postId:{type:Schema.Types.ObjectId}, 6 | userId:{type: Schema.Types.ObjectId, ref: "User", required: true}, 7 | imageUrl:[{type:String}], 8 | caption:{type:String}, 9 | likes:[{type:Schema.Types.ObjectId, ref:"User"}], 10 | comments:[{type:Schema.Types.ObjectId, ref:"Comment"}], 11 | dateTime:{type:Date}, 12 | // saved:{type:Boolean} 13 | }); 14 | 15 | const Post = model("Posts", PostSchema); 16 | 17 | export default Post -------------------------------------------------------------------------------- /src/environments/endpoints.config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | export const environment = { 4 | PORT: Number(process.env.PORT) || 3000, 5 | DBUrl: String(process.env.MONGODBURL), 6 | PROD_ENV: String(process.env.PROD_ENV), 7 | AUTH0SECRET: String(process.env.AUTH0SECRET), 8 | AUTH0BASEURL: String(process.env.AUTH0BASEURL), 9 | AUTH0CLIENTID: String(process.env.AUTH0CLIENTID), 10 | ISSUERBASEURL: String(process.env.ISSUERBASEURL), 11 | JWTKEY: String(process.env.JWTKEY), 12 | AWS_ACCESS_KEY: String(process.env.AWS_ACCESS_KEY), 13 | AWS_SECRET_ACCESS_KEY: String(process.env.AWS_SECRET_ACCESS_KEY), 14 | }; 15 | -------------------------------------------------------------------------------- /src/middleware/errorhandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { environment } from "../environments/endpoints.config"; 3 | 4 | const ErrorHandler = (err: any, req: Request, res: Response, next: NextFunction) => { 5 | const status = err.statusCode || 500; // if no status code is provided, default to 500 (internal server error) 6 | const message = err.message || "Something went wrong"; 7 | res.status(status).json({ 8 | success: false, 9 | status, 10 | message, 11 | stack: environment.PROD_ENV !== "development" ? {} : err.stack, 12 | }) 13 | } 14 | 15 | export default ErrorHandler; -------------------------------------------------------------------------------- /src/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "mongoose"; 2 | export interface IUser{ 3 | UserID:Schema.Types.ObjectId 4 | Auth0ID: string; 5 | Username: string; 6 | Name: string; 7 | Email: string; 8 | EmailVerified: boolean; 9 | Password: string; 10 | ProfileUrl: string; 11 | Bio: string; 12 | Followers: any[]; 13 | Followings: any[]; 14 | Posts:any[]; 15 | Likes: any; 16 | Comments: any; 17 | SavedPosts:any[] 18 | Stories: any[]; 19 | 20 | } 21 | export interface IFollowing{ 22 | userID: Schema.Types.ObjectId 23 | followingID: Schema.Types.ObjectId 24 | }export interface IFollower{ 25 | userID:Schema.Types.ObjectId 26 | followerID: Schema.Types.ObjectId 27 | } 28 | -------------------------------------------------------------------------------- /src/models/stories.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IStories } from "../interfaces/story.interface"; 3 | 4 | const StoriesSchema = new Schema( 5 | { 6 | storyId: { type: String, required: true }, 7 | userId: { type: Schema.Types.ObjectId, ref: "User", required: true }, 8 | postId: { type: Schema.Types.ObjectId, ref: "Posts", required: true }, 9 | expireAt: { 10 | type: Date, 11 | default: () => Date.now() + 24 * 60 * 60 * 1000, // expires in 24 hours 12 | expires: 60 * 60 * 24, 13 | }, 14 | }, 15 | { timestamps: true } 16 | ); 17 | 18 | const Stories = model("Stories", StoriesSchema); 19 | 20 | export default Stories; 21 | -------------------------------------------------------------------------------- /src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Schema, model } from "mongoose"; 2 | import { IUser } from "../interfaces/user.interface"; 3 | 4 | const UserSchema = new Schema({ 5 | UserID: { type: Schema.Types.ObjectId }, 6 | Auth0ID: { type: String, required: true }, 7 | Name: { type: String, required: true }, 8 | Username: { type: String, required: true }, 9 | Email: { type: String, required: true }, 10 | EmailVerified: { type: Boolean, }, 11 | ProfileUrl: { type: String, required: true }, 12 | Bio: { type: String}, 13 | Followers: [{ type: String, ref: "Followers", }], 14 | Followings: [{ type: String, ref: "Following",}], 15 | Posts: [{ type: Schema.Types.ObjectId, ref: "Posts",}], 16 | SavedPosts: [{ type: String, ref: "SavedPosts",}], 17 | Stories: [{type: Schema.Types.ObjectId, ref: "Stories",}], 18 | }); 19 | 20 | const User = model("User", UserSchema); 21 | 22 | export default User -------------------------------------------------------------------------------- /src/interfaces/post.interface.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "mongoose"; 2 | 3 | export interface IPost { 4 | postId: Schema.Types.ObjectId; 5 | userId: Schema.Types.ObjectId; 6 | imageUrl: string []; // URL or reference to the image or video 7 | caption: string; 8 | likes: Schema.Types.ObjectId[]; // Array of user IDs who liked the post 9 | comments: any[]; // Array of comment IDs 10 | dateTime: Date; // Date and time of the post in ISO format (e.g., "2023-11-10T12:34:56.789Z") 11 | saved: boolean; 12 | 13 | } 14 | 15 | export interface ISavedPost { 16 | userId: Schema.Types.ObjectId; 17 | postId: Schema.Types.ObjectId; 18 | } 19 | 20 | export interface IComment { 21 | commentId: Schema.Types.ObjectId; 22 | userId: Schema.Types.ObjectId; 23 | postId: Schema.Types.ObjectId; 24 | replies: IReplies[]; 25 | comment: string; 26 | dateTime: Date; 27 | } 28 | export interface IReplies{ 29 | replyId: string; 30 | userId: Schema.Types.ObjectId; 31 | commentId: Schema.Types.ObjectId; 32 | reply: string; 33 | dateTime: any; 34 | } 35 | -------------------------------------------------------------------------------- /src/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | import {Router} from "express"; 3 | import { verifyToken } from "../middleware/isAuthorized"; 4 | import { getFollowers, getFollowing, newFollow, removeFollow, unFollow, AddBio, deleteBio, getUser, editProfile, getotherUser, searchUser } from "../controllers/user"; 5 | 6 | 7 | const storage= multer.memoryStorage(); 8 | const upload = multer({storage}) 9 | const UserRoutes= Router(); 10 | //following & unfollowing functions 11 | UserRoutes.post('/addFollowing',verifyToken,newFollow) 12 | UserRoutes.post('/unfollow',verifyToken,unFollow) 13 | UserRoutes.get('/getFollowing',verifyToken, getFollowing) 14 | UserRoutes.get('/getFollowers',verifyToken, getFollowers) 15 | UserRoutes.post('/removeFollower',verifyToken, removeFollow) 16 | 17 | 18 | UserRoutes.get('/getUser',verifyToken, getUser) 19 | UserRoutes.get('/getuserprofile',verifyToken, getotherUser) 20 | UserRoutes.post('/searchuser',verifyToken,searchUser) 21 | 22 | 23 | //Bio & profile 24 | UserRoutes.post('/addBio',verifyToken, AddBio) 25 | UserRoutes.delete('/deleteBio',verifyToken, deleteBio) 26 | UserRoutes.put('/editprofile',upload.single('images'),verifyToken, editProfile) 27 | 28 | 29 | 30 | 31 | 32 | export default UserRoutes -------------------------------------------------------------------------------- /src/routes/post.routes.ts: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | import {Router} from "express"; 3 | import { verifyToken } from "../middleware/isAuthorized"; 4 | import { AddtoSaved, RemoveFromSaved, addLikes, createComment, deleteComment, deletePost, deleteReply, getAllComments, newPost, replyComment, unlike } from "../controllers/post"; 5 | 6 | const PostRoutes = Router(); 7 | 8 | const storage= multer.memoryStorage(); 9 | const upload = multer({storage}) 10 | const PostUpload=upload.array('images',10) 11 | 12 | PostRoutes.post('/newpost',PostUpload,verifyToken,newPost) 13 | PostRoutes.post('/deletepost',verifyToken,deletePost) 14 | PostRoutes.post('/likepost',verifyToken, addLikes) 15 | PostRoutes.post('/unlikepost',verifyToken, unlike) 16 | 17 | // routes for saved post 18 | PostRoutes.post('/savepost',verifyToken, AddtoSaved) 19 | PostRoutes.post('/removesaved', verifyToken, RemoveFromSaved) 20 | 21 | 22 | //routes for comments 23 | PostRoutes.post('/comment',verifyToken, createComment) 24 | PostRoutes.post('/allcomments',verifyToken, getAllComments) 25 | PostRoutes.delete('/removecomment',verifyToken, deleteComment) 26 | PostRoutes.post('/replycomment',verifyToken, replyComment) 27 | PostRoutes.delete('/removereply',verifyToken, deleteReply) 28 | 29 | 30 | export default PostRoutes -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendwave", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "dev": "nodemon ./src/app.ts", 9 | "start": "node ./build/app.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/express": "^4.17.20", 16 | "@types/jsonwebtoken": "^9.0.4", 17 | "@types/morgan": "^1.9.9", 18 | "@types/multer": "^1.4.9", 19 | "@types/node": "^20.8.10", 20 | "@types/node-cron": "^3.0.11", 21 | "ts-node": "^10.9.1", 22 | "typescript": "^5.2.2" 23 | }, 24 | "dependencies": { 25 | "@aws-sdk/client-s3": "^3.649.0", 26 | "@aws-sdk/lib-storage": "^3.445.0", 27 | "@aws-sdk/s3-request-presigner": "^3.649.0", 28 | "@types/uuid": "^9.0.7", 29 | "dotenv": "^16.3.1", 30 | "express": "^4.20.0", 31 | "express-openid-connect": "^2.17.1", 32 | "joi": "^17.11.0", 33 | "jsonwebtoken": "^9.0.2", 34 | "mongodb": "^6.2.0", 35 | "mongoose": "^8.0.0", 36 | "morgan": "^1.10.0", 37 | "multer": "^1.4.5-lts.1", 38 | "node-cron": "^3.0.3", 39 | "socket.io": "^4.7.2", 40 | "uuid": "^9.0.1", 41 | "winston": "^3.11.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/middleware/debughandler.ts: -------------------------------------------------------------------------------- 1 | import morgan, { StreamOptions } from "morgan"; 2 | 3 | import Logger from "../lib/logger"; 4 | import { environment } from "../environments/endpoints.config"; 5 | 6 | // Override the stream method by telling 7 | // Morgan to use our custom logger instead of the console.log. 8 | const stream: StreamOptions = { 9 | // Use the http severity 10 | write: (message) => Logger.http(message), 11 | }; 12 | 13 | // Skip all the Morgan http log if the 14 | // application is not running in development mode. 15 | // This method is not really needed here since 16 | // we already told to the logger that it should print 17 | // only warning and error messages in production. 18 | const skip = () => { 19 | const env = `${environment.PROD_ENV}` || "Development"; 20 | return env !== "Development"; 21 | }; 22 | 23 | // Build the morgan middleware 24 | const morganMiddleware = morgan( 25 | // Define message format string (this is the default one). 26 | // The message format is made from tokens, and each token is 27 | // defined inside the Morgan library. 28 | // You can create your custom token to show what do you want from a request. 29 | ":method :url :status :res[content-length] - :response-time ms", 30 | // Options: in this case, I overwrote the stream and the skip logic. 31 | // See the methods above. 32 | { stream, skip } 33 | ); 34 | 35 | export default morganMiddleware; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Friend Wave - Social Media App 2 | 3 | Friend Wave is a social media app designed to connect friends and share moments. This README provides basic information for running it in a development environment. 4 | 5 | ## Technology Stack 6 | 7 | Friend Wave is built using the following technologies: 8 | 9 | [![Stack Used](https://skillicons.dev/icons?i=mongodb,js,typescript,nodejs,express&theme=dark)](https://skillicons.dev) 10 | 11 | 12 | 13 | **Note: Friend Wave is currently in development.** 14 | 15 | ## Getting Started 16 | 17 | ### Prerequisites 18 | 19 | - Node.js 20 | - npm 21 | 22 | ### Installation 23 | 24 | 1. Clone the repository: 25 | git clone [https://github.com/LawrenceY-B/friend-wave.git](https://github.com/LawrenceY-B/Friendwave.git) 26 | 27 | 2. Navigate to the project directory: 28 | cd friendwave 29 | 30 | 3. Install dependencies: 31 | npm install 32 | 33 | 4. Start the development server: 34 | npm run dev 35 | 36 | 37 | Access Friend Wave at `http://localhost:3000`. 38 | 39 | ## Work Done 40 | 41 | - Feature 1: Follow and Unfollow ✅ 42 | - Feature 2: User Authentication (Auth0 and jwt) ✅ 43 | - Feature 3: Add Posts ✅ 44 | - Feature 4: Comments ✅ 45 | - Feature 5: Stories ✅ 46 | - Feature 6: Message ✅ 47 | - Feature 7: Real-time Messaging (working on it 😮‍💨) 48 | 49 | 50 | ## Contact 51 | 52 | - Email: [lawrencekybj@gmail.com] 53 | - GitHub: [LawrenceY-B](https://github.com/LawrenceY-B) 54 | 55 | --- 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/middleware/isAuthorized.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import { Schema } from "mongoose"; 4 | import { environment } from "../environments/endpoints.config"; 5 | import Logger from "../lib/logger"; 6 | 7 | //this module extends Request to create a new object, in this case, the user object 8 | declare module "express-serve-static-core" { 9 | interface Request { 10 | user: { 11 | // Properties of token payload 12 | Auth0id: string; 13 | userID: Schema.Types.ObjectId; 14 | userMail: string; 15 | iat: number; 16 | exp: number; 17 | }; 18 | } 19 | } 20 | 21 | export const verifyToken = async ( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) => { 26 | const Authorization = req.get("Authorization"); 27 | if (!Authorization || !Authorization.startsWith("Bearer ")) { 28 | res.status(401).json({ message: "Invalid Authorization header" }); 29 | Logger.warn("Invalid Authorization header"); 30 | } 31 | 32 | const token: string = Authorization!.split(" ")[1]; 33 | try { 34 | const payload: any = jwt.verify(token, environment.JWTKEY || ""); 35 | 36 | if (!payload) { 37 | res.status(401).json({ message: "Invalid access token" }); 38 | Logger.warn("Invalid access token"); 39 | } 40 | 41 | // check expiry 42 | if (payload.exp < Date.now() / 1000) { 43 | res.status(401).json({ message: "Token has expired" }); 44 | Logger.warn("Token has expired"); 45 | } 46 | req.user = payload; 47 | next(); 48 | } catch (error) { 49 | next(error); 50 | Logger.warn(error); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/jobs/story.jobs.ts: -------------------------------------------------------------------------------- 1 | import cron from "node-cron"; 2 | import { NextFunction, Request, Response } from "express"; 3 | import { validateStory } from "../services/story.service"; 4 | import User from "../models/user.model"; 5 | import Stories from "../models/stories.model"; 6 | import Logger from "../lib/logger"; 7 | import fs from "fs"; 8 | import path from "path"; 9 | 10 | export const storycheck = async () => { 11 | try { 12 | let task = cron.schedule("30 */1 * * *", async () => { 13 | Logger.info("task started"); 14 | const users = await User.find(); 15 | const singleuser = users.map((user) => { 16 | const stories = user.Stories.map(async (story) => { 17 | const check = await Stories.findOne({ _id: story }); 18 | if (!check) { 19 | const updatePost = await User.findOneAndUpdate( 20 | { UserID: user.UserID }, 21 | { $pull: { Stories: story } }, 22 | { new: true } 23 | ); 24 | Logger.info(updatePost); 25 | if (updatePost) { 26 | // task.stop(); 27 | Logger.info("removed story"); 28 | } 29 | } 30 | }); 31 | }); 32 | }); 33 | } catch (error: any) { 34 | Logger.error(error); 35 | } 36 | }; 37 | 38 | export const logcheck = () => { 39 | try { 40 | const filePath = path.join(__dirname, "../../logs/error.log"); 41 | let task = cron.schedule("* */24 * * *", async () => { 42 | Logger.info("task started"); 43 | fs.access(filePath, fs.constants.F_OK, (err) => { 44 | if (err) { 45 | Logger.error(`File ${filePath} does not exist`); 46 | return; 47 | } 48 | fs.writeFile(filePath, "", (err) => { 49 | if (err) { 50 | Logger.error(`Error deleting contents of ${filePath}:`, err); 51 | return; 52 | } 53 | Logger.info(`Successfully deleted contents of logs`); 54 | }); 55 | }); 56 | }); 57 | } catch (error: any) { 58 | Logger.error(error); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /src/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | S3, 3 | PutObjectCommand, 4 | S3Client, 5 | GetObjectCommand, 6 | } from "@aws-sdk/client-s3"; 7 | 8 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 9 | import { NextFunction, Request } from "express"; 10 | import joi from "joi"; 11 | import { Schema } from "mongoose"; 12 | import { environment } from "../environments/endpoints.config"; 13 | 14 | export const validateFollow = (person: Request) => { 15 | const schema = joi.object({ 16 | FollowingID: joi.string().min(3), 17 | }); 18 | return schema.validate(person); 19 | }; 20 | 21 | export const validateBio = (bio: Request) => { 22 | const schema = joi.object({ 23 | Bio: joi.string().min(3), 24 | }); 25 | return schema.validate(bio); 26 | }; 27 | export const validateProfile = (profile: Request) => { 28 | const schema = joi.object({ 29 | Name: joi.string().min(3), 30 | Username: joi.string().min(3), 31 | Bio: joi.string().min(0).max(150), 32 | }); 33 | return schema.validate(profile); 34 | }; 35 | export const ImageUpload = async ( 36 | images: Express.Multer.File, 37 | next: NextFunction, 38 | userid: Schema.Types.ObjectId 39 | ) => { 40 | try { 41 | let MIMEtype = images.mimetype; 42 | const fileformat = MIMEtype.replace(MIMEtype.slice(0, 6), "."); 43 | const awsS3 = new S3Client({ 44 | credentials: { 45 | accessKeyId: `${environment.AWS_ACCESS_KEY}`, 46 | secretAccessKey: `${environment.AWS_SECRET_ACCESS_KEY}`, 47 | }, 48 | region: "eu-west-2", 49 | }); 50 | const key = `/profile/${userid}${Date.now().toString()}${fileformat}`; 51 | let upload = new PutObjectCommand({ 52 | Bucket: "fwstorage-trial", 53 | Key: `${key}`, 54 | Body: images.buffer, 55 | }); 56 | await awsS3.send(upload); 57 | 58 | let retrieve = new GetObjectCommand({ 59 | Bucket: "fwstorage-trial", 60 | Key: `${key}`, 61 | }); 62 | const signedUrl = await getSignedUrl(awsS3, retrieve); 63 | return signedUrl; 64 | } catch (err) { 65 | next(err); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/controllers/socket.ts: -------------------------------------------------------------------------------- 1 | import { Server as SocketIOServer, Socket } from "socket.io"; 2 | import Logger from "../lib/logger"; 3 | 4 | export default class SocketManager { 5 | private io: SocketIOServer; 6 | private onlineUsers: Map = new Map(); 7 | 8 | constructor(io: SocketIOServer) { 9 | this.io = io; 10 | this.setupSocketEvents(); 11 | } 12 | 13 | private setupSocketEvents(): void { 14 | this.io.on("connection", (socket: Socket) => { 15 | this.handleUserConnection(socket); 16 | 17 | socket.on("disconnect", () => { 18 | this.handleUserDisconnection(socket); 19 | }); 20 | 21 | socket.on("privateMessage", (data) => { 22 | this.handlePrivateMessage(socket, data); 23 | }); 24 | }); 25 | } 26 | 27 | handleUserConnection(socket: Socket): void { 28 | Logger.info(`User connected: ${socket.id}`); 29 | socket.on("addUser", (userID) => { 30 | const {userId} = userID; 31 | Logger.info(`Received addUser event for userID: ${userId}`); 32 | const isExisting = this.onlineUsers.has(userId); 33 | 34 | if (!isExisting) { 35 | this.onlineUsers.set(userId, socket.id);; 36 | } 37 | 38 | Logger.debug(this.onlineUsers); 39 | }); 40 | } 41 | 42 | 43 | handlePrivateMessage( 44 | socket: Socket, 45 | data: { userId: string; message: string } 46 | ): void { 47 | const { userId, message } = data; 48 | const targetSocketId = this.onlineUsers.get(userId) as string; 49 | Logger.info( 50 | `Received privateMessage event from ${socket.id} to ${targetSocketId}` 51 | ); 52 | socket.to(targetSocketId).emit("privateMessage", { 53 | senderSocketId: socket.id, 54 | message: message, 55 | }); 56 | 57 | // socket.emit("privateMessage", { 58 | // receiverSocketId: targetSocketId, 59 | // message: message, 60 | // }); 61 | } 62 | handleUserDisconnection(socket: Socket): void { 63 | Logger.info(`User disconnected: ${socket.id}`); 64 | this.onlineUsers.forEach((value, key) => { 65 | if (value === socket.id) { 66 | this.onlineUsers.delete(key); 67 | } 68 | }) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/lib/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | import { environment } from '../environments/endpoints.config' 3 | 4 | 5 | // Define your severity levels. 6 | // With them, You can create log files, 7 | // see or hide levels based on the running ENV. 8 | const levels = { 9 | error: 0, 10 | warn: 1, 11 | info: 2, 12 | http: 3, 13 | debug: 4, 14 | } 15 | 16 | // This method set the current severity based on 17 | // the current NODE_ENV: show all the log levels 18 | // if the server was run in development mode; otherwise, 19 | // if it was run in production, show only warn and error messages. 20 | const level = () => { 21 | const env = `${environment.PROD_ENV}` || 'Development' 22 | const isDevelopment = env === 'Development' 23 | return isDevelopment ? 'debug' : 'warn' 24 | } 25 | 26 | // Define different colors for each level. 27 | // Colors make the log message more visible, 28 | // adding the ability to focus or ignore messages. 29 | const colors = { 30 | error: 'red', 31 | warn: 'yellow', 32 | info: 'green', 33 | http: 'magenta', 34 | debug: 'blue', 35 | } 36 | 37 | // Tell winston that you want to link the colors 38 | // defined above to the severity levels. 39 | winston.addColors(colors) 40 | 41 | // Chose the aspect of your log customizing the log format. 42 | const format = winston.format.combine( 43 | // Add the message timestamp with the preferred format 44 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), 45 | // Tell Winston that the logs must be colored 46 | winston.format.colorize({ all: true }), 47 | // Define the format of the message showing the timestamp, the level and the message 48 | winston.format.printf( 49 | (info) => `${info.timestamp} ${info.level}: ${info.message}`, 50 | ), 51 | ) 52 | 53 | // Define which transports the logger must use to print out messages. 54 | // In this example, we are using three different transports 55 | const transports = [ 56 | // Allow the use the console to print the messages 57 | new winston.transports.Console(), 58 | // Allow to print all the error level messages inside the error.log file 59 | new winston.transports.File({ 60 | filename: 'logs/error.log', 61 | level: 'error', 62 | }), 63 | ] 64 | 65 | // Create the logger instance that has to be exported 66 | // and used to log messages. 67 | const Logger = winston.createLogger({ 68 | level: level(), 69 | levels, 70 | format, 71 | transports, 72 | }) 73 | 74 | export default Logger -------------------------------------------------------------------------------- /src/controllers/auth.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import User from "../models/user.model"; 3 | import { Response, Request, NextFunction } from "express"; 4 | import { environment } from "../environments/endpoints.config"; 5 | 6 | export const Login = async ( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) => { 11 | try { 12 | const user = await User.findOne({ Auth0ID: req.oidc.user!.sub }); 13 | 14 | if (!user) { 15 | const result = await User.create({ 16 | Auth0ID: req.oidc.user!.sub, 17 | Name: req.oidc.user!.name, 18 | Username: req.oidc.user!.nickname, 19 | Email: req.oidc.user!.email, 20 | EmailVerified: req.oidc.user!.email_verified, 21 | ProfileUrl: req.oidc.user!.picture, 22 | }); 23 | if (result) { 24 | const token = jwt.sign( 25 | { 26 | Auth0id: req.oidc.user!.sub, 27 | userID: result?._id, 28 | userMail: req.oidc.user!.email, 29 | }, 30 | `${environment.JWTKEY}`, 31 | { 32 | expiresIn: "7d", 33 | } 34 | ); 35 | const addID=await User.findByIdAndUpdate(result?._id,{UserID:result?._id}) 36 | res 37 | .status(200) 38 | .json({ message: "User created successfully", token: token, userID: result?._id }); 39 | } else { 40 | res.status(404).json({ message: "User not found" }); 41 | } 42 | } else { 43 | const token = jwt.sign( 44 | { 45 | Auth0id: req.oidc.user!.sub, 46 | userID: user?._id, 47 | userMail: req.oidc.user!.email, 48 | 49 | }, 50 | `${environment.JWTKEY}`, 51 | { 52 | expiresIn: "7d", 53 | } 54 | ); 55 | res 56 | .status(200) 57 | .json({ message: "Login Successful ✅✅✅", token: token, userID: user?._id}); 58 | } 59 | } catch (error) { 60 | next(error); 61 | } 62 | }; 63 | 64 | export const Logout = async ( 65 | req: Request, 66 | res: Response, 67 | next: NextFunction 68 | ) => { 69 | try { 70 | const payload = { 71 | // sub: req.user.userID, 72 | iat: Date.now() / 1000, 73 | exp: 0, 74 | }; 75 | 76 | const newToken = jwt.sign(payload, `${environment.JWTKEY}`); 77 | res.oidc.logout(); 78 | 79 | // Respond with a success message and the new token 80 | return res.status(200).json({ message: "Logged out", token: newToken }); 81 | } catch (error) { 82 | next(error); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/controllers/chats.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { validateChat } from "../services/chat.service"; 3 | import Chat from "../models/conversation.model"; 4 | import Logger from "../lib/logger"; 5 | 6 | // new chat 7 | export const newChat = async ( 8 | req: Request, 9 | res: Response, 10 | next: NextFunction 11 | ) => { 12 | try { 13 | const { error } = validateChat(req.body); 14 | if (error) 15 | return res 16 | .status(400) 17 | .json({ success: false, message: error.details[0].message }); 18 | const userID = req.user.userID 19 | const {receiverID } = req.body; 20 | 21 | const isExisting = await Chat.findOne({ 22 | members: { $all: [userID, receiverID] }, 23 | }); 24 | if (isExisting) { 25 | return res 26 | .status(400) 27 | .json({ success: false, message: "Chat already exists" }); 28 | } 29 | let members = [userID, receiverID]; 30 | const chat = new Chat({ members }); 31 | const savechat = await chat.save(); 32 | if (!savechat) { 33 | Logger.error("Something went wrong"); 34 | return res 35 | .status(400) 36 | .json({ success: false, message: "Something went wrong" }); 37 | } 38 | 39 | res.status(201).json({ success: true, message: "Chat created" }); 40 | 41 | await chat.save(); 42 | } catch (error) {} 43 | }; 44 | 45 | //get chat 46 | export const getAllChats = async ( 47 | req: Request, 48 | res: Response, 49 | next: NextFunction 50 | ) => { 51 | try { 52 | const senderID = req.user.userID; 53 | const chat = await Chat.find({ 54 | members: { $in: [senderID] }, 55 | }); 56 | if (!chat) { 57 | return res 58 | .status(400) 59 | .json({ success: false, message: "Chat does not exist" }); 60 | } 61 | res.status(200).json({ success: true, message: "Chat found", chat }); 62 | } catch (error) { 63 | next(error); 64 | } 65 | }; 66 | 67 | //delete chat 68 | export const deleteChat = async ( 69 | req: Request, 70 | res:Response, 71 | next: NextFunction 72 | ) => { 73 | try { 74 | const chatID = req.params.id; 75 | const chat = await Chat.findByIdAndDelete(chatID); 76 | if (!chat) { 77 | return res 78 | .status(400) 79 | .json({ success: false, message: "Chat does not exist" }); 80 | } 81 | res.status(200).json({ success: true, message: "Chat deleted" }); 82 | } catch (error) { 83 | next(error); 84 | } 85 | } -------------------------------------------------------------------------------- /src/controllers/message.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import Message from "../models/message.model"; 3 | import Logger from "../lib/logger"; 4 | import { IMessage } from "../interfaces/chat.interface"; 5 | 6 | export const newMessage = (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | const { message } = req.body; 9 | const senderId = req.user.userID; 10 | const chatId = req.params.id; 11 | const newMessage = { 12 | message, 13 | senderId, 14 | chatId, 15 | }; 16 | 17 | const sendMessage = new Message({ ...newMessage, dateTime: Date.now() 18 | }); 19 | let isSaved = sendMessage.save(); 20 | if (!isSaved) { 21 | res.status(400).json({success:"false", message: "Message not sent" }); 22 | } 23 | res.status(200).json({success:"true", message: "Message Sent" }); 24 | } catch (error) { 25 | next(error); 26 | } 27 | }; 28 | 29 | export const getMessages = async ( 30 | req:Request, 31 | res:Response, 32 | next:NextFunction 33 | )=>{ 34 | try { 35 | const chatId = req.params.id; 36 | const getMessage = await Message.find({chatId: chatId}) 37 | if(!getMessage){ 38 | res.status(400).json({success:"false", message: "No Messages" }); 39 | } 40 | res.status(200).json({success:"true", message: "Messages Found", data:getMessage }); 41 | 42 | } catch (error) { 43 | next(error) 44 | } 45 | } 46 | 47 | export const deleteMessage = async ( 48 | req:Request, 49 | res:Response, 50 | next:NextFunction 51 | ) => { 52 | try { 53 | const {messageId} = req.params; 54 | const deleteMessage = await Message.findByIdAndDelete(messageId); 55 | if(!deleteMessage){ 56 | res.status(400).json({success:"false", message: "Message not found" }); 57 | } 58 | res.status(200).json({success:"true", message: "Message deleted" }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | } 63 | 64 | export const editMessage = async ( 65 | req:Request, 66 | res:Response, 67 | next:NextFunction 68 | ) => { 69 | try { 70 | const {messageId} = req.params; 71 | const {message} = req.body; 72 | const editMessage = await Message.findByIdAndUpdate(messageId, {message: message}); 73 | if(!editMessage){ 74 | res.status(400).json({success:"false", message: "Message not found" }); 75 | } 76 | res.status(200).json({success:"true", message: "Message edited" }); 77 | } catch (error) { 78 | next(error); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from "express"; 2 | import { environment } from "./environments/endpoints.config"; 3 | import { createServer } from "http"; 4 | import { Server as SocketIOServer } from "socket.io"; 5 | 6 | import { DB_Connection } from "./database/db"; 7 | import { auth, requiresAuth } from "express-openid-connect"; 8 | import ErrorHandler from "./middleware/errorhandler"; 9 | import Authroutes from "./routes/auth.routes"; 10 | import UserRoutes from "./routes/user.routes"; 11 | import PostRoutes from "./routes/post.routes"; 12 | import StoryRoutes from "./routes/story.routes"; 13 | import { storycheck, logcheck } from "./jobs/story.jobs"; 14 | import Logger from "./lib/logger"; 15 | import morganMiddleware from "./middleware/debughandler"; 16 | import ChatRoutes from "./routes/chat.routes"; 17 | import MessageRoutes from "./routes/message.routes"; 18 | import SocketManager from "./controllers/socket"; 19 | 20 | const app = express(); 21 | const httpServer = createServer(app); 22 | const io = new SocketIOServer(httpServer); 23 | const config = { 24 | authRequired: false, 25 | auth0Logout: true, 26 | secret: `${environment.AUTH0SECRET}`, 27 | baseURL: `${environment.AUTH0BASEURL}`, 28 | clientID: `${environment.AUTH0CLIENTID}`, 29 | issuerBaseURL: `${environment.ISSUERBASEURL}`, 30 | }; 31 | app 32 | .use((req: Request, res: Response, next: NextFunction) => { 33 | res.setHeader("Access-Control-Allow-Origin", "*"); 34 | res.setHeader( 35 | "Access-Control-Allow-Methods", 36 | "GET, POST, PUT, PATCH, DELETE" 37 | ); 38 | res.setHeader("Content-Type", "application/json"); 39 | res.setHeader( 40 | "Access-Control-Allow-Headers", 41 | "Content-Type, Authorization" 42 | ); 43 | next(); 44 | }) 45 | .use(express.json()) 46 | .use(auth(config)) 47 | .use("/api", Authroutes) 48 | .use("/api/users", UserRoutes) 49 | .use("/api/posts", PostRoutes) 50 | .use("/api/story", StoryRoutes) 51 | .use("/api/chat", ChatRoutes) 52 | .use("/api/messages", MessageRoutes); 53 | 54 | // auth router attaches /login, /logout, and /callback routes to the baseURL 55 | app.all("*", (req: Request, res: Response) => { 56 | res.status(404).json({ message: "Page Not Found 😔" }); 57 | Logger.http(`${req.method} - ${req.url} - ${req.ip} Page Not Found 🔦🔦`); 58 | }); 59 | 60 | storycheck(); 61 | logcheck(); 62 | app.use(morganMiddleware); 63 | app.use(ErrorHandler); 64 | 65 | const socketManager = new SocketManager(io); 66 | 67 | 68 | const server = httpServer.listen(environment.PORT, async () => { 69 | await DB_Connection(); 70 | Logger.debug(`🚀🚀🚀Server is running on port ${process.env.PORT}`); 71 | }); 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/services/post.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | S3Client, 3 | PutObjectCommand, 4 | GetObjectCommand, 5 | } from "@aws-sdk/client-s3"; 6 | import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; 7 | import { NextFunction, Request } from "express"; 8 | import joi from "joi"; 9 | import { Schema } from "mongoose"; 10 | import { environment } from "../environments/endpoints.config"; 11 | 12 | export const validatePost = (caption: Request) => { 13 | const schema = joi.object({ 14 | caption: joi.string().min(0).max(250), 15 | }); 16 | return schema.validate(caption); 17 | }; 18 | export const validatePostID = (caption: Request) => { 19 | const schema = joi.object({ 20 | postID: joi.string().min(6), 21 | }); 22 | return schema.validate(caption); 23 | }; 24 | export const validateCommentID = (caption: Request) => { 25 | const schema = joi.object({ 26 | commentId: joi.string().min(6), 27 | }); 28 | return schema.validate(caption); 29 | }; 30 | 31 | export const validateReply = (caption: Request) => { 32 | const schema = joi.object({ 33 | commentID: joi.string().min(6), 34 | message: joi.string().min(0).max(250), 35 | }); 36 | return schema.validate(caption); 37 | }; 38 | export const validateComment = (caption: Request) => { 39 | const schema= joi .object({ 40 | message:joi.string().min(0).max(250), 41 | postID:joi.string().min(6), 42 | }); 43 | return schema.validate(caption); 44 | } 45 | 46 | 47 | export const ImageUpload = async ( 48 | images: Express.Multer.File[], 49 | next: NextFunction, 50 | userid: Schema.Types.ObjectId 51 | ) => { 52 | try { 53 | const awsS3 = new S3Client({ 54 | credentials: { 55 | accessKeyId: environment.AWS_ACCESS_KEY, 56 | secretAccessKey: environment.AWS_SECRET_ACCESS_KEY, 57 | }, 58 | region: "eu-west-2", 59 | }); 60 | 61 | const uploadPromises = images.map(async (element) => { 62 | try { 63 | const MIMEtype = element.mimetype; 64 | const fileformat = MIMEtype.replace(MIMEtype.slice(0, 6), "."); 65 | const key = `/posts/${userid}${Date.now().toString()}${fileformat}`; 66 | 67 | const upload = new PutObjectCommand({ 68 | Bucket: "fwstorage-trial", 69 | Key: key, 70 | Body: element.buffer, 71 | }); 72 | await awsS3.send(upload); 73 | 74 | // Generate a signed URL for the uploaded object 75 | const signedUrl = await getSignedUrl(awsS3, new GetObjectCommand({ 76 | Bucket: "fwstorage-trial", 77 | Key: key, 78 | })); 79 | 80 | return signedUrl; 81 | } catch (error) { 82 | next(error); 83 | } 84 | }); 85 | 86 | const results = await Promise.all(uploadPromises); 87 | return results; 88 | } catch (err) { 89 | next(err); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /src/controllers/story.ts: -------------------------------------------------------------------------------- 1 | import Post from "../models/post.model"; 2 | import Stories from "../models/stories.model"; 3 | import User from "../models/user.model"; 4 | import { v4 as uiv4 } from "uuid"; 5 | import { NextFunction, Request, Response } from "express"; 6 | import { validateStory } from "../services/story.service"; 7 | import Logger from "../lib/logger"; 8 | 9 | export const addtoStory = async ( 10 | req: Request, 11 | res: Response, 12 | next: NextFunction 13 | ) => { 14 | try { 15 | const userID = req.user.userID; 16 | const { error } = validateStory(req.body); 17 | if (error) { 18 | return res 19 | .status(400) 20 | .json({ success: false, message: error.details[0].message }); 21 | } 22 | const { postID } = req.body; 23 | const story = new Stories({ 24 | userId: userID, 25 | postId: postID, 26 | storyId: uiv4(), 27 | }); 28 | const result = await story.save(); 29 | const data = await result.populate({ 30 | path: "postId", 31 | populate: { 32 | path: "userId", 33 | select: "Username ProfileUrl postId", 34 | }, 35 | }); 36 | 37 | const updatedPost = await User.findOneAndUpdate( 38 | { UserID: userID }, 39 | { $push: { Stories: result._id } }, 40 | { new: true } 41 | ); 42 | // let task = cron.schedule("*/30 * * * * *", async () => { 43 | // let trial = await Stories.findOne({ _id: result._id }); 44 | // if (!trial) { 45 | // const updatePost = await User.findOneAndUpdate( 46 | // { UserID: userID }, 47 | // { $pull: { Stories: result._id } }, 48 | // { new: true } 49 | // ); 50 | // console.log(updatePost); 51 | // if (updatePost) { 52 | // task.stop(); 53 | // console.log("task ended"); 54 | // } 55 | // } 56 | // }); 57 | if (!updatedPost) { 58 | return res 59 | .status(404) 60 | .json({ success: false, message: "Something went wrong" }); 61 | } else { 62 | res.status(200).json({ 63 | success: true, 64 | message: "Story added successfully", 65 | data: data, 66 | }); 67 | } 68 | } catch (error) { 69 | next(error); 70 | } 71 | }; 72 | 73 | export const deleteStory = async ( 74 | req: Request, 75 | res: Response, 76 | next: NextFunction 77 | ) => { 78 | try { 79 | const user = req.user.userID; 80 | const { storyId } = req.body; 81 | 82 | const deleteStory = await Stories.findOneAndDelete({ 83 | _id: storyId, 84 | userId: user, 85 | }); 86 | 87 | if (!deleteStory) { 88 | return res 89 | .status(404) 90 | .json({ success: false, message: "Story not found" }); 91 | } 92 | 93 | if (deleteStory) { 94 | const deletedId = deleteStory._id; 95 | const findUser = await User.findById(user); 96 | if (findUser) { 97 | const updatePost = await User.findOneAndUpdate( 98 | { UserID: user }, 99 | { $pull: { Stories: deletedId } }, 100 | { new: true } 101 | ); 102 | if (updatePost) { 103 | return res 104 | .status(200) 105 | .json({ success: true, message: "Story deleted successfully" }); 106 | } 107 | } 108 | } 109 | } catch (error) { 110 | next(error) 111 | } 112 | }; 113 | export const getStory = async (req: Request, res: Response, next: NextFunction) => { 114 | try { 115 | const user = req.user.userID; 116 | const story = await Stories.find({userId: user}).populate({ 117 | path: "postId", 118 | populate: { 119 | path: "userId", 120 | select: "Username ProfileUrl postId" 121 | } 122 | }) 123 | Logger.info(story); 124 | if (!story || story.length === 0) { 125 | return res.status(404).json({success: false, message: "Story not found"}) 126 | } 127 | res.status(200).json({success: true, message: "Story found", data: story}) 128 | 129 | } catch (error) { 130 | next(error) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import User from "../models/user.model"; 3 | import Following from "../models/following.model"; 4 | import { 5 | ImageUpload, 6 | validateBio, 7 | validateFollow, 8 | validateProfile, 9 | } from "../services/user.service"; 10 | import Follower from "../models/follower.model"; 11 | 12 | export const newFollow = async ( 13 | req: Request, 14 | res: Response, 15 | next: NextFunction 16 | ) => { 17 | try { 18 | const { error } = validateFollow(req.body); 19 | if (error) { 20 | return res 21 | .status(400) 22 | .json({ success: false, message: error.details[0].message }); 23 | } 24 | const { FollowingID } = req.body; 25 | const UserID = req.user.userID; 26 | if (UserID === FollowingID) { 27 | return res 28 | .status(400) 29 | .json({ success: false, message: "Can't follow self" }); 30 | } 31 | 32 | const existingfollow = await Following.findOne({ 33 | userID: UserID, 34 | followingID: FollowingID, 35 | }); 36 | if (!existingfollow) { 37 | //follow the new user 38 | const following = new Following({ 39 | userID: UserID, 40 | followingID: FollowingID, 41 | }); 42 | await following.save(); 43 | //move following to the users model 44 | const addFollowing = await User.findById(UserID); 45 | addFollowing?.Followings.push(following.followingID); 46 | await addFollowing!.save(); 47 | 48 | //update the other individuals followers list with MongoID 49 | try { 50 | const followers = await Follower.findOneAndUpdate( 51 | { 52 | userID: FollowingID, 53 | followerID: UserID, 54 | }, 55 | { userID: FollowingID, followerID: UserID }, 56 | { new: true, upsert: true } 57 | ); 58 | const addFollowers = await User.findById(FollowingID); 59 | addFollowers?.Followers.push(followers?.followerID); 60 | await addFollowers!.save(); 61 | } catch (error) { 62 | next(error); 63 | } 64 | 65 | res.status(200).json({ success: true, message: "Following" }); 66 | } else { 67 | res 68 | .status(400) 69 | .json({ success: false, message: "User has already been added" }); 70 | } 71 | } catch (error) { 72 | next(error); 73 | } 74 | }; 75 | 76 | export const unFollow = async ( 77 | req: Request, 78 | res: Response, 79 | next: NextFunction 80 | ) => { 81 | try { 82 | const { error } = validateFollow(req.body); 83 | if (error) { 84 | return res 85 | .status(400) 86 | .json({ success: false, message: error.details[0].message }); 87 | } 88 | const { FollowingID } = req.body; 89 | const UserID = req.user.userID; 90 | 91 | // Remove the following from the user's model 92 | const following = await Following.findOneAndDelete({ 93 | userID: UserID, 94 | followingID: FollowingID, 95 | }); 96 | 97 | if (!following) { 98 | return res 99 | .status(404) 100 | .json({ success: false, message: "User not in following list." }); 101 | } 102 | 103 | const userData = following.userID; 104 | const user = await User.findById(userData); 105 | if (!user) { 106 | return res 107 | .status(404) 108 | .json({ success: false, message: "Couldn't find user" }); 109 | } 110 | 111 | user.Followings = user.Followings.filter( 112 | (followingId) => followingId !== FollowingID 113 | ); 114 | await user.save(); 115 | 116 | // Remove the follower from the user's model 117 | try { 118 | const followers = await Follower.findOneAndDelete({ 119 | userID: FollowingID, 120 | followerID: UserID, 121 | }); 122 | 123 | if (followers) { 124 | const userId = followers.userID; 125 | const user = await User.findById(userId); 126 | 127 | if (user) { 128 | user.Followers = user.Followers.filter( 129 | (followerId) => followerId !== UserID 130 | ); 131 | await user.save(); 132 | } 133 | } 134 | } catch (error) { 135 | next(error); 136 | } 137 | 138 | res.status(200).json({ success: true, message: "Not Following" }); 139 | } catch (error) { 140 | next(error); 141 | } 142 | }; 143 | 144 | export const getFollowing = async ( 145 | req: Request, 146 | res: Response, 147 | next: NextFunction 148 | ) => { 149 | try { 150 | const following = await Following.find({ 151 | userID: req.user.userID, 152 | }) 153 | .populate({ 154 | path: "followingID", 155 | options: { 156 | select: "Username ProfileUrl UserID", // Exclude fields from the populated document 157 | sort: { name: -1 }, 158 | strictPopulate: false, 159 | }, 160 | }) 161 | .select("-_id -__v"); 162 | res.status(200).json({ success: true, following: following }); 163 | } catch (err: any) { 164 | next(err); 165 | } 166 | }; 167 | 168 | export const getFollowers = async ( 169 | req: Request, 170 | res: Response, 171 | next: NextFunction 172 | ) => { 173 | try { 174 | const followers = await Follower.find({ 175 | userID: req.user.userID, 176 | }) 177 | .populate({ 178 | path: "followerID", 179 | options: { 180 | select: "Username ProfileUrl UserID", // Exclude fields from the populated document 181 | sort: { name: -1 }, 182 | strictPopulate: false, 183 | }, 184 | }) 185 | .select("-_id -__v"); 186 | res.status(200).json({ success: true, followers: followers }); 187 | } catch (err: any) { 188 | next(err); 189 | } 190 | }; 191 | export const removeFollow = async ( 192 | req: Request, 193 | res: Response, 194 | next: NextFunction 195 | ) => { 196 | try { 197 | const { FollowerID } = req.body; 198 | const UserID = req.user.userID; 199 | const follower = await Follower.findOneAndDelete({ 200 | userID: UserID, 201 | followerID: FollowerID, 202 | }); 203 | if (!follower) 204 | res 205 | .status(404) 206 | .json({ success: false, message: "User not in followers list." }); 207 | try { 208 | const following = await Following.findOneAndDelete({ 209 | followingID: UserID, 210 | userID: FollowerID, 211 | }); 212 | } catch (err) { 213 | next(err); 214 | } 215 | 216 | res.status(200).json({ success: true, message: "Removed." }); 217 | } catch (error) { 218 | next(error); 219 | } 220 | }; 221 | //find a way to update the id field 222 | export const AddBio = async ( 223 | req: Request, 224 | res: Response, 225 | next: NextFunction 226 | ) => { 227 | try { 228 | const UserID = req.user.userID; 229 | const { error } = validateBio(req.body); 230 | if (error) res.status(400).json({ success: false, message: error.message }); 231 | const { Bio } = req.body; 232 | const user = await User.findByIdAndUpdate( 233 | UserID, 234 | { Bio: Bio }, 235 | { new: true, upsert: true } 236 | ); 237 | if (!user) 238 | res.status(404).json({ success: false, message: "User not found." }); 239 | user.save(); 240 | res.status(200).json({ success: true, message: "Bio Updated" }); 241 | } catch (error) { 242 | next(error); 243 | } 244 | }; 245 | 246 | export const deleteBio = async ( 247 | req: Request, 248 | res: Response, 249 | next: NextFunction 250 | ) => { 251 | try { 252 | const UserID = req.user.userID; 253 | const user = await User.findOneAndUpdate( 254 | { UserID: UserID }, 255 | { Bio: "" }, 256 | { new: true, upsert: true } 257 | ); 258 | if (!user) 259 | res.status(404).json({ success: false, message: "User not found." }); 260 | user.save(); 261 | res.status(200).json({ success: true, message: "Bio Deleted" }); 262 | } catch (error) { 263 | next(error); 264 | } 265 | }; 266 | 267 | export const getUser = async ( 268 | req: Request, 269 | res: Response, 270 | next: NextFunction 271 | ) => { 272 | try { 273 | const UserID = req.user.userID; 274 | const userData = await User.find({ UserID: UserID }) 275 | .select("-_id -__v -EmailVerified") 276 | .populate({ 277 | path: "Posts", 278 | options: { 279 | select: "-__v -EmailVerified -userId", // Exclude fields from the populated document 280 | sort: { name: -1 }, 281 | strictPopulate: false, 282 | }, 283 | }); 284 | res.status(200).json({ success: true, userData: userData }); 285 | if (!userData) 286 | res.status(404).json({ success: false, message: "User not found" }); 287 | } catch (error) { 288 | next(error); 289 | } 290 | }; 291 | 292 | export const editProfile = async ( 293 | req: Request, 294 | res: Response, 295 | next: NextFunction 296 | ) => { 297 | try { 298 | const { Name, Username, Bio } = req.body; 299 | const image = req.file; 300 | const UserID = req.user.userID; 301 | 302 | const { error } = validateProfile(req.body); 303 | if (error) res.status(400).json({ success: false, message: error.message }); 304 | 305 | let ImgUrl; 306 | if (image) { 307 | const link = await ImageUpload(image, next, UserID); 308 | ImgUrl = link; 309 | } 310 | const user = await User.findByIdAndUpdate( 311 | UserID, 312 | { Name: Name, Username: Username, Bio: Bio, ProfileUrl: ImgUrl }, 313 | { new: true, upsert: true } 314 | ); 315 | if (!user) 316 | res.status(404).json({ success: false, message: "User not found" }); 317 | user.save(); 318 | res.status(200).json({ success: true, message: "Profile Updated" }); 319 | } catch (error) { 320 | next(error); 321 | } 322 | }; 323 | 324 | export const getotherUser = async ( 325 | req: Request, 326 | res: Response, 327 | next: NextFunction 328 | ) => { 329 | try { 330 | const { userID } = req.query; 331 | const userData = await User.find({ UserID: userID }) 332 | .select("-_id -__v -EmailVerified") 333 | .populate({ 334 | path: "Posts", 335 | options: { 336 | select: "-__v -EmailVerified -userId", // Exclude fields from the populated document 337 | sort: { name: -1 }, 338 | strictPopulate: false, 339 | }, 340 | }); 341 | res.status(200).json({ success: true, userData: userData }); 342 | if (!userData) 343 | res.status(404).json({ success: false, message: "User not found" }); 344 | } catch (error) { 345 | next(error); 346 | } 347 | }; 348 | 349 | export const searchUser = async ( 350 | req: Request, 351 | res: Response, 352 | next: NextFunction 353 | ) => { 354 | try { 355 | const { username } = req.body; 356 | const text = new RegExp(username as string, "i"); 357 | 358 | const user = await User.find({ 359 | $or: [{ Username: text }, { Name: text }], 360 | }).select( 361 | "-_id -__v -EmailVerified -Posts -SavedPosts -Followers -Followings" 362 | ); 363 | res.status(200).json({ success: true, message: "User found", user: user }); 364 | if (!user) 365 | res.status(404).json({ success: false, message: "User not found" }); 366 | } catch (error) { 367 | next(error); 368 | } 369 | }; 370 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | "rootDir": "./src", /* Specify the root folder within your source files. */ 30 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./build", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["src/**/*"] 110 | } 111 | -------------------------------------------------------------------------------- /src/controllers/post.ts: -------------------------------------------------------------------------------- 1 | import Post from "../models/post.model"; 2 | import { NextFunction, Request, Response } from "express"; 3 | import { 4 | ImageUpload, 5 | validateComment, 6 | validateCommentID, 7 | validatePost, 8 | validatePostID, 9 | validateReply, 10 | } from "../services/post.service"; 11 | import User from "../models/user.model"; 12 | import SavedPost from "../models/savedpost.model"; 13 | import { v4 as uui4 } from "uuid"; 14 | import { Schema } from "mongoose"; 15 | import Comment from "../models/comment.model"; 16 | import { IReplies } from "../interfaces/post.interface"; 17 | import Logger from "../lib/logger"; 18 | 19 | export const newPost = async ( 20 | req: Request, 21 | res: Response, 22 | next: NextFunction 23 | ) => { 24 | try { 25 | const userId = req.user.userID; 26 | const { error } = validatePost(req.body); 27 | if (error) { 28 | res.status(400).json({ success: false, message: error.message }); 29 | } 30 | const { caption } = req.body; 31 | 32 | const images = req.files as Express.Multer.File[]; 33 | const imageUrls = await ImageUpload(images, next, userId); 34 | 35 | const post = new Post({ 36 | userId: userId, 37 | imageUrl: imageUrls, 38 | caption: caption, 39 | likes: [], 40 | comments: [], 41 | dateTime: Date.now(), 42 | saved: false, 43 | }); 44 | 45 | await post.save(); 46 | 47 | const updatedPost = await Post.findOneAndUpdate( 48 | { _id: post._id }, 49 | { $set: { postId: post._id } }, 50 | { new: true } 51 | ); 52 | const updateUser = await User.findById(userId); 53 | updateUser?.Posts.push(updatedPost?.postId); 54 | updateUser?.save(); 55 | 56 | return res 57 | .status(200) 58 | .json({ success: true, message: "Post created successfully", post }); 59 | } catch (error) { 60 | next(error); 61 | } 62 | }; 63 | //test this 👇🏽👇🏽👇🏽👇🏽 64 | export const getPost = async(req:Request, res:Response, next:NextFunction)=>{ 65 | try { 66 | const postId = req.params.postId; 67 | const post = await Post.findOne({postId:postId}) 68 | if(!post){ 69 | return res.status(404).json({success:false, message:"Post not found"}) 70 | } 71 | res.status(200).json({success:true, message:"Post found", post}) 72 | } catch (error) { 73 | next(error); 74 | } 75 | } 76 | export const deletePost = async ( 77 | req: Request, 78 | res: Response, 79 | next: NextFunction 80 | ) => { 81 | try { 82 | const userId = req.user.userID; 83 | const { error } = validatePostID(req.body); 84 | if (error) { 85 | res.status(400).json({ success: false, message: error.message }); 86 | } 87 | const { postID } = req.body; 88 | const post = await Post.findById(postID); 89 | if (!post) 90 | return res 91 | .status(404) 92 | .json({ success: false, message: "Post not found" }); 93 | const deletePost = await Post.findOneAndDelete({ 94 | postId: postID, 95 | userId: userId, 96 | }); 97 | if (deletePost) { 98 | const deletedpostID = deletePost.postId; 99 | const user = await User.findById(userId); 100 | 101 | if (user) { 102 | user.Posts = user.Posts.filter( 103 | (postId) => postId.toString() !== deletedpostID.toString() 104 | ); 105 | await user.save(); 106 | } 107 | } 108 | 109 | return res 110 | .status(200) 111 | .json({ success: true, message: "Post deleted successfully" }); 112 | } catch (error) { 113 | next(error); 114 | } 115 | }; 116 | export const addLikes = async ( 117 | req: Request, 118 | res: Response, 119 | next: NextFunction 120 | ) => { 121 | try { 122 | const userID = req.user.userID; 123 | const { error } = validatePostID(req.body); 124 | if (error) { 125 | res.status(400).json({ success: false, message: error.message }); 126 | } 127 | const { postID } = req.body; 128 | const isExisting = await Post.findOne({ postId: postID }); 129 | if (!isExisting) { 130 | res.status(404).json({ success: false, message: "Post not found" }); 131 | } 132 | if (isExisting) { 133 | if (isExisting.likes.includes(userID)) { 134 | return res.status(400).json({ 135 | success: false, 136 | message: "You have already liked this post", 137 | }); 138 | } 139 | isExisting?.likes.push(userID); 140 | isExisting?.save(); 141 | } 142 | 143 | res.status(200).json({ success: true, message: `Successfully added` }); 144 | } catch (error) { 145 | next(error); 146 | } 147 | }; 148 | export const unlike = async ( 149 | req: Request, 150 | res: Response, 151 | next: NextFunction 152 | ) => { 153 | try { 154 | const userID = req.user.userID; 155 | const { error } = validatePostID(req.body); 156 | if (error) { 157 | res.status(400).json({ success: false, message: error.message }); 158 | } 159 | const { postID } = req.body; 160 | const isExisting = await Post.findOne({ postId: postID }); 161 | 162 | if (!isExisting) { 163 | return res 164 | .status(404) 165 | .json({ success: false, message: "Post not found" }); 166 | } 167 | if (!isExisting.likes.includes(userID)) { 168 | return res.status(400).json({ 169 | success: false, 170 | message: "You have not liked this post", 171 | }); 172 | } 173 | 174 | const updatedLikes = isExisting.likes.filter((like) => like != userID); 175 | 176 | isExisting.likes = updatedLikes; 177 | 178 | await isExisting.save(); 179 | 180 | res.status(200).json({ success: true, message: `Successfully unliked` }); 181 | } catch (error) { 182 | next(error); 183 | } 184 | }; 185 | 186 | export const AddtoSaved = async ( 187 | req: Request, 188 | res: Response, 189 | next: NextFunction 190 | ) => { 191 | try { 192 | const userID = req.user.userID; 193 | const { error } = validatePostID(req.body); 194 | if (error) { 195 | res.status(400).json({ success: false, message: error.message }); 196 | } 197 | const { postID } = req.body; 198 | 199 | const isSaved = await SavedPost.findOne({ postId: postID, userId: userID }); 200 | if (!isSaved) { 201 | const savedPost = new SavedPost({ 202 | postId: postID, 203 | userId: userID, 204 | }); 205 | await savedPost.save(); 206 | 207 | // const filter = { postId: postID, userId: userID }; 208 | // const update = { saved: true }; 209 | 210 | // const result = await Post.findByIdAndUpdate( 211 | // postID , { $set: { saved:true }} 212 | // ); 213 | 214 | const user = await User.findOne({ UserID: userID }); 215 | if (user) { 216 | user.SavedPosts.push(savedPost._id); 217 | await user.save(); 218 | } 219 | 220 | res 221 | .status(200) 222 | .json({ success: true, message: "Post saved successfully" }); 223 | } 224 | } catch (error) { 225 | next(error); 226 | } 227 | }; 228 | 229 | export const RemoveFromSaved = async ( 230 | req: Request, 231 | res: Response, 232 | next: NextFunction 233 | ) => { 234 | try { 235 | const userID = req.user.userID; 236 | const { error } = validatePostID(req.body); 237 | if (error) { 238 | res.status(400).json({ success: false, message: error.message }); 239 | } 240 | const { postID } = req.body; 241 | const filter = { postId: postID, userId: userID }; 242 | Logger.info(filter); 243 | 244 | const removeSaved = await SavedPost.findOneAndDelete({ 245 | userId: userID, 246 | postId: postID, 247 | }); 248 | Logger.info(removeSaved); 249 | if (!removeSaved) { 250 | res.status(404).json({ success: false, message: "Post not found" }); 251 | } 252 | const user = await User.findOne({ UserID: userID }); 253 | if (user) { 254 | user.SavedPosts = user.SavedPosts.filter( 255 | (post: any) => post.toString() !== removeSaved?._id.toString() 256 | ); 257 | await user.save(); 258 | } 259 | return res.status(200).json({ 260 | status: true, 261 | message: "Post removed from saved", 262 | }); 263 | } catch (error) { 264 | next(error); 265 | } 266 | }; 267 | // work on comments here 👇🏽👇🏽👇🏽👇🏽 268 | 269 | export const createComment = async ( 270 | req: Request, 271 | res: Response, 272 | next: NextFunction 273 | ) => { 274 | try { 275 | const user = req.user.userID; 276 | const { postID, message } = req.body; 277 | const { error } = validateComment(req.body); 278 | if (error) { 279 | return res.status(400).json({ success: false, message: error.message }); 280 | } 281 | const isExisting = await Post.findOne({ postId: postID }); 282 | if (!isExisting) { 283 | return res 284 | .status(404) 285 | .json({ success: false, message: "Post not found" }); 286 | } 287 | 288 | const newComment = new Comment({ 289 | userId: user, 290 | postId: postID, 291 | comment: message, 292 | dateTime: Date.now(), 293 | }); 294 | await newComment.save(); 295 | 296 | const updatedPost = await Post.findOneAndUpdate( 297 | { postId: postID }, 298 | { $push: { comments: newComment._id } }, 299 | { new: true } 300 | ); 301 | if (!updatedPost) { 302 | return res 303 | .status(404) 304 | .json({ success: false, message: "Something went wrong" }); 305 | } 306 | res.status(200).json({ 307 | success: true, 308 | message: "Comment created successfully", 309 | comment: newComment, 310 | }); 311 | } catch (error) { 312 | next(error); 313 | } 314 | }; 315 | 316 | export const getAllComments = async ( 317 | req: Request, 318 | res: Response, 319 | next: NextFunction 320 | ) => { 321 | try { 322 | const { postID } = req.body; 323 | const { error } = validatePostID(req.body); 324 | if (error) { 325 | return res.status(400).json({ success: false, message: error.message }); 326 | } 327 | const isExisting = await Post.findOne({ postId: postID }); 328 | if (!isExisting) { 329 | return res 330 | .status(404) 331 | .json({ success: false, message: "Post not found" }); 332 | } 333 | const comments = await Comment.find({ postId: postID }) 334 | .select("-postId") 335 | .populate({ 336 | path: "userId", 337 | options: { 338 | select: "Username ProfileUrl ", 339 | sort: { name: -1 }, 340 | strictPopulate: false, 341 | }, 342 | }) 343 | .sort({ dateTime: -1 }); 344 | 345 | res.status(200).json({ 346 | success: true, 347 | message: "Comments retrieved successfully", 348 | comments: comments, 349 | }); 350 | } catch (error) { 351 | next(error); 352 | } 353 | }; 354 | 355 | export const deleteComment = async ( 356 | req: Request, 357 | res: Response, 358 | next: NextFunction 359 | ) => { 360 | try { 361 | const { error } = validateCommentID(req.body); 362 | if (error) { 363 | return res.status(400).json({ success: false, message: error.message }); 364 | } 365 | const { commentId } = req.body; 366 | const comment = await Comment.findOneAndDelete({ _id: commentId }); 367 | if (!comment) { 368 | return res 369 | .status(404) 370 | .json({ success: false, message: "Comment not found" }); 371 | } 372 | res 373 | .status(200) 374 | .json({ success: true, message: "Comment deleted successfully" }); 375 | } catch (error) { 376 | next(error); 377 | } 378 | }; 379 | export const replyComment = async ( 380 | req: Request, 381 | res: Response, 382 | next: NextFunction 383 | ) => { 384 | try { 385 | const { commentID, message } = req.body; 386 | const { error } = validateReply(req.body); 387 | if (error) { 388 | return res.status(400).json({ success: false, message: error.message }); 389 | } 390 | const user = req.user.userID; 391 | const comment = await Comment.findById(commentID); 392 | if (!comment) { 393 | return res 394 | .status(404) 395 | .json({ success: false, message: "Comment not found" }); 396 | } 397 | const newReply: IReplies = { 398 | replyId: uui4(), 399 | userId: user, 400 | commentId: commentID, 401 | reply: message, 402 | dateTime: Date.now(), 403 | }; 404 | 405 | comment.replies.push(newReply); 406 | await comment.save(); 407 | res 408 | .status(200) 409 | .json({ success: true, message: "Reply added successfully", comment: comment}); 410 | } catch (error) { 411 | next(error); 412 | } 413 | }; 414 | 415 | export const deleteReply = async ( 416 | req: Request, 417 | res: Response, 418 | next: NextFunction 419 | ) => { 420 | try { 421 | const user = req.user.userID; 422 | const { commentID, replyID } = req.body; 423 | 424 | const comment = await Comment.findById(commentID); 425 | if (!comment) { 426 | return res 427 | .status(404) 428 | .json({ success: false, message: "Comment not found" }); 429 | } 430 | const reply = comment.replies.find( 431 | (reply: IReplies) => reply.replyId.toString() === replyID.toString() 432 | ); 433 | if (!reply) { 434 | return res 435 | .status(404) 436 | .json({ success: false, message: "Reply not found" }); 437 | } 438 | comment.replies = comment.replies.filter( 439 | (reply: IReplies) => reply.replyId.toString() !== replyID.toString() 440 | ); 441 | comment.save(); 442 | res 443 | .status(200) 444 | .json({ 445 | success: true, 446 | message: "Reply deleted successfully", 447 | reply: comment.replies, 448 | }); 449 | } catch (error) { 450 | next(error); 451 | } 452 | }; 453 | 454 | /* 455 | 456 | 1. create IG story array 457 | 2. add posts to user stories 458 | 459 | 460 | 461 | 462 | */ 463 | 464 | // if (user){ 465 | // const index = user.Posts.indexOf(postId); 466 | // user.Posts.splice(index,1); 467 | // user.save(); 468 | // } 469 | --------------------------------------------------------------------------------