├── images └── dum.txt ├── logs └── dummy.txt ├── private └── dum.js ├── services ├── dum.js ├── readMessages.js ├── postServices.js ├── subredditRules.js ├── subredditDetails.js ├── topics.js ├── commentActionsServices.js ├── subredditSettings.js └── getPinnedPosts.js ├── videos └── dum.txt ├── jest.setup.js ├── .eslintignore ├── test ├── dum.js ├── dum.test.js ├── database.js ├── utils │ ├── subredditFlairs.test.js │ ├── verifyUser.test.js │ ├── generateRadnomUsername.test.js │ ├── createUser.test.js │ ├── passwordUtils.test.js │ ├── sendEmails.test.js │ ├── generateTokens.test.js │ └── subredditRules.test.js ├── middlewares │ ├── checkId.test.js │ ├── validationResult.test.js │ ├── getSubredditMiddleware.test.js │ ├── subredditDetails.test.js │ ├── verifySignUp.test.js │ ├── verifySubredditName.test.js │ ├── subredditRules.test.js │ └── verifyModerator.test.js └── services │ ├── userDetails.test.js │ ├── topicsServices.test.js │ ├── subredditModerationServices.test.js │ └── subredditRules.test.js ├── .gitattributes ├── assets ├── reddit.png └── coverage.png ├── .dockerignore ├── Dockerfile ├── spec └── support │ └── jasmine.json ├── seeds └── categories.js ├── config.json ├── notification.cjs ├── models ├── Topics.js ├── Category.js ├── VerifyToken.js ├── Conversation.js ├── Mention.js ├── PostReplies.js ├── Flair.js ├── Notification.js ├── Message.js ├── Comment.js └── Post.js ├── .nycrc.json ├── jsdoc.json ├── jest.config.js ├── utils ├── subredditFlairs.js ├── verifyUser.js ├── prepareLimit.js ├── files.js ├── passwordUtils.js ├── createUser.js ├── generateTokens.js ├── generateRandomUsername.js ├── subredditRules.js ├── messagesUtils.js ├── prepareUserListing.js ├── prepareSubredditListing.js └── prepareSubreddit.js ├── tests ├── endpoints │ ├── GenerateUsernameSpec.js │ ├── categorySpec.js │ └── readMessagesSpec.js ├── helpers │ └── reporter.js └── utils │ ├── verifyUserSpec.js │ ├── sendEmailsSpec.js │ ├── passwordUtilsSpec.js │ ├── generateTokensSpec.js │ └── subredditRulesUtilSpec.js ├── controllers ├── subredditDetails.js ├── HuserController.js ├── NgenerateUsername.js ├── HmessageController.js ├── categoryController.js ├── commentActionsController.js ├── BitemsActionsController.js ├── BpostActionsController.js ├── loginController.js ├── notificationController.js ├── searchController.js ├── BcommentController.js └── subredditRulesController.js ├── middleware ├── optionalToken.js ├── validationResult.js ├── getSubredditMiddleware.js ├── checkId.js ├── subredditDetails.js ├── NverifySubredditName.js ├── verifySignUp.js ├── subredditRules.js ├── NverifyModerator.js ├── pinnedPosts.js ├── NJoiningValidation.js ├── verifyPostActions.js └── postModeration.js ├── LICENSE ├── .eslintrc ├── Jenkinsfile ├── routes ├── categories.js ├── routes.js ├── comment-action.js └── itemsActions.js ├── package.json ├── .circleci └── config.yml ├── .gitignore └── app.js /images/dum.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logs/dummy.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /private/dum.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /services/dum.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /videos/dum.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000); 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | docs 2 | apidoc 3 | test 4 | seeds -------------------------------------------------------------------------------- /test/dum.js: -------------------------------------------------------------------------------- 1 | export function addNumbers(a, b) { 2 | return a + b; 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /assets/reddit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reddit-Replica/SW-Backend/HEAD/assets/reddit.png -------------------------------------------------------------------------------- /assets/coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Reddit-Replica/SW-Backend/HEAD/assets/coverage.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | videos 3 | images 4 | npm-debug.log 5 | .git 6 | *Dockerfile* 7 | *docker-compose* 8 | node_modules -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | CMD ["npm","run","start"] 14 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "tests", 3 | "spec_files": ["**/*[sS]pec.?(m)js"], 4 | "helpers": ["helpers/**/*.?(m)js"], 5 | "env": { 6 | "stopSpecOnExpectationFailure": false, 7 | "random": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /seeds/categories.js: -------------------------------------------------------------------------------- 1 | import { Categories } from "../services/categories.js"; 2 | 3 | export const categories = []; 4 | 5 | for (let i = 0; i < Categories.length; i++) { 6 | categories.push({ 7 | name: Categories[i], 8 | randomIndex: i, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "swaggerDefinition": { 3 | "openapi": "3.0.0", 4 | "info": { 5 | "title": "Read-it API", 6 | "version": "1.0.6", 7 | "description": "API Documentation for all endpoints" 8 | } 9 | }, 10 | "apis": ["./routes/*.js"] 11 | } 12 | -------------------------------------------------------------------------------- /notification.cjs: -------------------------------------------------------------------------------- 1 | // const admin = require("firebase-admin"); 2 | const FCM = require("fcm-node"); 3 | 4 | const serviceAccount = require("./private/privateKey.json"); 5 | // const certPath = admin.credential.cert(serviceAccount); 6 | const fcm = new FCM(serviceAccount); 7 | 8 | 9 | module.exports = fcm; 10 | -------------------------------------------------------------------------------- /models/Topics.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const topicsSchema = mongoose.Schema({ 5 | topicName: { 6 | type: String, 7 | required: true, 8 | }, 9 | }); 10 | 11 | const Topics = mongoose.model("Topics", topicsSchema); 12 | 13 | export default Topics; 14 | -------------------------------------------------------------------------------- /.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [".js"], 3 | "all": true, 4 | "include": [ 5 | "controllers/*.js", 6 | "middleware/*.js", 7 | "models/*.js", 8 | "routes/*.js", 9 | "utils/*.js" 10 | ], 11 | "exclude": ["**/*[sS]pec.?(m)js"], 12 | "reporter": ["html"], 13 | "report-dir": "coverage" 14 | } 15 | -------------------------------------------------------------------------------- /test/dum.test.js: -------------------------------------------------------------------------------- 1 | import { addNumbers } from "./dum.js"; 2 | import { connectDatabase, closeDatabaseConnection } from "./database.js"; 3 | describe("desc", () => { 4 | beforeAll(async () => { 5 | await connectDatabase(); 6 | }); 7 | afterAll(() => { 8 | closeDatabaseConnection(); 9 | }); 10 | test("Testing jest", () => { 11 | expect(addNumbers(2, 5)).toBe(7); 12 | 13 | expect(1 + 2).not.toBe(5); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["./middleware", "./models", "./utils", "./services"], 4 | "includePattern": ".js$", 5 | "excludePattern": "(node_modules/|docs)" 6 | }, 7 | "plugins": ["plugins/markdown"], 8 | "opts": { 9 | "recurse": true, 10 | "destination": "./docs/", 11 | "readme": "./README.md" 12 | }, 13 | "templates": { 14 | "cleverLinks": true, 15 | "monospaceLinks": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/database.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | const DB_URL = process.env.MONGO_URL_TESTING.trim(); 6 | 7 | export async function connectDatabase() { 8 | try { 9 | await mongoose.connect(DB_URL, { useNewUrlParser: true }); 10 | } catch (err) { 11 | console.log(err); 12 | } 13 | } 14 | 15 | export function closeDatabaseConnection() { 16 | mongoose.connection.close(); 17 | } 18 | -------------------------------------------------------------------------------- /models/Category.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const categorySchema = mongoose.Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | randomIndex: { 10 | type: Number, 11 | required: true, 12 | }, 13 | visited: { 14 | type: Boolean, 15 | required: true, 16 | default: false, 17 | }, 18 | }); 19 | 20 | const Category = mongoose.model("Category", categorySchema); 21 | 22 | export default Category; 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testTimeout: 30000, 3 | testEnvironment: "jest-environment-node", 4 | transform: {}, 5 | coverageReporters: ["clover", "json", "lcov", ["text", { skipFull: true }]], 6 | collectCoverageFrom: ["./services/**", "./utils/**", "./middleware/**"], 7 | coverageReporters: ["text-summary", "html"], 8 | reporters: [ 9 | "default", 10 | [ 11 | "./node_modules/jest-html-reporter", 12 | { 13 | pageTitle: "Read-it Test Report", 14 | }, 15 | ], 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /utils/subredditFlairs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A utility function used as a comparator for the built in sort function to sort the flairs based on their order 3 | * @param {Object} a the first flair object 4 | * @param {Object} b the second flair object 5 | * @returns {Integer} -1 if the first is smaller, 1 if the second is smaller, 0 if they are equal 6 | */ 7 | export function compareFlairs(a, b) { 8 | if (a.flairOrder < b.flairOrder) { 9 | return -1; 10 | } 11 | if (a.flairOrder > b.flairOrder) { 12 | return 1; 13 | } 14 | return 0; 15 | } 16 | -------------------------------------------------------------------------------- /tests/endpoints/GenerateUsernameSpec.js: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import app from "../../app.js"; 3 | const request = supertest(app); 4 | 5 | // eslint-disable-next-line max-statements 6 | describe("Testing Generating random usernames", () => { 7 | it("Generate random username", async () => { 8 | const response = await request.get("/random-username"); 9 | const username1 = response.body.username; 10 | const response2 = await request.get("/random-username"); 11 | const username2 = response2.body.username; 12 | expect(username1).not.toEqual(username2); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /models/VerifyToken.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const tokenSchema = mongoose.Schema({ 5 | userId: { 6 | type: String, 7 | required: true, 8 | }, 9 | token: { 10 | type: String, 11 | required: true, 12 | }, 13 | expireAt: { 14 | type: Date, 15 | required: true, 16 | default: Date.now() + 3600000, // token will be expired after one hour 17 | }, 18 | type: { 19 | type: String, 20 | required: true, 21 | }, 22 | }); 23 | 24 | const Token = mongoose.model("Token", tokenSchema); 25 | 26 | export default Token; 27 | -------------------------------------------------------------------------------- /controllers/subredditDetails.js: -------------------------------------------------------------------------------- 1 | import { getSubredditDetails } from "../services/subredditDetails.js"; 2 | 3 | const subredditDetails = async (req, res) => { 4 | try { 5 | const subreddit = req.subreddit; 6 | const subbredditDetails = await getSubredditDetails( 7 | subreddit, 8 | req.loggedIn, 9 | req.payload 10 | ); 11 | res.status(200).json(subbredditDetails); 12 | } catch (err) { 13 | console.log(err); 14 | if (err.statusCode) { 15 | res.status(err.statusCode).json({ error: err.message }); 16 | } else { 17 | res.status(500).json("Internal server error"); 18 | } 19 | } 20 | }; 21 | 22 | export default { 23 | subredditDetails, 24 | }; 25 | -------------------------------------------------------------------------------- /controllers/HuserController.js: -------------------------------------------------------------------------------- 1 | import { listingBannedUsers } from "../services/userListing.js"; 2 | 3 | const getBannedUsers = async (req, res) => { 4 | try { 5 | const { before, after, limit } = req.query; 6 | const result = await listingBannedUsers( 7 | limit, 8 | before, 9 | after, 10 | req.subreddit 11 | ); 12 | 13 | res.status(200).json(result); 14 | } catch (error) { 15 | console.log(error.message); 16 | if (error.statusCode) { 17 | res.status(error.statusCode).json({ error: error.message }); 18 | } else { 19 | res.status(500).json("Internal server error"); 20 | } 21 | } 22 | }; 23 | 24 | export default { 25 | getBannedUsers, 26 | }; 27 | -------------------------------------------------------------------------------- /tests/endpoints/categorySpec.js: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import app from "../../app.js"; 3 | import Category from "../../models/Category.js"; 4 | 5 | const request = supertest(app); 6 | 7 | describe("Testing category endpoints", () => { 8 | afterAll(async () => { 9 | await Category.deleteMany({}); 10 | }); 11 | 12 | it("Get all categories", async () => { 13 | const response = await request.get("/saved-categories"); 14 | const categoriesCount = await Category.countDocuments(); 15 | expect(response.statusCode).toEqual(200); 16 | expect(categoriesCount).toEqual(30); 17 | expect(response.body).toBeDefined(); 18 | expect(response.body.length).toEqual(30); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/utils/subredditFlairs.test.js: -------------------------------------------------------------------------------- 1 | import { compareFlairs } from "../../utils/subredditFlairs.js"; 2 | 3 | describe("Testing subreddit flairs utils", () => { 4 | it("compareFlairs method should exist", () => { 5 | expect(compareFlairs).toBeDefined(); 6 | }); 7 | 8 | it("compareFlairs method should return -1", () => { 9 | expect(compareFlairs({ flairOrder: 1 }, { flairOrder: 2 })).toBe(-1); 10 | }); 11 | 12 | it("compareFlairs method should return 1", () => { 13 | expect(compareFlairs({ flairOrder: 4 }, { flairOrder: 2 })).toBe(1); 14 | }); 15 | 16 | it("compareFlairs method should return 0", () => { 17 | expect(compareFlairs({ flairOrder: 4 }, { flairOrder: 4 })).toBe(0); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /middleware/optionalToken.js: -------------------------------------------------------------------------------- 1 | import verifyUser from "../utils/verifyUser.js"; 2 | 3 | /** 4 | * Middleware used to check if there is a token in the request 5 | * header and if yes, then userId is passed with the request and 6 | * a flag to indicate that the user is logged in 7 | * 8 | * @param {Object} req Request object 9 | * @param {Object} res Response object 10 | * @param {function} next Next function 11 | * @returns {void} 12 | */ 13 | export function optionalToken(req, _res, next) { 14 | const authResult = verifyUser(req); 15 | if (authResult) { 16 | req.loggedIn = true; 17 | req.userId = authResult.userId; 18 | req.payload = authResult; 19 | } else { 20 | req.loggedIn = false; 21 | } 22 | next(); 23 | } 24 | -------------------------------------------------------------------------------- /utils/verifyUser.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | /** 4 | * This function checks for the authorization of a request 5 | * and extracts the token from the body for verification with the help of jwt 6 | * 7 | * @param {object} req The request made 8 | * @returns {object} Returns the decoded payload of the token 9 | * containing the userId and username, else null 10 | */ 11 | export default function verifyUser(req) { 12 | const authorizationHeader = req.headers?.authorization; 13 | const token = authorizationHeader?.split(" ")[1]; 14 | if (!token) { 15 | return null; 16 | } 17 | try { 18 | const decodedPayload = jwt.verify(token, process.env.TOKEN_SECRET); 19 | return decodedPayload; 20 | } catch (err) { 21 | return null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /utils/prepareLimit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function used to prepare the limit that will be used by mongoose to limit the results 3 | * 4 | * @param {Number} listingLimit Time interval that we want to get the post in 5 | * @returns {Object} Object that will be used by mongoose to limit the results 6 | */ 7 | export function prepareLimit(listingLimit) { 8 | let result = null; 9 | if (!listingLimit && listingLimit !== 0) { 10 | result = 25; 11 | } else { 12 | listingLimit = parseInt(listingLimit); 13 | if (isNaN(listingLimit)) { 14 | result = 25; 15 | } else if (listingLimit > 100) { 16 | result = 100; 17 | } else if (listingLimit <= 0) { 18 | result = 1; 19 | } else { 20 | result = listingLimit; 21 | } 22 | } 23 | return result; 24 | } 25 | -------------------------------------------------------------------------------- /middleware/validationResult.js: -------------------------------------------------------------------------------- 1 | import { validationResult } from "express-validator"; 2 | 3 | /** 4 | * Middleware used to check the request if there is an error 5 | * it will send a response with status code 400 with all errors 6 | * 7 | * @param {Object} req Request object 8 | * @param {Object} res Response object 9 | * @param {function} next Next function 10 | * @returns {void} 11 | */ 12 | export function validateRequestSchema(req, res, next) { 13 | const result = validationResult(req); 14 | if (!result.isEmpty()) { 15 | let message = result.array()[0].msg; 16 | 17 | for (let i = 1; i < result.array().length; i++) { 18 | message += ` - ${result.array()[i].msg}`; 19 | } 20 | 21 | return res.status(400).json({ error: message }); 22 | } 23 | next(); 24 | } 25 | -------------------------------------------------------------------------------- /controllers/NgenerateUsername.js: -------------------------------------------------------------------------------- 1 | import { generateRandomUsernameUtil } from "../utils/generateRandomUsername.js"; 2 | 3 | const generateRandomUsername = async (req, res) => { 4 | try { 5 | let numberOfNames = 1; 6 | if (req.query.count) { 7 | numberOfNames = req.query.count; 8 | } 9 | const randomUsernames = []; 10 | for (let i = 0; i < numberOfNames; i++) { 11 | const RandomUsername = await generateRandomUsernameUtil(); 12 | if (RandomUsername === "Couldn't generate") { 13 | throw new Error("Couldn't generate"); 14 | } 15 | randomUsernames.push(RandomUsername); 16 | } 17 | return res.status(200).json({ usernames: randomUsernames }); 18 | } catch (err) { 19 | res.status(500).json("Internal server error"); 20 | } 21 | }; 22 | 23 | export default { 24 | generateRandomUsername, 25 | }; 26 | -------------------------------------------------------------------------------- /models/Conversation.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const conversationSchema = mongoose.Schema({ 5 | latestDate: { 6 | type: Date, 7 | }, 8 | subject: { 9 | type: String, 10 | required: true, 11 | }, 12 | firstUsername: { 13 | type: String, 14 | required: true, 15 | }, 16 | secondUsername: { 17 | type: String, 18 | required: true, 19 | }, 20 | messages: [ 21 | { 22 | type: Schema.Types.ObjectId, 23 | ref: "Messages", 24 | }, 25 | ], 26 | isFirstNameUser: { 27 | type: Boolean, 28 | required: true, 29 | }, 30 | isSecondNameUser: { 31 | type: Boolean, 32 | required: true, 33 | }, 34 | }); 35 | 36 | const Conversation = mongoose.model("Conversation", conversationSchema); 37 | 38 | export default Conversation; 39 | -------------------------------------------------------------------------------- /middleware/getSubredditMiddleware.js: -------------------------------------------------------------------------------- 1 | import Subreddit from "../models/Community.js"; 2 | 3 | /** 4 | * A middleware used to make sure that the provided subreddit name exists 5 | * If that subreddit exists it adds it to the request object to make the next middleware access it 6 | * It it doesn't exist then it returns a response with status code 404 and error message 7 | * 8 | * @param {Object} req Request object 9 | * @param {Object} res Response object 10 | * @param {function} next Next function 11 | * @returns {void} 12 | */ 13 | export async function getBodySubreddit(req, res, next) { 14 | const subreddit = await Subreddit.findOne({ 15 | title: req.body.subreddit, 16 | deletedAt: null, 17 | }); 18 | if (!subreddit) { 19 | res.status(404).json({ 20 | error: "Subreddit not found", 21 | }); 22 | } else { 23 | req.subreddit = subreddit; 24 | next(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /middleware/checkId.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | /** 4 | * Middleware used to check the id sent in the request if it was 5 | * a valid mongo ObjectId. 6 | * If it was invalid a status code of 400 will be sent. 7 | * 8 | * @param {Object} req Request object 9 | * @param {Object} res Response object 10 | * @param {function} next Next function 11 | * @returns {void} 12 | */ 13 | export function checkId(req, res, next) { 14 | const paramId = req.params.id; 15 | const bodyId = req.body.id; 16 | 17 | if (paramId) { 18 | if (!mongoose.Types.ObjectId.isValid(paramId)) { 19 | return res.status(400).json({ 20 | error: "In valid id", 21 | }); 22 | } 23 | } 24 | 25 | if (bodyId) { 26 | if (!mongoose.Types.ObjectId.isValid(bodyId)) { 27 | return res.status(400).json({ 28 | error: "In valid id", 29 | }); 30 | } 31 | } 32 | 33 | next(); 34 | } 35 | -------------------------------------------------------------------------------- /models/Mention.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const mentionSchema = mongoose.Schema({ 5 | postId: { 6 | type: Schema.Types.ObjectId, 7 | ref: "Post", 8 | required: true, 9 | }, 10 | commentId: { 11 | type: Schema.Types.ObjectId, 12 | ref: "Comment", 13 | required: true, 14 | }, 15 | receiverUsername: { 16 | type: String, 17 | required: true, 18 | }, 19 | createdAt: { 20 | type: Date, 21 | required: true, 22 | }, 23 | editedAt: { 24 | type: Date, 25 | }, 26 | deletedAt: { 27 | type: Date, 28 | }, 29 | type: { 30 | type: String, 31 | required: true, 32 | default: "Mention", 33 | }, 34 | isRead: { 35 | type: Boolean, 36 | required: true, 37 | default: false, 38 | }, 39 | }); 40 | 41 | const Mention = mongoose.model("Mention", mentionSchema); 42 | 43 | export default Mention; 44 | -------------------------------------------------------------------------------- /models/PostReplies.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const postRepliesSchema = mongoose.Schema({ 5 | postId: { 6 | type: Schema.Types.ObjectId, 7 | ref: "Post", 8 | required: true, 9 | }, 10 | commentId: { 11 | type: Schema.Types.ObjectId, 12 | ref: "Comment", 13 | required: true, 14 | }, 15 | receiverUsername: { 16 | type: String, 17 | required: true, 18 | }, 19 | createdAt: { 20 | type: Date, 21 | required: true, 22 | }, 23 | editedAt: { 24 | type: Date, 25 | }, 26 | deletedAt: { 27 | type: Date, 28 | }, 29 | type: { 30 | type: String, 31 | required: true, 32 | default: "post reply", 33 | }, 34 | isRead: { 35 | type: Boolean, 36 | required: true, 37 | default: false, 38 | }, 39 | }); 40 | 41 | const PostReplies = mongoose.model("PostReplies", postRepliesSchema); 42 | 43 | export default PostReplies; 44 | -------------------------------------------------------------------------------- /utils/files.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | /* istanbul ignore file */ 3 | export const fileStorage = multer.diskStorage({ 4 | destination: (_req, file, cb) => { 5 | if (file.mimetype.split("/")[0] === "video") { 6 | cb(null, "videos"); 7 | } else { 8 | cb(null, "images"); 9 | } 10 | }, 11 | filename: (_req, file, cb) => { 12 | cb( 13 | null, 14 | new Date().toISOString().replace(/:/g, "-") + "-" + file.originalname 15 | ); 16 | }, 17 | }); 18 | 19 | export const fileFilter = (_req, file, cb) => { 20 | if ( 21 | file.mimetype === "image/png" || 22 | file.mimetype === "image/jpg" || 23 | file.mimetype === "image/jpeg" || 24 | file.mimetype === "image/gif" || 25 | file.mimetype === "video/mp4" || 26 | file.mimetype === "video/webm" || 27 | file.mimetype === "video/x-m4v" || 28 | file.mimetype === "video/x-matroska" 29 | ) { 30 | cb(null, true); 31 | } else { 32 | cb(null, false); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /tests/helpers/reporter.js: -------------------------------------------------------------------------------- 1 | import { SpecReporter } from "jasmine-spec-reporter"; 2 | import jasmineReporter from "jasmine-reporters"; 3 | 4 | jasmine.getEnv().clearReporters(); // remove default reporter logs 5 | jasmine.getEnv().addReporter( 6 | new jasmineReporter.JUnitXmlReporter({ 7 | // setup the output path for the junit reports 8 | savePath: "test_output/", 9 | 10 | // conslidate all true: 11 | // output/junitresults.xml 12 | // 13 | // conslidate all set to false: 14 | // output/junitresults-example1.xml 15 | // output/junitresults-example2.xml 16 | consolidateAll: false, 17 | }) 18 | ); 19 | 20 | jasmine.getEnv().addReporter( 21 | new jasmineReporter.NUnitXmlReporter({ 22 | savePath: "test_output2/", 23 | }) 24 | ); 25 | 26 | jasmine.getEnv().addReporter( 27 | new SpecReporter({ 28 | // add jasmine-spec-reporter 29 | spec: { 30 | displayPending: true, 31 | }, 32 | summary: { 33 | displayDuration: false, 34 | }, 35 | }) 36 | ); 37 | -------------------------------------------------------------------------------- /middleware/subredditDetails.js: -------------------------------------------------------------------------------- 1 | import Subreddit from "../models/Community.js"; 2 | 3 | /** 4 | * A middleware used to make sure that the provided subreddit name exists 5 | * If that subreddit exists it adds it to the request object to make the next middleware access it 6 | * It it doesn't exist then it returns a response with status code 404 and error message 7 | * 8 | * @param {Object} req Request object 9 | * @param {Object} res Response object 10 | * @param {function} next Next function 11 | * @returns {void} 12 | */ 13 | 14 | const checkSubreddit = async (req, res, next) => { 15 | const subredditName = req.params.subreddit; 16 | const subbredditObject = await Subreddit.findOne({ 17 | title: subredditName, 18 | deletedAt: null, 19 | }); 20 | if (!subbredditObject || subbredditObject.deletedAt) { 21 | res.status(404).json({ 22 | error: "Subreddit not found!", 23 | }); 24 | } else { 25 | req.subreddit = subbredditObject; 26 | next(); 27 | } 28 | }; 29 | 30 | export default { 31 | checkSubreddit, 32 | }; 33 | -------------------------------------------------------------------------------- /utils/passwordUtils.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | 3 | /** 4 | * This function accepts a password and uses the 5 | * bcrypt hashSync function to hash it 6 | * 7 | * @param {string} password The password to be hashed 8 | * @returns {string} The hashed password 9 | */ 10 | export function hashPassword(password) { 11 | const hashedPass = bcrypt.hashSync( 12 | password + process.env.BCRYPT_PASSWORD, 13 | parseInt(process.env.SALT_ROUNDS) 14 | ); 15 | 16 | return hashedPass; 17 | } 18 | 19 | /** 20 | * This function takes 2 string passwords and 21 | * compares them using the bcrypt compareSync function 22 | * 23 | * @param {string} password The first password 24 | * @param {string} dbPassword The second password which is the correct one 25 | * @returns {boolean} True if both passwords match, otherwise False 26 | */ 27 | export function comparePasswords(password, dbPassword) { 28 | const result = bcrypt.compareSync( 29 | password + process.env.BCRYPT_PASSWORD, 30 | dbPassword 31 | ); 32 | 33 | return result; 34 | } 35 | -------------------------------------------------------------------------------- /models/Flair.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const flairSchema = mongoose.Schema({ 5 | flairName: { 6 | type: String, 7 | required: true, 8 | }, 9 | subreddit: { 10 | type: Schema.Types.ObjectId, 11 | ref: "Subreddit", 12 | }, 13 | flairOrder: { 14 | type: Number, 15 | required: true, 16 | }, 17 | backgroundColor: { 18 | type: String, 19 | }, 20 | textColor: { 21 | type: String, 22 | }, 23 | createdAt: { 24 | type: Date, 25 | required: true, 26 | // default: Date.now(), 27 | }, 28 | editedAt: { 29 | type: Date, 30 | }, 31 | deletedAt: { 32 | type: Date, 33 | }, 34 | flairSettings: { 35 | modOnly: { 36 | type: Boolean, 37 | default: false, 38 | }, 39 | allowUserEdits: { 40 | type: Boolean, 41 | default: false, 42 | }, 43 | flairType: { 44 | type: String, 45 | }, 46 | emojisLimit: { 47 | type: Number, 48 | }, 49 | }, 50 | }); 51 | 52 | const Flair = mongoose.model("Flair", flairSchema); 53 | 54 | export default Flair; 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Zeyad Tarek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/utils/verifyUser.test.js: -------------------------------------------------------------------------------- 1 | import verifyUser from "../../utils/verifyUser.js"; 2 | import { generateJWT } from "../../utils/generateTokens.js"; 3 | import dotenv from "dotenv"; 4 | dotenv.config(); 5 | 6 | describe("Testing verifying a user is logged in", () => { 7 | it("should have verifyUser method", () => { 8 | expect(verifyUser).toBeDefined(); 9 | }); 10 | 11 | it("check if verifyUser verifies a valid jwt", () => { 12 | const user = { 13 | id: "mongodbId", 14 | username: "Hamdy", 15 | }; 16 | const token = generateJWT(user); 17 | const req = { 18 | headers: { 19 | authorization: `Bearer ${token}`, 20 | }, 21 | }; 22 | const decodedPayload = verifyUser(req); 23 | expect(decodedPayload.userId).toEqual("mongodbId"); 24 | expect(decodedPayload.username).toEqual("Hamdy"); 25 | }); 26 | 27 | it("Send an invalid token to verifyUser", () => { 28 | const req = { 29 | headers: { 30 | authorization: "Bearer invalidToken", 31 | }, 32 | }; 33 | const decodedPayload = verifyUser(req); 34 | expect(decodedPayload).toBeNull(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/utils/generateRadnomUsername.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /* eslint-disable max-statements */ 3 | /* eslint-disable max-len */ 4 | import {generateRandomUsernameUtil} from "../../utils/generateRandomUsername.js" 5 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 6 | import User from "./../../models/User.js"; 7 | import Subreddit from "../../models/Community.js"; 8 | import Post from "../../models/Post.js"; 9 | 10 | // eslint-disable-next-line max-statements 11 | describe("Testing generate random username function", () => { 12 | 13 | it("should have generateRandomUsernameUtil function", () => { 14 | expect(generateRandomUsernameUtil).toBeDefined(); 15 | }); 16 | 17 | it("try generateRandomUsernameUtil ", async () => { 18 | const result1=generateRandomUsernameUtil(); 19 | const result2=generateRandomUsernameUtil(); 20 | expect(result1).not.toBeNull(); 21 | expect(result2).not.toBeNull(); 22 | let changed=true; 23 | if(result1!==result2){ 24 | changed=false; 25 | } 26 | expect(changed).toEqual(false); 27 | }); 28 | 29 | }); 30 | -------------------------------------------------------------------------------- /models/Notification.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const notificationSchema = mongoose.Schema({ 5 | ownerId: { 6 | type: Schema.Types.ObjectId, 7 | ref: "User", 8 | required: true, 9 | }, 10 | sendingUserId: { 11 | type: Schema.Types.ObjectId, 12 | ref: "User", 13 | required: true, 14 | }, 15 | followingUsername: { 16 | type: String, 17 | }, 18 | postId: { 19 | type: Schema.Types.ObjectId, 20 | }, 21 | commentId: { 22 | type: Schema.Types.ObjectId, 23 | }, 24 | type: { 25 | type: String, 26 | required: true, 27 | }, 28 | data: { 29 | required: true, 30 | type: String, 31 | }, 32 | link: { 33 | required: true, 34 | type: String, 35 | }, 36 | read: { 37 | required: true, 38 | type: Boolean, 39 | default: false, 40 | }, 41 | hidden: { 42 | type: Boolean, 43 | required: true, 44 | default: false, 45 | }, 46 | date: { 47 | type: Date, 48 | required: true, 49 | }, 50 | }); 51 | const Notification = mongoose.model("Notification", notificationSchema); 52 | 53 | export default Notification; 54 | -------------------------------------------------------------------------------- /utils/createUser.js: -------------------------------------------------------------------------------- 1 | import { generateJWT, generateVerifyToken } from "./generateTokens.js"; 2 | import { sendVerifyEmail } from "./sendEmails.js"; 3 | 4 | /** 5 | * Functin used to send the verification email after signing in 6 | * and then create a jwt and send it back to the user in the response 7 | * 8 | * @param {Object} user User object 9 | * @param {Boolean} sendEmail Flag to know if you will send a verify email or not 10 | * @returns {Object} Response to the request containing [statusCode, body] 11 | */ 12 | export async function finalizeCreateUser(user, sendEmail) { 13 | // Create the verify token and send an email to the user 14 | if (sendEmail) { 15 | const verifyToken = await generateVerifyToken(user._id, "verifyEmail"); 16 | const sentEmail = sendVerifyEmail(user, verifyToken); 17 | 18 | if (!sentEmail) { 19 | return { 20 | statusCode: 400, 21 | body: { 22 | error: "Could not send the verification email", 23 | }, 24 | }; 25 | } 26 | } 27 | 28 | const token = generateJWT(user); 29 | return { 30 | statusCode: 201, 31 | body: { username: user.username, token: token }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "array-bracket-spacing": [2, "never"], 7 | "block-scoped-var": 2, 8 | "brace-style": [2, "1tbs"], 9 | "camelcase": 1, 10 | "computed-property-spacing": [2, "never"], 11 | "curly": 2, 12 | "indent": ["error", 2, { "SwitchCase": 1 }], 13 | "eol-last": 2, 14 | "eqeqeq": [2, "smart"], 15 | "max-depth": [1, 3], 16 | "max-len": [ 17 | "error", 18 | { 19 | // "code": 60, 20 | "tabWidth": 2, 21 | "ignoreComments": true 22 | // "ignoreUrls": true, 23 | // "ignoreStrings": true, 24 | // "ignoreTemplateLiterals": true 25 | } 26 | ], 27 | "max-statements": [1, 15], 28 | "new-cap": 1, 29 | "no-extend-native": 2, 30 | "no-mixed-spaces-and-tabs": 2, 31 | "no-trailing-spaces": 2, 32 | "no-unused-vars": 1, 33 | "no-use-before-define": [2, "nofunc"], 34 | "object-curly-spacing": [2, "always"], 35 | "quotes": [2, "double", "avoid-escape"], 36 | "semi": [2, "always"], 37 | "keyword-spacing": [2, { "before": true, "after": true }], 38 | "space-unary-ops": 2 39 | }, 40 | "parserOptions": { 41 | "sourceType": "module", 42 | "ecmaVersion": "latest" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /controllers/HmessageController.js: -------------------------------------------------------------------------------- 1 | import { body, check } from "express-validator"; 2 | import { 3 | readPostReplies, 4 | readReceivedMessages, 5 | readUsernameMentions, 6 | } from "../services/readMessages.js"; 7 | 8 | const messageValidator = [ 9 | body("type").not().isEmpty().withMessage("Message type can't be empty"), 10 | check("type").isIn(["Post Replies", "Messages", "Username Mentions"]), 11 | ]; 12 | 13 | const readAllMessages = async (req, res) => { 14 | const userId = req.payload.userId; 15 | const type = req.body.type; 16 | try { 17 | type === "Messages" && (await readReceivedMessages(userId)); 18 | type === "Post Replies" && (await readPostReplies(userId)); 19 | type === "Username Mentions" && (await readUsernameMentions(userId)); 20 | return res.status(200).json("Messages marked as read successfully"); 21 | } catch (error) { 22 | console.log(error.message); 23 | if (error.statusCode) { 24 | if (error.statusCode === 400) { 25 | res.status(error.statusCode).json({ error: error.message }); 26 | } else { 27 | res.status(error.statusCode).json(error.message); 28 | } 29 | } else { 30 | res.status(500).json("Internal server error"); 31 | } 32 | } 33 | }; 34 | 35 | export default { 36 | messageValidator, 37 | readAllMessages, 38 | }; 39 | -------------------------------------------------------------------------------- /middleware/NverifySubredditName.js: -------------------------------------------------------------------------------- 1 | import Subreddit from "../models/Community.js"; 2 | 3 | /** 4 | * Middleware used to check if the name of the subreddit was used before or not 5 | * It searches for the name of the subreddit that it took from the request 6 | * this search is done in all the subreddits that created before 7 | * if it's found so it send status code 400 saying title is already in use 8 | * else it will continue to the next middleware 9 | * 10 | * @param {Object} req Request object 11 | * @param {Object} res Response object 12 | * @param {function} next Next function 13 | * @returns {void} 14 | */ 15 | 16 | export async function checkDuplicateSubredditTitle(req, res, next) { 17 | try { 18 | const subreddit = await Subreddit.findOne({ 19 | title: req.body.subredditName, 20 | deletedAt: undefined, 21 | }); 22 | if (!subreddit) { 23 | next(); 24 | return; 25 | } 26 | if (subreddit.title && !subreddit.deletedAt) { 27 | // eslint-disable-next-line max-len 28 | return res 29 | .status(409) 30 | .json({ error: "Subreddit's name is already taken" }); 31 | } 32 | } catch (err) { 33 | if (err.cause) { 34 | return res.status(err.cause).json({ 35 | error: err.message, 36 | }); 37 | } else { 38 | return res.status(500).json("Internal Server Error"); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/utils/verifyUserSpec.js: -------------------------------------------------------------------------------- 1 | import verifyUser from "../../utils/verifyUser.js"; 2 | import { generateJWT } from "../../utils/generateTokens.js"; 3 | import User from "../../models/User.js"; 4 | import Token from "./../../models/VerifyToken.js"; 5 | import supertest from "supertest"; 6 | import app from "../../app.js"; 7 | supertest(app); 8 | 9 | describe("Testing verifying a user is logged in", () => { 10 | afterAll(async () => { 11 | await User.deleteMany({}); 12 | await Token.deleteMany({}); 13 | }); 14 | 15 | it("should have verifyUser method", () => { 16 | expect(verifyUser).toBeDefined(); 17 | }); 18 | 19 | it("check if verifyUser verifies a valid jwt", () => { 20 | const user = { 21 | id: "mongodbId", 22 | username: "Hamdy", 23 | }; 24 | const token = generateJWT(user); 25 | const req = { 26 | headers: { 27 | authorization: `Bearer ${token}`, 28 | }, 29 | }; 30 | const decodedPayload = verifyUser(req); 31 | expect(decodedPayload.userId).toEqual("mongodbId"); 32 | expect(decodedPayload.username).toEqual("Hamdy"); 33 | }); 34 | 35 | it("Send an invalid token to verifyUser", () => { 36 | const req = { 37 | headers: { 38 | authorization: "Bearer invalidToken", 39 | }, 40 | }; 41 | const decodedPayload = verifyUser(req); 42 | expect(decodedPayload).toBeNull(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /controllers/categoryController.js: -------------------------------------------------------------------------------- 1 | import { 2 | getSortedCategories, 3 | insertCategoriesIfNotExists, 4 | getRandomSubreddits, 5 | } from "../services/categories.js"; 6 | import { getLoggedInUser } from "../services/search.js"; 7 | 8 | const getAllCategories = async (req, res) => { 9 | try { 10 | await insertCategoriesIfNotExists(); 11 | const categories = await getSortedCategories(); 12 | return res.status(200).json(categories); 13 | } catch (err) { 14 | return res.status(500).json("Internal server error"); 15 | } 16 | }; 17 | 18 | const getSubredditsFromRandomCategories = async (req, res) => { 19 | try { 20 | let loggedInUser = undefined; 21 | if (req.loggedIn) { 22 | const user = await getLoggedInUser(req.userId); 23 | if (user) { 24 | loggedInUser = user; 25 | } 26 | } 27 | const result = await getRandomSubreddits(loggedInUser); 28 | return res.status(200).json(result); 29 | } catch (error) { 30 | if (error.statusCode) { 31 | if (error.statusCode === 400) { 32 | res.status(error.statusCode).json({ error: error.message }); 33 | } else { 34 | res.status(error.statusCode).json(error.message); 35 | } 36 | } else { 37 | res.status(500).json("Internal server error"); 38 | } 39 | } 40 | }; 41 | 42 | export default { 43 | getAllCategories, 44 | getSubredditsFromRandomCategories, 45 | }; 46 | -------------------------------------------------------------------------------- /test/utils/createUser.test.js: -------------------------------------------------------------------------------- 1 | import { finalizeCreateUser } from "../../utils/createUser.js"; 2 | import User from "../../models/User.js"; 3 | import Token from "./../../models/VerifyToken.js"; 4 | import { connectDatabase, closeDatabaseConnection } from "./../database.js"; 5 | 6 | describe("Testing finializeCreateUser file", () => { 7 | let user = {}; 8 | beforeAll(async () => { 9 | await connectDatabase(); 10 | user = await new User({ 11 | username: "Beshoy", 12 | email: "beshoy@gmail.com", 13 | createdAt: Date.now(), 14 | }).save(); 15 | }); 16 | afterAll(async () => { 17 | await User.deleteMany({}); 18 | await Token.deleteMany({}); 19 | await closeDatabaseConnection(); 20 | }); 21 | 22 | it("should have finalizeCreateUser method", () => { 23 | expect(finalizeCreateUser).toBeDefined(); 24 | }); 25 | 26 | it("try to call finalize create user without sending an email", async () => { 27 | const user = await new User({ 28 | username: "Beshoy", 29 | email: "beshoy@gmail.com", 30 | createdAt: Date.now(), 31 | }).save(); 32 | const result = await finalizeCreateUser(user, false); 33 | expect(result.statusCode).toEqual(201); 34 | }); 35 | 36 | it("try to call finalize create user and send a verification email", async () => { 37 | const result = await finalizeCreateUser(user, true); 38 | expect(result.statusCode).toEqual(201); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /middleware/verifySignUp.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | 3 | /** 4 | * Middleware used to check the username and email used to sign up 5 | * if there was used before by another user. 6 | * It searches for the username and email and if one document was found 7 | * it will send status code 400 saying that the username or email is already in use 8 | * 9 | * @param {Object} req Request object 10 | * @param {Object} res Response object 11 | * @param {function} next Next function 12 | * @returns {void} 13 | */ 14 | export async function checkDuplicateUsernameOrEmail(req, res, next) { 15 | try { 16 | // Username 17 | const username = await User.findOne({ 18 | username: req.body.username.trim(), 19 | deletedAt: null, 20 | }); 21 | if (username) { 22 | return res.status(400).json({ error: "Username is already in use" }); 23 | } 24 | 25 | //Email 26 | const email = req.body.email.trim(); 27 | const emailUser = await User.findOne().or([ 28 | { email: email, deletedAt: null }, 29 | { googleEmail: email, deletedAt: null }, 30 | { facebookEmail: email, deletedAt: null }, 31 | ]); 32 | if (emailUser) { 33 | return res.status(400).json({ error: "Email is already in use" }); 34 | } 35 | 36 | // if everything is good then continue 37 | next(); 38 | } catch (error) { 39 | console.log(error); 40 | res.status(500).json("Internal server error"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /utils/generateTokens.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import Token from "../models/VerifyToken.js"; 3 | import crypto from "crypto"; 4 | 5 | /** 6 | * This function accepts user object and return the JWT created for that user 7 | * using jwt.sign function 8 | * 9 | * @param {Object} user User object that contains userId, and username 10 | * @returns {string} JWT created for that user 11 | */ 12 | export function generateJWT(user) { 13 | try { 14 | const token = jwt.sign( 15 | { userId: user.id, username: user.username }, 16 | process.env.TOKEN_SECRET 17 | ); 18 | 19 | return token; 20 | } catch (error) { 21 | throw new Error("Could not create a token"); 22 | } 23 | } 24 | 25 | /** 26 | * This function accepts user id and return a verification token 27 | * that will be used later to verify email or reset password 28 | * 29 | * @param {string} userId User Id 30 | * @param {string} type Type of the token (verfiy email, rest password, ...etc) 31 | * @returns {string} Token created for that user 32 | */ 33 | export async function generateVerifyToken(userId, type) { 34 | try { 35 | await Token.deleteOne({ userId: userId, type: type }); 36 | const token = await new Token({ 37 | userId: userId, 38 | token: crypto.randomBytes(32).toString("hex"), 39 | type: type, 40 | }).save(); 41 | 42 | return token.token; 43 | } catch (error) { 44 | throw new Error("Could not create a token"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /models/Message.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const messageSchema = mongoose.Schema({ 5 | subredditName: { 6 | type: String, 7 | }, 8 | text: { 9 | type: String, 10 | required: true, 11 | }, 12 | createdAt: { 13 | type: Date, 14 | required: true, 15 | }, 16 | editedAt: { 17 | type: Date, 18 | }, 19 | deletedAt: { 20 | type: Date, 21 | }, 22 | receiverId: { 23 | type: Schema.Types.ObjectId, 24 | ref: "User", 25 | }, 26 | senderUsername: { 27 | type: String, 28 | required: true, 29 | }, 30 | isSenderUser: { 31 | type: Boolean, 32 | required: true, 33 | }, 34 | receiverUsername: { 35 | type: String, 36 | required: true, 37 | }, 38 | isReceiverUser: { 39 | type: Boolean, 40 | required: true, 41 | }, 42 | subject: { 43 | type: String, 44 | }, 45 | isRead: { 46 | type: Boolean, 47 | required: true, 48 | default: false, 49 | }, 50 | isSpam: { 51 | type: Boolean, 52 | default: false, 53 | required: true, 54 | }, 55 | type: { 56 | type: String, 57 | required: true, 58 | default: "Message", 59 | }, 60 | isReply: { 61 | type: Boolean, 62 | required: true, 63 | default: false, 64 | }, 65 | repliedMsgId: { 66 | type: Schema.Types.ObjectId, 67 | ref: "Message", 68 | }, 69 | }); 70 | 71 | const Message = mongoose.model("Message", messageSchema); 72 | 73 | export default Message; 74 | -------------------------------------------------------------------------------- /utils/generateRandomUsername.js: -------------------------------------------------------------------------------- 1 | import User from "./../models/User.js"; 2 | import { generateUsername } from "unique-username-generator"; 3 | /** 4 | * This function generates a random username that wasn't taken before 5 | * using generateUsername 6 | * generateUsername takes three parameters(type of name,num of digits in the name,max num of characters in the name) 7 | * so it randomizes the type of username and num of digits to make the operation random as much as possible 8 | * *then if the generated username is not in the database so it return it 9 | * 10 | * @returns {string} random username of the user 11 | */ 12 | 13 | export async function generateRandomUsernameUtil() { 14 | try { 15 | while (true) { 16 | //SETTING TYPE OF USER NAME (IF IT HAS - OR NOT) 17 | const typeOfUsername = Math.floor(Math.random() * 2); 18 | //SETTING NUMBER OF DIGITS IN THE USERNAME WITH MAX VALUE OF 5 19 | const numOfDigits = Math.floor(Math.random() * 6); 20 | let RandomUsername; 21 | //DECIDING THE TYPE OF THE GENERATED USERNAME 22 | if (typeOfUsername) { 23 | RandomUsername = generateUsername("", numOfDigits, 20); 24 | } else { 25 | RandomUsername = generateUsername("-", numOfDigits, 20); 26 | } 27 | //CHECKING IF THERE IS NO USERNAME IN THE DATABASE AS THAT RANDOM ONE 28 | const user = await User.findOne({ 29 | username: RandomUsername, 30 | deletedAt: undefined, 31 | }); 32 | if (!user) { 33 | return RandomUsername; 34 | } 35 | } 36 | } catch (err) { 37 | return "Couldn't generate"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/utils/sendEmailsSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | sendResetPasswordEmail, 3 | sendVerifyEmail, 4 | sendUsernameEmail, 5 | } from "../../utils/sendEmails.js"; 6 | import User from "../../models/User.js"; 7 | import Token from "./../../models/VerifyToken.js"; 8 | import supertest from "supertest"; 9 | import app from "../../app.js"; 10 | supertest(app); 11 | 12 | describe("Testing send emails functions", () => { 13 | let user = {}, 14 | token = {}; 15 | beforeAll(async () => { 16 | user = new User({ 17 | username: "Beshoy", 18 | email: "beshoy@gmail.com", 19 | }); 20 | await user.save(); 21 | 22 | token = new Token({ 23 | userId: user._id, 24 | token: "token", 25 | type: "type", 26 | }); 27 | await token.save(); 28 | }); 29 | 30 | afterAll(async () => { 31 | await User.deleteMany({}); 32 | await Token.deleteMany({}); 33 | }); 34 | 35 | it("should have sendResetPasswordEmail method", () => { 36 | expect(sendResetPasswordEmail).toBeDefined(); 37 | }); 38 | 39 | it("try to send reset password email", async () => { 40 | expect(sendResetPasswordEmail(user, token.token)).toBeTruthy(); 41 | }); 42 | 43 | it("should have sendVerifyEmail method", () => { 44 | expect(sendVerifyEmail).toBeDefined(); 45 | }); 46 | 47 | it("try to send verify email", () => { 48 | expect(sendVerifyEmail(user, token.token)).toBeTruthy(); 49 | }); 50 | 51 | it("should have sendUsernameEmail method", () => { 52 | expect(sendUsernameEmail).toBeDefined(); 53 | }); 54 | 55 | it("try to send forget username email", () => { 56 | expect(sendUsernameEmail(user)).toBeTruthy(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | triggers { 4 | githubPush() 5 | } 6 | 7 | environment { 8 | DOCKERHUB_CREDENTIALS=credentials('Dockerhub') 9 | } 10 | 11 | stages{ 12 | 13 | stage("unit test"){ 14 | agent { 15 | docker { 16 | image 'node:lts' 17 | reuseNode true 18 | } 19 | } 20 | 21 | steps { 22 | sh "npm i " 23 | sh""" 24 | cp /Read-it/deployment/envfiles/backend_testing.env ./.env 25 | cp /Read-it/Backend/private/privateKey.json ./private/ 26 | """ 27 | sh "npm run test" 28 | } 29 | } 30 | 31 | stage('Build') { 32 | 33 | steps { 34 | sh 'docker build -t waer/backend:latest .' 35 | } 36 | } 37 | 38 | stage("intgration testing"){ 39 | steps { 40 | sh """ 41 | cd /Read-it-testing/deployment-testing 42 | docker-compose up --exit-code-from cypress 43 | docker wait cypress 44 | """ 45 | } 46 | } 47 | 48 | stage('Login') { 49 | 50 | steps { 51 | sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin' 52 | } 53 | } 54 | 55 | 56 | stage('Push') { 57 | steps { 58 | sh 'docker push waer/backend:latest' 59 | } 60 | } 61 | 62 | } 63 | 64 | post { 65 | always { 66 | sh 'docker logout' 67 | echo "------------------------------ down the Testing enviroment ---------------------------------------------" 68 | sh""" 69 | cd /Read-it-testing/deployment-testing 70 | docker-compose down 71 | """ 72 | } 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /tests/utils/passwordUtilsSpec.js: -------------------------------------------------------------------------------- 1 | import User from "../../models/User.js"; 2 | import supertest from "supertest"; 3 | import { hashPassword, comparePasswords } from "../../utils/passwordUtils.js"; 4 | import bcrypt from "bcryptjs"; 5 | import app from "../../app.js"; 6 | supertest(app); 7 | 8 | describe("Testing Password utilities", () => { 9 | afterAll(async () => { 10 | await User.deleteMany({}); 11 | }); 12 | 13 | it("should have hashPassword method", () => { 14 | expect(hashPassword).toBeDefined(); 15 | }); 16 | 17 | it("check if hashPassword returns a valid hashed password", () => { 18 | const hashedPassword = hashPassword("12345678"); 19 | const result = bcrypt.compareSync( 20 | "12345678" + process.env.BCRYPT_PASSWORD, 21 | hashedPassword 22 | ); 23 | expect(result).toBeTruthy(); 24 | }); 25 | 26 | it("should have comparePasswords method", () => { 27 | expect(comparePasswords).toBeDefined(); 28 | }); 29 | 30 | // eslint-disable-next-line max-len 31 | it("check if passwords are compared correctly", async () => { 32 | const hashedPass = bcrypt.hashSync( 33 | "987654321" + process.env.BCRYPT_PASSWORD, 34 | parseInt(process.env.SALT_ROUNDS) 35 | ); 36 | const result = comparePasswords("987654321", hashedPass); 37 | expect(result).toBeTruthy(); 38 | }); 39 | 40 | it("check if function returns false if passwords don't match", async () => { 41 | const hashedPass = bcrypt.hashSync( 42 | "987654321" + process.env.BCRYPT_PASSWORD, 43 | parseInt(process.env.SALT_ROUNDS) 44 | ); 45 | const result = comparePasswords("123456789", hashedPass); 46 | expect(result).toBeFalsy(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /middleware/subredditRules.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | /** 4 | * A middleware used to make sure that the provided ruleId name exists 5 | * If that rule exists it adds it to the request object to make the next middleware access it 6 | * It it doesn't exist then it returns a response with status code 404 and error message 7 | * 8 | * @param {Object} req Request object 9 | * @param {Object} res Response object 10 | * @param {function} next Next function 11 | * @returns {void} 12 | */ 13 | 14 | const checkRule = (req, res, next) => { 15 | const ruleId = req.params.ruleId; 16 | const neededRule = req.subreddit.rules.find( 17 | (el) => el._id.toString() === ruleId 18 | ); 19 | 20 | if (!neededRule || neededRule.deletedAt) { 21 | res.status(404).json({ 22 | error: "Rule not found!", 23 | }); 24 | } else { 25 | req.neededRule = neededRule; 26 | next(); 27 | } 28 | }; 29 | 30 | /** 31 | * A middleware used to make sure that the provided ruleId is a valid mongo ObjectId 32 | * If that RuleId is valid it make the next middleware access it 33 | * It it is not valid then it returns a response with status code 400 and error message 34 | * 35 | * @param {Object} req Request object 36 | * @param {Object} res Response object 37 | * @param {function} next Next function 38 | * @returns {void} 39 | */ 40 | 41 | const validateRuleId = (req, res, next) => { 42 | const ruleId = req.params.ruleId; 43 | if (!mongoose.Types.ObjectId.isValid(ruleId)) { 44 | return res.status(400).json({ 45 | error: "Invalid id", 46 | }); 47 | } else { 48 | next(); 49 | } 50 | }; 51 | 52 | export default { 53 | checkRule, 54 | validateRuleId, 55 | }; 56 | -------------------------------------------------------------------------------- /test/utils/passwordUtils.test.js: -------------------------------------------------------------------------------- 1 | import { hashPassword, comparePasswords } from "../../utils/passwordUtils.js"; 2 | import bcrypt from "bcryptjs"; 3 | import { connectDatabase, closeDatabaseConnection } from "./../database.js"; 4 | 5 | describe("Testing Password utilities", () => { 6 | beforeAll(async () => { 7 | await connectDatabase(); 8 | }); 9 | afterAll(async () => { 10 | await closeDatabaseConnection(); 11 | }); 12 | 13 | it("should have hashPassword method", () => { 14 | expect(hashPassword).toBeDefined(); 15 | }); 16 | 17 | it("check if hashPassword returns a valid hashed password", () => { 18 | const hashedPassword = hashPassword("12345678"); 19 | const result = bcrypt.compareSync( 20 | "12345678" + process.env.BCRYPT_PASSWORD, 21 | hashedPassword 22 | ); 23 | expect(result).toBeTruthy(); 24 | }); 25 | 26 | it("should have comparePasswords method", () => { 27 | expect(comparePasswords).toBeDefined(); 28 | }); 29 | 30 | // eslint-disable-next-line max-len 31 | it("check if passwords are compared correctly", async () => { 32 | const hashedPass = bcrypt.hashSync( 33 | "987654321" + process.env.BCRYPT_PASSWORD, 34 | parseInt(process.env.SALT_ROUNDS) 35 | ); 36 | const result = comparePasswords("987654321", hashedPass); 37 | expect(result).toBeTruthy(); 38 | }); 39 | 40 | it("check if function returns false if passwords don't match", async () => { 41 | const hashedPass = bcrypt.hashSync( 42 | "987654321" + process.env.BCRYPT_PASSWORD, 43 | parseInt(process.env.SALT_ROUNDS) 44 | ); 45 | const result = comparePasswords("123456789", hashedPass); 46 | expect(result).toBeFalsy(); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /utils/subredditRules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function used to validate the request body to create a rule to a subreddit 3 | * If the request body is valid it adds to the request the ruleObject 4 | * @param {Object} req Request object 5 | * @returns {boolean} boolean indicates if the request body is valid 6 | */ 7 | export function validateCreatingRuleBody(req) { 8 | if (!req.body.ruleName || !req.body.appliesTo) { 9 | return false; 10 | } else if ( 11 | req.body.appliesTo !== "posts and comments" && 12 | req.body.appliesTo !== "posts only" && 13 | req.body.appliesTo !== "comments only" 14 | ) { 15 | return false; 16 | } else { 17 | const ruleObject = { 18 | ruleTitle: req.body.ruleName, 19 | appliesTo: req.body.appliesTo, 20 | }; 21 | if (req.body.description) { 22 | ruleObject.ruleDescription = req.body.description; 23 | } 24 | if (req.body.reportReason) { 25 | ruleObject.reportReason = req.body.reportReason; 26 | } 27 | 28 | req.ruleObject = ruleObject; 29 | return true; 30 | } 31 | } 32 | 33 | /** 34 | * A function used to validate the request body to edit a rule to a subreddit 35 | * If the request body is valid it adds to the request the ruleObject 36 | * @param {Object} req Request object 37 | * @returns {boolean} boolean indicates if the request body is valid 38 | */ 39 | export function validateEditingRuleBody(req) { 40 | const firstValidate = validateCreatingRuleBody(req); 41 | if (!firstValidate) { 42 | } 43 | if ( 44 | !firstValidate || 45 | (!req.body.ruleOrder && req.body.ruleOrder?.toString() !== "0") 46 | ) { 47 | return false; 48 | } 49 | req.ruleObject.ruleOrder = req.body.ruleOrder; 50 | return true; 51 | } 52 | -------------------------------------------------------------------------------- /routes/categories.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import categoryController from "../controllers/categoryController.js"; 3 | 4 | // eslint-disable-next-line new-cap 5 | const categoryRouter = express.Router(); 6 | 7 | /** 8 | * @swagger 9 | * /saved-categories: 10 | * get: 11 | * summary: Get a list of all categories 12 | * tags: [Categories] 13 | * responses: 14 | * 200: 15 | * description: Categories returned successfully 16 | * content: 17 | * application/json: 18 | * schema: 19 | * type: object 20 | * properties: 21 | * categories: 22 | * type: array 23 | * items: 24 | * $ref: '#/components/schemas/Category' 25 | * 404: 26 | * description: Page not found 27 | * 400: 28 | * description: The request was invalid. You may refer to response for details around why this happened. 29 | * content: 30 | * application/json: 31 | * schema: 32 | * properties: 33 | * error: 34 | * type: string 35 | * description: Type of error 36 | * 401: 37 | * description: Unauthorized to view saved categories 38 | * 500: 39 | * description: Server Error 40 | * security: 41 | * - bearerAuth: [] 42 | */ 43 | categoryRouter.get("/saved-categories", categoryController.getAllCategories); 44 | 45 | export default categoryRouter; 46 | -------------------------------------------------------------------------------- /middleware/NverifyModerator.js: -------------------------------------------------------------------------------- 1 | import { searchForSubreddit } from "./../services/communityServices.js"; 2 | 3 | /** 4 | * Middleware used to check if the user is a moderator in the desired subreddit or not 5 | * It gets the moderators of that desired one 6 | * and searches for the id of that user 7 | * if it's found then this user is a moderator and has the rights to do that action 8 | * if it's not found then he can't access the action 9 | * it will send status code 400 saying that the user doesn't have the right to do this action 10 | * it may send status code 400 saying Token may be invalid or not found 11 | * 12 | * @param {Object} req Request object 13 | * @param {Object} res Response object 14 | * @param {function} next Next function 15 | * @returns {void} 16 | */ 17 | // eslint-disable-next-line max-statements 18 | export async function checkModerator(req, res, next) { 19 | const authPayload = req.payload; 20 | try { 21 | const subreddit = await searchForSubreddit(req.params.subreddit); 22 | // eslint-disable-next-line max-len 23 | const { moderators } = subreddit; 24 | let isThere = false; 25 | const userId = authPayload.userId; 26 | for (const moderator of moderators) { 27 | if (moderator.userID.toString() === userId) { 28 | isThere = true; 29 | } 30 | } 31 | if (isThere) { 32 | next(); 33 | } else { 34 | let error = new Error("you don't have the right to do this action"); 35 | error.statusCode = 400; 36 | throw error; 37 | } 38 | } catch (err) { 39 | if (err.statusCode) { 40 | return res.status(err.statusCode).json({ 41 | error: err.message, 42 | }); 43 | } else { 44 | return res.status(500).json("Internal Server Error"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/middlewares/checkId.test.js: -------------------------------------------------------------------------------- 1 | import { checkId } from "../../middleware/checkId"; 2 | import { jest } from "@jest/globals"; 3 | 4 | describe("Testing checkId middleware", () => { 5 | let mockRequest; 6 | let mockResponse; 7 | let nextFunction = jest.fn(); 8 | 9 | beforeEach(() => { 10 | mockRequest = {}; 11 | mockResponse = { 12 | status: () => { 13 | jest.fn(); 14 | return mockResponse; 15 | }, 16 | json: () => { 17 | jest.fn(); 18 | return mockResponse; 19 | }, 20 | }; 21 | }); 22 | 23 | it("should have checkId function", () => { 24 | expect(checkId).toBeDefined(); 25 | }); 26 | 27 | it("try to send an invalid id in the params", () => { 28 | mockRequest = { 29 | params: { 30 | id: "lol", 31 | }, 32 | body: {}, 33 | }; 34 | checkId(mockRequest, mockResponse, nextFunction); 35 | expect(nextFunction).not.toHaveBeenCalled(); 36 | }); 37 | it("try to send an invalid id in the body", () => { 38 | mockRequest = { 39 | params: {}, 40 | body: { id: "lol" }, 41 | }; 42 | checkId(mockRequest, mockResponse, nextFunction); 43 | expect(nextFunction).not.toHaveBeenCalled(); 44 | }); 45 | 46 | it("try to send a valid id in the params", () => { 47 | mockRequest = { 48 | params: { 49 | id: "6398e23a159be70e26e54dd5", 50 | }, 51 | body: {}, 52 | }; 53 | checkId(mockRequest, mockResponse, nextFunction); 54 | expect(nextFunction).toHaveBeenCalled(); 55 | }); 56 | it("try to send a valid id in the body", () => { 57 | mockRequest = { 58 | params: {}, 59 | body: { 60 | id: "6398e23a159be70e26e54dd5", 61 | }, 62 | }; 63 | checkId(mockRequest, mockResponse, nextFunction); 64 | expect(nextFunction).toHaveBeenCalled(); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /tests/utils/generateTokensSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | generateJWT, 3 | generateVerifyToken, 4 | } from "../../utils/generateTokens.js"; 5 | import User from "../../models/User.js"; 6 | import Token from "./../../models/VerifyToken.js"; 7 | import supertest from "supertest"; 8 | import app from "../../app.js"; 9 | supertest(app); 10 | 11 | describe("Testing generate tokens", () => { 12 | afterAll(async () => { 13 | await User.deleteMany({}); 14 | await Token.deleteMany({}); 15 | }); 16 | 17 | it("should have generateJWT method", () => { 18 | expect(generateJWT).toBeDefined(); 19 | }); 20 | 21 | it("check if generateJWT returns a valid jwt", () => { 22 | const user = { 23 | userId: "mongodbId", 24 | username: "Beshoy", 25 | }; 26 | const token = generateJWT(user); 27 | try { 28 | const decodedPayload = jwt.verify(token, process.env.TOKEN_SECRET); 29 | expect(decodedPayload).toEqual(user); 30 | } catch (err) { 31 | return false; 32 | } 33 | }); 34 | 35 | it("try to send empty user object to generateJWT", () => { 36 | const user = {}; 37 | const token = generateJWT(user); 38 | try { 39 | const decodedPayload = jwt.verify(token, process.env.TOKEN_SECRET); 40 | expect(decodedPayload).toEqual(user); 41 | } catch (err) { 42 | return false; 43 | } 44 | }); 45 | 46 | it("should have generateVerifyToken method", () => { 47 | expect(generateVerifyToken).toBeDefined(); 48 | }); 49 | 50 | // eslint-disable-next-line max-len 51 | it("check if generateVerifyToken returns a valid token with length 64", async () => { 52 | const user = new User({ 53 | username: "Beshoy", 54 | email: "beshoy@gmail.com", 55 | }); 56 | await user.save(); 57 | const token = await generateVerifyToken(user._id, "random"); 58 | expect(token.length).toEqual(64); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /controllers/commentActionsController.js: -------------------------------------------------------------------------------- 1 | import { body } from "express-validator"; 2 | import { validateId } from "../services/subredditFlairs.js"; 3 | // eslint-disable-next-line max-len 4 | import { 5 | addToUserFollowedComments, 6 | addToCommentFollowedUsers, 7 | removeFromUserFollowedComments, 8 | removeFromCommentFollowedUsers, 9 | } from "../services/commentActionsServices.js"; 10 | const followUnfollowValidator = [ 11 | body("commentId").trim().not().isEmpty().withMessage("commentId is required"), 12 | ]; 13 | 14 | const followComment = async (req, res) => { 15 | try { 16 | validateId(req.body.commentId); 17 | 18 | const userAndComment = await addToUserFollowedComments( 19 | req.payload.userId, 20 | req.body.commentId 21 | ); 22 | await addToCommentFollowedUsers( 23 | userAndComment.user, 24 | userAndComment.comment 25 | ); 26 | res.status(200).json("Followed comment successfully"); 27 | } catch (err) { 28 | console.log(err.message); 29 | if (err.statusCode) { 30 | res.status(err.statusCode).json({ error: err.message }); 31 | } else { 32 | res.status(500).json("Internal Server Error"); 33 | } 34 | } 35 | }; 36 | 37 | const unfollowComment = async (req, res) => { 38 | try { 39 | validateId(req.body.commentId); 40 | 41 | const userAndComment = await removeFromUserFollowedComments( 42 | req.payload.userId, 43 | req.body.commentId 44 | ); 45 | await removeFromCommentFollowedUsers( 46 | userAndComment.user, 47 | userAndComment.comment 48 | ); 49 | res.status(200).json("Unfollowed comment successfully"); 50 | } catch (err) { 51 | console.log(err.message); 52 | if (err.statusCode) { 53 | res.status(err.statusCode).json({ error: err.message }); 54 | } else { 55 | res.status(500).json("Internal Server Error"); 56 | } 57 | } 58 | }; 59 | 60 | export default { 61 | followUnfollowValidator, 62 | followComment, 63 | unfollowComment, 64 | }; 65 | -------------------------------------------------------------------------------- /middleware/pinnedPosts.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | 3 | /** 4 | * Middleware used to check if the post is already pinned in the 5 | * user's collection of pinned posts and return an appropriate response. 6 | * The postId and user object are passed with the request as well. 7 | * 8 | * @param {Object} req Request object 9 | * @param {Object} res Response object 10 | * @param {function} next Next function 11 | * @returns {void} 12 | */ 13 | export async function checkPinnedPosts(req, res, next) { 14 | const userId = req.payload.userId; 15 | const postId = req.body.id; 16 | try { 17 | const user = await User.findById(userId); 18 | if (!user || user.deletedAt) { 19 | return res.status(404).json("User not found or deleted"); 20 | } 21 | if ( 22 | req.body.pin && 23 | user.pinnedPosts.find((id) => id.toString() === postId) 24 | ) { 25 | return res.status(409).json("Post is already pinned"); 26 | } 27 | if (req.body.pin && user.pinnedPosts.length === 4) { 28 | return res.status(409).json("Can only pin up to 4 posts"); 29 | } 30 | req.postId = postId; 31 | req.user = user; 32 | next(); 33 | } catch (err) { 34 | return res.status(500).json("Internal server error"); 35 | } 36 | } 37 | 38 | /** 39 | * Middleware used to check if the post is already unpinned and not found in the 40 | * user's collection of pinned posts then return an appropriate response. 41 | * 42 | * @param {Object} req Request object 43 | * @param {Object} res Response object 44 | * @param {function} next Next function 45 | * @returns {void} 46 | */ 47 | export async function checkUnpinnedPosts(req, res, next) { 48 | try { 49 | if ( 50 | !req.body.pin && 51 | !req.user.pinnedPosts.find((id) => id.toString() === req.postId) 52 | ) { 53 | return res.status(409).json("Post is already unpinned"); 54 | } 55 | next(); 56 | } catch (err) { 57 | return res.status(500).json("Internal server error"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /services/readMessages.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | import Message from "../models/Message.js"; 3 | import Mention from "../models/Mention.js"; 4 | import PostReplies from "../models/PostReplies.js"; 5 | 6 | /** 7 | * A function used to mark all username mentions of the user's collection as read but 8 | * first checks if the userId is found in the database. 9 | * @param {string} userId User ID 10 | * @returns {void} 11 | */ 12 | 13 | export async function readUsernameMentions(userId) { 14 | const user = await User.findById(userId); 15 | if (!user || user.deletedAt) { 16 | const error = new Error("User not found"); 17 | error.statusCode = 401; 18 | throw error; 19 | } 20 | await Mention.updateMany( 21 | { _id: { $in: user.usernameMentions } }, 22 | { $set: { isRead: true } } 23 | ); 24 | } 25 | 26 | /** 27 | * A function used to mark all post replies of the user's collection as read but 28 | * first checks if the userId is found in the database. 29 | * @param {string} userId User ID 30 | * @returns {void} 31 | */ 32 | 33 | export async function readPostReplies(userId) { 34 | const user = await User.findById(userId); 35 | if (!user || user.deletedAt) { 36 | const error = new Error("User not found"); 37 | error.statusCode = 401; 38 | throw error; 39 | } 40 | await PostReplies.updateMany( 41 | { _id: { $in: user.postReplies } }, 42 | { $set: { isRead: true } } 43 | ); 44 | } 45 | 46 | /** 47 | * A function used to mark all received messages of the user's collection as read but 48 | * first checks if the userId is found in the database. 49 | * @param {string} userId User ID 50 | * @returns {void} 51 | */ 52 | 53 | export async function readReceivedMessages(userId) { 54 | const user = await User.findById(userId); 55 | if (!user || user.deletedAt) { 56 | const error = new Error("User not found"); 57 | error.statusCode = 401; 58 | throw error; 59 | } 60 | await Message.updateMany( 61 | { _id: { $in: user.receivedMessages } }, 62 | { $set: { isRead: true } } 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /controllers/BitemsActionsController.js: -------------------------------------------------------------------------------- 1 | import { body } from "express-validator"; 2 | import Comment from "../models/Comment.js"; 3 | import { deleteItems } from "../services/itemsActionsServices.js"; 4 | 5 | const deleteValidator = [ 6 | body("id").not().isEmpty().withMessage("Id can not be empty"), 7 | body("type").not().isEmpty().withMessage("Type can not be empty"), 8 | ]; 9 | 10 | const editComValidator = [ 11 | body("id").not().isEmpty().withMessage("Id can not be empty"), 12 | body("content").not().isEmpty().withMessage("New content can not be empty"), 13 | ]; 14 | 15 | // eslint-disable-next-line max-statements 16 | const deletePoComMes = async (req, res) => { 17 | try { 18 | const { id, type } = req.body; 19 | 20 | const result = await deleteItems(req.payload.userId, id, type); 21 | res.status(result.statusCode).json(result.message); 22 | } catch (error) { 23 | console.log(error.message); 24 | if (error.statusCode) { 25 | res.status(error.statusCode).json({ error: error.message }); 26 | } else { 27 | res.status(500).json("Internal server error"); 28 | } 29 | } 30 | }; 31 | 32 | // eslint-disable-next-line max-statements 33 | const editComment = async (req, res) => { 34 | try { 35 | const { id, content } = req.body; 36 | const comment = await Comment.findById(id); 37 | 38 | // check if the comment was deleted before or does not exist 39 | if (!comment || comment.deletedAt) { 40 | return res.status(404).json("Comment was not found"); 41 | } 42 | 43 | // check if the comment was created by the same user making the request 44 | const { userId } = req.payload; 45 | if (comment.ownerId.toString() !== userId) { 46 | return res.status(401).json("Access Denied"); 47 | } 48 | 49 | comment.content = content; 50 | await comment.save(); 51 | res.status(200).json("Item edited successfully"); 52 | } catch (error) { 53 | console.log(error); 54 | res.status(500).json("Internal Server Error"); 55 | } 56 | }; 57 | 58 | export default { 59 | deleteValidator, 60 | deletePoComMes, 61 | editComValidator, 62 | editComment, 63 | }; 64 | -------------------------------------------------------------------------------- /test/middlewares/validationResult.test.js: -------------------------------------------------------------------------------- 1 | import { check } from "express-validator"; 2 | import { validateRequestSchema } from "./../../middleware/validationResult.js"; 3 | import { jest } from "@jest/globals"; 4 | 5 | const testExpressValidatorMiddleware = async (req, res, middlewares) => { 6 | await Promise.all( 7 | middlewares.map(async (middleware) => { 8 | await middleware(req, res, () => undefined); 9 | }) 10 | ); 11 | }; 12 | 13 | describe("Testing validationResult middleware", () => { 14 | let mockResponse; 15 | let nextFunction = jest.fn(); 16 | beforeEach(() => { 17 | mockResponse = { 18 | status: () => { 19 | jest.fn(); 20 | return mockResponse; 21 | }, 22 | json: () => { 23 | jest.fn(); 24 | return mockResponse; 25 | }, 26 | }; 27 | }); 28 | 29 | it("try to send an empty paramerters to validateResult middleware", async () => { 30 | const req = { 31 | body: { 32 | postId: "", 33 | commentId: "", 34 | }, 35 | }; 36 | const postMsg = "postId can not be empty"; 37 | const commentMsg = "commentId can not be empty"; 38 | await testExpressValidatorMiddleware(req, mockResponse, [ 39 | check("postId").not().isEmpty().withMessage(postMsg), 40 | check("commentId").not().isEmpty().withMessage(commentMsg), 41 | ]); 42 | 43 | validateRequestSchema(req, mockResponse, nextFunction); 44 | expect(nextFunction).not.toHaveBeenCalled(); 45 | }); 46 | 47 | it("try to send correct parameters to validateResult middleware", async () => { 48 | const req = { 49 | body: { 50 | postId: "id", 51 | commentId: "id", 52 | }, 53 | }; 54 | const postMsg = "postId can not be empty"; 55 | const commentMsg = "commentId can not be empty"; 56 | await testExpressValidatorMiddleware(req, mockResponse, [ 57 | check("postId").not().isEmpty().withMessage(postMsg), 58 | check("commentId").not().isEmpty().withMessage(commentMsg), 59 | ]); 60 | 61 | validateRequestSchema(req, mockResponse, nextFunction); 62 | expect(nextFunction).toHaveBeenCalled(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/middlewares/getSubredditMiddleware.test.js: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 3 | import { getBodySubreddit } from "../../middleware/getSubredditMiddleware"; 4 | import Subreddit from "../../models/Community"; 5 | 6 | // eslint-disable-next-line max-statements 7 | describe("Testing getBodySubreddit middleware", () => { 8 | let mockRequest; 9 | let mockResponse; 10 | let nextFunction = jest.fn(); 11 | 12 | let subreddit = {}; 13 | beforeAll(async () => { 14 | await connectDatabase(); 15 | 16 | subreddit = await new Subreddit({ 17 | type: "public", 18 | title: "subreddit", 19 | category: "fun", 20 | viewName: "LOL", 21 | owner: { 22 | username: "Beshoy", 23 | }, 24 | dateOfCreation: Date.now(), 25 | }).save(); 26 | }); 27 | 28 | afterAll(async () => { 29 | await Subreddit.deleteMany({}); 30 | await closeDatabaseConnection(); 31 | }); 32 | 33 | beforeEach(() => { 34 | mockRequest = {}; 35 | mockResponse = { 36 | status: () => { 37 | jest.fn(); 38 | return mockResponse; 39 | }, 40 | json: () => { 41 | jest.fn(); 42 | return mockResponse; 43 | }, 44 | }; 45 | }); 46 | 47 | it("should have getBodySubreddit function", () => { 48 | expect(getBodySubreddit).toBeDefined(); 49 | }); 50 | 51 | // eslint-disable-next-line max-len 52 | it("try to send an invalid subreddit name to getBodySubreddit function", async () => { 53 | mockRequest = { 54 | body: { 55 | subreddit: "invalid", 56 | }, 57 | }; 58 | await getBodySubreddit(mockRequest, mockResponse, nextFunction); 59 | expect(nextFunction).not.toHaveBeenCalled(); 60 | }); 61 | 62 | // eslint-disable-next-line max-len 63 | it("try to send a valid subreddit name to getBodySubreddit function", async () => { 64 | mockRequest = { 65 | body: { 66 | subreddit: subreddit.title, 67 | }, 68 | }; 69 | await getBodySubreddit(mockRequest, mockResponse, nextFunction); 70 | expect(nextFunction).toHaveBeenCalled(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /routes/routes.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import moderationRouter from "./moderation.js"; 3 | import communitiesRouter from "./communities.js"; 4 | import signupRouter from "./signup.js"; 5 | import loginRouter from "./login.js"; 6 | import commentsRouter from "./comments.js"; 7 | import itemsActionsRouter from "./itemsActions.js"; 8 | import postActionsRouter from "./postActions.js"; 9 | import postRouter from "./posts.js"; 10 | import subredditRouter from "./subreddit.js"; 11 | import subredditRulesRouter from "./subredditRules.js"; 12 | import userRouter from "./user.js"; 13 | import categoryRouter from "./categories.js"; 14 | import subredditFlairsRouter from "./subredditFlairs.js"; 15 | import messageRouter from "./message.js"; 16 | import subredditModerationsRouter from "./subredditModeration.js"; 17 | import userSettingsRouter from "./userSettings.js"; 18 | import postAndCommentActionsRouter from "./post-and-comment-actions.js"; 19 | import commentActionsRouter from "./comment-action.js"; 20 | import searchRouter from "./search.js"; 21 | import listingRouter from "./listing.js"; 22 | import notificationRouter from "./notification.js"; 23 | // eslint-disable-next-line new-cap 24 | const mainRouter = express.Router(); 25 | 26 | mainRouter.use(communitiesRouter); 27 | mainRouter.use(signupRouter); 28 | mainRouter.use(loginRouter); 29 | mainRouter.use(commentsRouter); 30 | mainRouter.use(itemsActionsRouter); 31 | mainRouter.use(postActionsRouter); 32 | mainRouter.use(moderationRouter); 33 | mainRouter.use(postRouter); 34 | mainRouter.use(userRouter); 35 | mainRouter.use(subredditRouter); 36 | mainRouter.use(subredditRulesRouter); 37 | mainRouter.use(categoryRouter); 38 | mainRouter.use(subredditFlairsRouter); 39 | mainRouter.use(userSettingsRouter); 40 | mainRouter.use(messageRouter); 41 | mainRouter.use(subredditModerationsRouter); 42 | mainRouter.use(postAndCommentActionsRouter); 43 | mainRouter.use(commentActionsRouter); 44 | mainRouter.use(searchRouter); 45 | mainRouter.use(listingRouter); 46 | mainRouter.use(notificationRouter); 47 | 48 | // ! should add your router before this middleware 49 | mainRouter.use((req, res) => { 50 | res.status(404).json(`Can't ${req.method} ${req.url}`); 51 | }); 52 | 53 | export default mainRouter; 54 | -------------------------------------------------------------------------------- /services/postServices.js: -------------------------------------------------------------------------------- 1 | import Post from "../models/Post.js"; 2 | import Subreddit from "../models/Community.js"; 3 | 4 | /** 5 | * A function used to check if the owner user is trying to edit the post and throws an error if another user is trying to edit that post 6 | * @param {ObjectID} postId Request object 7 | * @param {ObjectID} userId Request object 8 | * @returns {neededPost} the needed post to edit is all checks succeeded to help the next service 9 | */ 10 | export async function checkSameUserEditing(postId, userId) { 11 | const neededPost = await Post.findById(postId); 12 | if (!neededPost || neededPost.deletedAt) { 13 | const error = new Error("Post is not found"); 14 | error.statusCode = 404; 15 | throw error; 16 | } 17 | if (neededPost.ownerId.toString() !== userId) { 18 | const error = new Error("User not allowed to edit this post"); 19 | error.statusCode = 401; 20 | throw error; 21 | } 22 | if (neededPost.kind !== "hybrid") { 23 | const error = new Error("Not allowed to edit this post"); 24 | error.statusCode = 400; 25 | throw error; 26 | } 27 | return neededPost; 28 | } 29 | 30 | /** 31 | * A function used to edit the post and add the post tothe subreddit edited posts if the post belongs to a subreddit 32 | * @param {post} post Request object 33 | * @param {string} postContent Request object 34 | * @returns {void} the needed post to edit is all checks succeeded to help the next service 35 | */ 36 | export async function editPostService(post, postContent) { 37 | const subreddit = post.subredditName; 38 | const subbredditObject = await Subreddit.findOne({ 39 | title: subreddit, 40 | deletedAt: null, 41 | }); 42 | if (!subbredditObject) { 43 | const error = new Error("Subreddit not found!"); 44 | error.statusCode = 404; 45 | throw error; 46 | } 47 | post.content = postContent; 48 | post.editedAt = Date.now(); 49 | if (subreddit) { 50 | const postIndex = subbredditObject.editedPosts.findIndex( 51 | (postId) => postId.toString() === post._id.toString() 52 | ); 53 | if (postIndex === -1) { 54 | subbredditObject.editedPosts.push(post._id); 55 | await subbredditObject.save(); 56 | } 57 | } 58 | await post.save(); 59 | } 60 | -------------------------------------------------------------------------------- /services/subredditRules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A function used to validate the request body, if the number of rules doesn't match it throws an error 3 | * @param {Object} req Request object 4 | * @returns {void} 5 | */ 6 | 7 | export function checkEditRulesOrderService(req) { 8 | if (req.body.rulesOrder.length !== req.subreddit.numberOfRules) { 9 | const error = new Error("Number of rules doesn't match"); 10 | error.statusCode = 400; 11 | throw error; 12 | } 13 | } 14 | 15 | /** 16 | * A function used to validate the request body, if the rule order or the rule id is dublicated it throws an error 17 | * @param {Object} req Request object 18 | * @returns {void} 19 | */ 20 | 21 | export function checkDublicateRuleOrderService(req) { 22 | const ruleOrders = new Map(); 23 | const ruleIds = new Map(); 24 | req.body.rulesOrder.forEach((element) => { 25 | if (ruleOrders.has(element.ruleOrder)) { 26 | const error = new Error("dublicate rule order"); 27 | error.statusCode = 400; 28 | throw error; 29 | } else if (ruleIds.has(element.ruleId)) { 30 | const error = new Error("dublicate rule id"); 31 | error.statusCode = 400; 32 | throw error; 33 | } else { 34 | ruleOrders.set(element.ruleOrder, 1); 35 | ruleIds.set(element.ruleId, 1); 36 | } 37 | }); 38 | } 39 | 40 | /** 41 | * A function used to update the rules orders of the subreddit 42 | * @param {Object} req Request object 43 | * @returns {void} 44 | */ 45 | 46 | export async function editRulesOrderService(req) { 47 | // loop through the subreddit rules and the request body rules 48 | for (let i = 0; i < req.subreddit.rules.length; i++) { 49 | for (let j = 0; j < req.body.rulesOrder.length; j++) { 50 | if ( 51 | req.subreddit.rules[i]._id.toString() === 52 | req.body.rulesOrder[j].ruleId && 53 | req.subreddit.rules[i].deletedAt 54 | ) { 55 | const error = new Error("Rule not found"); 56 | error.statusCode = 400; 57 | throw error; 58 | } else if ( 59 | req.subreddit.rules[i]._id.toString() === req.body.rulesOrder[j].ruleId 60 | ) { 61 | req.subreddit.rules[i].ruleOrder = req.body.rulesOrder[j].ruleOrder; 62 | } 63 | } 64 | } 65 | await req.subreddit.save(); 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "set NODE_ENV=development && nodemon app.js", 9 | "start": "node app.js", 10 | "test": "set NODE_ENV=testing && node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand", 11 | "doc": "swagger-jsdoc-generator config.json > documentation.json", 12 | "doc2": "redoc-cli bundle -o apidoc/index.html documentation.json", 13 | "lint": "eslint ./**/*.js", 14 | "prettier": "prettier ./**/*.js --write", 15 | "jsdoc": "jsdoc -c jsdoc.json", 16 | "coverage": "npm run test -- --coverage", 17 | "seed": "node ./seeds/seeder.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/ZeyadTarekk/Test-Deploy.git" 22 | }, 23 | "author": "Zeyad", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/ZeyadTarekk/Test-Deploy/issues" 27 | }, 28 | "homepage": "https://github.com/ZeyadTarekk/Test-Deploy#readme", 29 | "dependencies": { 30 | "bcryptjs": "^2.4.3", 31 | "body-parser": "^1.20.1", 32 | "cors": "^2.8.5", 33 | "crypto": "^1.0.1", 34 | "dotenv": "^16.0.3", 35 | "express": "^4.18.1", 36 | "express-rate-limit": "^6.7.0", 37 | "express-validator": "^6.14.2", 38 | "fcm-node": "^1.6.1", 39 | "jest-html-reporter": "^3.7.0", 40 | "jsonwebtoken": "^8.5.1", 41 | "jwt-decode": "^3.1.2", 42 | "mailgun-js": "^0.22.0", 43 | "mongodb": "^4.11.0", 44 | "mongoose": "^6.6.4", 45 | "morgan": "^1.10.0", 46 | "multer": "^1.4.5-lts.1", 47 | "nodemailer": "^6.8.0", 48 | "nodemailer-sendgrid-transport": "^0.2.0", 49 | "unique-username-generator": "^1.1.3" 50 | }, 51 | "devDependencies": { 52 | "@faker-js/faker": "^7.6.0", 53 | "eslint": "^8.25.0", 54 | "eslint-plugin-prettier": "^4.2.1", 55 | "istanbul": "^0.4.5", 56 | "jest": "^29.3.1", 57 | "jsdoc": "^3.6.11", 58 | "nodemon": "^2.0.20", 59 | "nyc": "^15.1.0", 60 | "prettier": "2.7.1", 61 | "redoc-cli": "^0.13.20", 62 | "supertest": "^6.3.1", 63 | "swagger-jsdoc": "^6.2.5", 64 | "swagger-jsdoc-generator": "^1.0.3", 65 | "swagger-ui-express": "^4.5.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /controllers/BpostActionsController.js: -------------------------------------------------------------------------------- 1 | import { body } from "express-validator"; 2 | 3 | const postActionsValidator = [ 4 | body("id").not().isEmpty().withMessage("Id can not be empty"), 5 | ]; 6 | 7 | const markSpoiler = async (req, res) => { 8 | try { 9 | // check if the post already marked as spoiler 10 | if (req.post.spoiler) { 11 | return res.status(409).json("Post content already blurred"); 12 | } 13 | 14 | // else mark it as spoiler and save 15 | req.post.spoiler = true; 16 | await req.post.save(); 17 | res.status(200).json("Spoiler set successfully"); 18 | } catch (error) { 19 | res.status(500).json("Internal Server Error"); 20 | } 21 | }; 22 | 23 | const unmarkSpoiler = async (req, res) => { 24 | try { 25 | // check if the post already unmarked as spoiler 26 | if (!req.post.spoiler) { 27 | return res.status(409).json("Post spoiler already turned off"); 28 | } 29 | 30 | // else unmark it and save 31 | req.post.spoiler = false; 32 | await req.post.save(); 33 | res.status(200).json("Post spoiler turned off successfully"); 34 | } catch (error) { 35 | res.status(500).json("Internal Server Error"); 36 | } 37 | }; 38 | 39 | const markNSFW = async (req, res) => { 40 | try { 41 | // check if the post already marked as nsfw 42 | if (req.post.nsfw) { 43 | return res.status(409).json("Post already marked NSFW"); 44 | } 45 | 46 | // else mark it as nsfw and save 47 | req.post.nsfw = true; 48 | await req.post.save(); 49 | res.status(200).json("Post marked NSFW successfully"); 50 | } catch (error) { 51 | res.status(500).json("Internal Server Error"); 52 | } 53 | }; 54 | 55 | const unmarkNSFW = async (req, res) => { 56 | try { 57 | // check if the post already marked as nsfw 58 | if (!req.post.nsfw) { 59 | return res.status(409).json("NSFW mark already removed"); 60 | } 61 | 62 | // else mark it as nsfw and save 63 | req.post.nsfw = false; 64 | await req.post.save(); 65 | res.status(200).json("NSFW unmarked successfully"); 66 | } catch (error) { 67 | res.status(500).json("Internal Server Error"); 68 | } 69 | }; 70 | 71 | export default { 72 | postActionsValidator, 73 | markSpoiler, 74 | unmarkSpoiler, 75 | markNSFW, 76 | unmarkNSFW, 77 | }; 78 | -------------------------------------------------------------------------------- /test/middlewares/subredditDetails.test.js: -------------------------------------------------------------------------------- 1 | import Subreddit from "../../models/Community"; 2 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 3 | import subredditDetailsMiddleware from "../../middleware/subredditDetails.js"; 4 | import User from "../../models/User.js"; 5 | import { jest } from "@jest/globals"; 6 | describe("subreddit details middlewares", () => { 7 | beforeAll(async () => { 8 | await connectDatabase(); 9 | }); 10 | afterAll(async () => { 11 | await User.deleteMany({}); 12 | await Subreddit.deleteMany({}); 13 | closeDatabaseConnection(); 14 | }); 15 | 16 | describe("checkSubreddit", () => { 17 | it("Valid subreddit", async () => { 18 | const subredditObject = await new Subreddit({ 19 | title: "title", 20 | viewName: "title", 21 | category: "Sports", 22 | type: "Public", 23 | owner: { 24 | username: "zeyad", 25 | }, 26 | }).save(); 27 | const nextFunction = jest.fn(); 28 | const req = { 29 | params: { 30 | subreddit: "title", 31 | }, 32 | }; 33 | await subredditDetailsMiddleware.checkSubreddit(req, {}, nextFunction); 34 | expect(nextFunction).toHaveBeenCalled(); 35 | await Subreddit.deleteMany({}); 36 | }); 37 | it("deleted subreddit", async () => { 38 | const subredditObject = await new Subreddit({ 39 | title: "title", 40 | viewName: "title", 41 | category: "Sports", 42 | type: "Public", 43 | owner: { 44 | username: "zeyad", 45 | }, 46 | deletedAt: Date.now(), 47 | }).save(); 48 | const mockResponse = { 49 | status: () => { 50 | jest.fn(); 51 | return mockResponse; 52 | }, 53 | json: () => { 54 | jest.fn(); 55 | return mockResponse; 56 | }, 57 | }; 58 | const nextFunction2 = jest.fn(); 59 | const req = { 60 | params: { 61 | subreddit: "title", 62 | }, 63 | }; 64 | await subredditDetailsMiddleware.checkSubreddit( 65 | req, 66 | mockResponse, 67 | nextFunction2 68 | ); 69 | expect(nextFunction2).not.toHaveBeenCalled(); 70 | await Subreddit.deleteMany({}); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /services/subredditDetails.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | 3 | /** 4 | * A function used to get the subreddit Details 5 | * @param {Object} subreddit the subreddit to get the Details 6 | * @param {Boolean} isLoggedIn to know if the user is logged in or not 7 | * @returns {Object} the details of the subreddit 8 | */ 9 | // eslint-disable-next-line max-statements 10 | export async function getSubredditDetails( 11 | subreddit, 12 | isLoggedIn, 13 | payload = null 14 | ) { 15 | let isMember = false; 16 | let isFavorite = false; 17 | let isModerator = false; 18 | 19 | const details = { 20 | subredditId: subreddit._id, 21 | title: subreddit.title, 22 | nsfw: subreddit.nsfw, 23 | nickname: subreddit.viewName, 24 | type: subreddit.type, 25 | category: subreddit.category, 26 | members: subreddit.members, 27 | // online: subreddit.online, 28 | description: subreddit.description, 29 | dateOfCreation: subreddit.dateOfCreation, 30 | banner: subreddit.banner, 31 | picture: subreddit.picture, 32 | views: subreddit.views, 33 | mainTopic: subreddit.mainTopic, 34 | subTopics: subreddit.subTopics, 35 | }; 36 | 37 | if (isLoggedIn) { 38 | const userId = payload.userId; 39 | const neededUser = await User.findById(userId); 40 | if (!neededUser) { 41 | const error = new Error("User not found"); 42 | error.statusCode = 404; 43 | throw error; 44 | } 45 | 46 | for (let i = 0; i < subreddit.moderators.length; i++) { 47 | if (subreddit.moderators[i].userID.toString() === userId) { 48 | isModerator = true; 49 | break; 50 | } 51 | } 52 | 53 | for (let i = 0; i < neededUser.joinedSubreddits.length; i++) { 54 | if (neededUser.joinedSubreddits[i].name === subreddit.title) { 55 | isMember = true; 56 | break; 57 | } 58 | } 59 | 60 | for (let i = 0; i < neededUser.favoritesSubreddits.length; i++) { 61 | if (neededUser.favoritesSubreddits[i].name === subreddit.title) { 62 | isFavorite = true; 63 | break; 64 | } 65 | } 66 | 67 | details.isModerator = isModerator; 68 | details.isMember = isMember; 69 | details.isFavorite = isFavorite; 70 | } 71 | subreddit.numberOfViews++; 72 | await subreddit.save(); 73 | 74 | return details; 75 | } 76 | -------------------------------------------------------------------------------- /tests/utils/subredditRulesUtilSpec.js: -------------------------------------------------------------------------------- 1 | import { 2 | validateCreatingRuleBody, 3 | validateEditingRuleBody, 4 | } from "../../utils/subredditRules.js"; 5 | 6 | describe("Testing subreddit rules utils", () => { 7 | it("validateCreatingRuleBody method should exist", () => { 8 | expect(validateCreatingRuleBody).toBeDefined(); 9 | }); 10 | 11 | it("validateEditingRuleBody method should exist", () => { 12 | expect(validateEditingRuleBody).toBeDefined(); 13 | }); 14 | 15 | it("Testing a body with missing required params", () => { 16 | const req = { 17 | body: { 18 | ruleName: "test rule", 19 | }, 20 | }; 21 | expect(validateCreatingRuleBody(req)).toBe(false); 22 | }); 23 | 24 | it("Testing a body with appliesTo invalid value", () => { 25 | const req = { 26 | body: { 27 | ruleName: "test rule", 28 | appliesTo: "any invalid value", 29 | }, 30 | }; 31 | expect(validateCreatingRuleBody(req)).toBe(false); 32 | }); 33 | 34 | it("Testing a body with appliesTo valid value", () => { 35 | const req = { 36 | body: { 37 | ruleName: "test rule", 38 | appliesTo: "comments only", 39 | }, 40 | }; 41 | expect(validateCreatingRuleBody(req)).toBe(true); 42 | }); 43 | 44 | it("Testing a body with all params", () => { 45 | const req = { 46 | body: { 47 | ruleName: "test rule", 48 | appliesTo: "comments only", 49 | description: "test desc", 50 | reportReason: "test reason", 51 | }, 52 | }; 53 | expect(validateCreatingRuleBody(req)).toBe(true); 54 | }); 55 | 56 | it("Testing a edit body without the rule order", () => { 57 | const req = { 58 | body: { 59 | ruleName: "test rule", 60 | appliesTo: "comments only", 61 | description: "test desc", 62 | reportReason: "test reason", 63 | }, 64 | }; 65 | expect(validateEditingRuleBody(req)).toBe(false); 66 | }); 67 | 68 | it("Testing a edit body with the rule order", () => { 69 | const req = { 70 | body: { 71 | ruleName: "test rule", 72 | appliesTo: "comments only", 73 | description: "test desc", 74 | reportReason: "test reason", 75 | ruleOrder: 2, 76 | }, 77 | }; 78 | expect(validateEditingRuleBody(req)).toBe(true); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /middleware/NJoiningValidation.js: -------------------------------------------------------------------------------- 1 | import { searchForUserService } from "../services/userServices.js"; 2 | import { searchForSubredditById } from "./../services/communityServices.js"; 3 | 4 | /** 5 | * Middleware used to check if the user joined the desired subreddit before or not 6 | * It gets the subreddits that the user joined before and searches for the desired one 7 | * if the desired one is found then the user cannot join it 8 | * but if he doesn't that user passed that validation successfully 9 | * it will send status code 400 saying the user joined this subreddit before 10 | * it may send status code 400 saying Token may be invalid or not found 11 | * 12 | * @param {Object} req Request object 13 | * @param {Object} res Response object 14 | * @param {function} next Next function 15 | * @returns {void} 16 | */ 17 | 18 | // eslint-disable-next-line max-statements 19 | export async function checkJoinedBefore(req, res, next) { 20 | const authPayload = req.payload; 21 | try { 22 | const user = await searchForUserService(authPayload.username); 23 | //GETTING LIST OF SUBREDDITS THE USER JOINED BEFORE 24 | const { joinedSubreddits } = user; 25 | const subreddit = await searchForSubredditById(req.body.subredditId); 26 | for (const smallSubreddit of joinedSubreddits) { 27 | //CHECKING IF THE SUBREDDIT HE WANTS TO JOIN WAS JOINED BEFORE 28 | if (smallSubreddit.subredditId.toString() === subreddit.id) { 29 | // eslint-disable-next-line max-len 30 | let error = new Error("you already joined the subreddit"); 31 | error.statusCode = 409; 32 | throw error; 33 | } 34 | } 35 | const waitedUsers = subreddit.waitedUsers; 36 | for (const user of waitedUsers) { 37 | //CHECKING IF THE SUBREDDIT HE WANTS TO JOIN WAS JOINED BEFORE 38 | if (user.userID.toString() === authPayload.userId) { 39 | let error = new Error("your request is already pending"); 40 | error.statusCode = 409; 41 | throw error; 42 | } 43 | } 44 | //CONTINUE TO JOIN CONTROLLER TO DO THE LOGIC OF JOINING 45 | next(); 46 | } catch (err) { 47 | console.log(err); 48 | if (err.statusCode) { 49 | res.status(err.statusCode).json({ 50 | error: err.message, 51 | }); 52 | } else { 53 | res.status(500).json("Internal Server Error"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/utils/sendEmails.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | sendResetPasswordEmail, 3 | sendVerifyEmail, 4 | sendUsernameEmail, 5 | } from "../../utils/sendEmails.js"; 6 | import User from "../../models/User.js"; 7 | import Token from "./../../models/VerifyToken.js"; 8 | import { connectDatabase, closeDatabaseConnection } from "./../database.js"; 9 | 10 | describe("Testing send emails functions", () => { 11 | // let user = {}, 12 | // token = {}; 13 | // beforeAll(async () => { 14 | // await connectDatabase(); 15 | 16 | // user = new User({ 17 | // username: "Beshoy", 18 | // email: "beshoy@gmail.com", 19 | // createdAt: Date.now(), 20 | // }); 21 | // await user.save(); 22 | 23 | // token = new Token({ 24 | // userId: user._id, 25 | // token: "token", 26 | // type: "type", 27 | // }); 28 | // await token.save(); 29 | // }); 30 | 31 | // afterAll(async () => { 32 | // await user.remove(); 33 | // await token.remove(); 34 | // await closeDatabaseConnection(); 35 | // }); 36 | 37 | // it("should have sendResetPasswordEmail method", () => { 38 | // expect(sendResetPasswordEmail).toBeDefined(); 39 | // }); 40 | 41 | // it("try to send reset password email", async () => { 42 | // expect(await sendResetPasswordEmail(user, token.token)).toBeTruthy(); 43 | // }); 44 | 45 | // it("try let sendResetPasswordEmail throw an error", async () => { 46 | // expect(await sendResetPasswordEmail(null, token.token)).toBeFalsy(); 47 | // }); 48 | 49 | it("should have sendVerifyEmail method", () => { 50 | expect(sendVerifyEmail).toBeDefined(); 51 | }); 52 | 53 | // it("try to send verify email", () => { 54 | // expect(sendVerifyEmail(user, token.token)).toBeTruthy(); 55 | // }); 56 | 57 | // it("try let sendVerifyEmail throw an error", () => { 58 | // expect(sendVerifyEmail(null, token.token)).toBeFalsy(); 59 | // }); 60 | 61 | // it("should have sendUsernameEmail method", () => { 62 | // expect(sendUsernameEmail).toBeDefined(); 63 | // }); 64 | 65 | // it("try to send forget username email", () => { 66 | // expect(sendUsernameEmail(user)).toBeTruthy(); 67 | // }); 68 | 69 | // it("try let sendUsernameEmail throw an error", () => { 70 | // expect(sendUsernameEmail(null)).toBeFalsy(); 71 | // }); 72 | }); 73 | -------------------------------------------------------------------------------- /models/Comment.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const commentSchema = mongoose.Schema({ 5 | parentId: { 6 | type: Schema.Types.ObjectId, 7 | required: true, 8 | }, 9 | parentType: { 10 | type: String, 11 | required: true, 12 | }, 13 | postId: { 14 | type: Schema.Types.ObjectId, 15 | required: true, 16 | ref: "Post", 17 | }, 18 | subredditName: { 19 | type: String, 20 | }, 21 | level: { 22 | type: Number, 23 | required: true, 24 | }, 25 | content: { 26 | type: Object, 27 | }, 28 | numberOfVotes: { 29 | type: Number, 30 | required: true, 31 | default: 1, 32 | }, 33 | createdAt: { 34 | type: Date, 35 | required: true, 36 | }, 37 | editedAt: { 38 | type: Date, 39 | }, 40 | deletedAt: { 41 | type: Date, 42 | }, 43 | ownerUsername: { 44 | type: String, 45 | required: true, 46 | }, 47 | ownerId: { 48 | type: Schema.Types.ObjectId, 49 | required: true, 50 | ref: "User", 51 | }, 52 | markedSpam: { 53 | type: Boolean, 54 | required: true, 55 | default: false, 56 | }, 57 | nsfw: { 58 | type: Boolean, 59 | required: true, 60 | default: false, 61 | }, 62 | children: [ 63 | { 64 | type: Schema.Types.ObjectId, 65 | ref: "Comment", 66 | }, 67 | ], 68 | moderation: { 69 | approve: { 70 | approvedBy: { 71 | type: String, 72 | }, 73 | approvedDate: { 74 | type: Date, 75 | }, 76 | }, 77 | remove: { 78 | removedBy: { 79 | type: String, 80 | }, 81 | removedDate: { 82 | type: Date, 83 | }, 84 | }, 85 | spam: { 86 | spammedBy: { 87 | type: String, 88 | }, 89 | spammedDate: { 90 | type: Date, 91 | }, 92 | }, 93 | lock: { 94 | type: Boolean, 95 | default: false, 96 | }, 97 | }, 98 | followingUsers: [ 99 | { 100 | username: { 101 | type: String, 102 | required: true, 103 | }, 104 | userId: { 105 | type: Schema.Types.ObjectId, 106 | ref: "User", 107 | }, 108 | }, 109 | ], 110 | }); 111 | 112 | const Comment = mongoose.model("Comment", commentSchema); 113 | 114 | export default Comment; 115 | -------------------------------------------------------------------------------- /test/utils/generateTokens.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | generateJWT, 3 | generateVerifyToken, 4 | } from "../../utils/generateTokens.js"; 5 | import User from "../../models/User.js"; 6 | import Token from "./../../models/VerifyToken.js"; 7 | import { connectDatabase, closeDatabaseConnection } from "./../database.js"; 8 | 9 | describe("Testing generate tokens", () => { 10 | beforeAll(async () => { 11 | await connectDatabase(); 12 | }); 13 | afterAll(async () => { 14 | await closeDatabaseConnection(); 15 | }); 16 | 17 | it("should have generateJWT method", () => { 18 | expect(generateJWT).toBeDefined(); 19 | }); 20 | 21 | it("try to let generateJWT to throw an error", () => { 22 | try { 23 | generateJWT(); 24 | } catch (err) { 25 | expect(err).toBeDefined(); 26 | } 27 | }); 28 | 29 | it("check if generateJWT returns a valid jwt", () => { 30 | const user = { 31 | userId: "mongodbId", 32 | username: "Beshoy", 33 | }; 34 | const token = generateJWT(user); 35 | try { 36 | const decodedPayload = jwt.verify(token, process.env.TOKEN_SECRET); 37 | expect(decodedPayload).toEqual(user); 38 | } catch (err) { 39 | return undefined; 40 | } 41 | }); 42 | 43 | it("try to send empty user object to generateJWT", () => { 44 | const user = {}; 45 | const token = generateJWT(user); 46 | try { 47 | const decodedPayload = jwt.verify(token, process.env.TOKEN_SECRET); 48 | expect(decodedPayload).toEqual(user); 49 | } catch (err) { 50 | return undefined; 51 | } 52 | }); 53 | 54 | it("should have generateVerifyToken method", () => { 55 | expect(generateVerifyToken).toBeDefined(); 56 | }); 57 | 58 | it("try to let generateVerifyToken to throw an error", async () => { 59 | try { 60 | await generateVerifyToken(); 61 | } catch (err) { 62 | expect(err).toBeDefined(); 63 | } 64 | }); 65 | 66 | // eslint-disable-next-line max-len 67 | it("check if generateVerifyToken returns a valid token with length 64", async () => { 68 | const user = new User({ 69 | username: "Beshoy", 70 | email: "beshoy@gmail.com", 71 | createdAt: Date.now(), 72 | }); 73 | await user.save(); 74 | const token = await generateVerifyToken(user._id, "random"); 75 | expect(token.length).toEqual(64); 76 | 77 | await user.remove(); 78 | await Token.deleteMany({}); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | install_nodejs: 5 | description: Install Node.js 13.8.0 6 | steps: 7 | - run: 8 | name: Install Node.js 13.8.0 9 | command: | 10 | # Install Node.js LTS version as our base Node.js version 11 | curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - 12 | sudo apt install -y nodejs 13 | # Use n version manager to use Node.js v13.8.0 14 | sudo npm install --global n 15 | sudo n 13.8. 16 | 17 | configure_docker: 18 | description: login with my dockerhub 19 | steps: 20 | - run: 21 | name: Install Node.js 13.8.0 22 | command: | 23 | docker login -u $DOCKERNAME -p $DOCKERPASS 24 | install_docker: 25 | description: Install docker 26 | steps: 27 | - setup_remote_docker 28 | - configure_docker 29 | - run: 30 | name: install docker 31 | command: | 32 | apk add --no-cache \ 33 | py-pip=9.0.0-r1 34 | pip install \ 35 | docker-compose==1.12.0 \ 36 | 37 | jobs: 38 | unit-testing: 39 | docker: 40 | - image: cimg/node:16.17.0 41 | steps: 42 | - checkout 43 | 44 | - restore_cache: 45 | name: Restore nodemodules Cache 46 | keys: 47 | - v1-node-modules-{{ checksum "package.json" }} 48 | - v1-node-modules- 49 | 50 | - run: 51 | name: Install npm dependencies 52 | command: | 53 | npm install 54 | 55 | - save_cache: 56 | name: Save nodemodelues Cache 57 | key: v1-node-modules-{{ checksum "package.json" }} 58 | paths: 59 | - node_modules 60 | 61 | - run: 62 | name: Run Unit Tests 63 | command: | 64 | npm run test 65 | 66 | build-backend: 67 | docker: 68 | - image: docker:17.05.0-ce-git 69 | steps: 70 | - checkout 71 | - install_docker 72 | - run: 73 | name: Build , tag , push front-end 74 | command: | 75 | docker build -t backend:v1 . 76 | docker tag backend:v1 waer/backend 77 | docker push waer/backend:latest 78 | 79 | workflows: 80 | main: 81 | jobs: 82 | - unit-testing 83 | - build-backend: 84 | requires: 85 | - unit-testing 86 | filters: 87 | branches: 88 | only: master -------------------------------------------------------------------------------- /services/topics.js: -------------------------------------------------------------------------------- 1 | import Topics from "../models/Topics.js"; 2 | 3 | const topics = [ 4 | "Activism", 5 | "Addition Support", 6 | "Animals And Pets", 7 | "Anime", 8 | "Art", 9 | "Beauty And Makeup", 10 | "Bussiness, Economics, And Finance", 11 | "Careers", 12 | "Cars And Motor Vehicles", 13 | "Celebrity", 14 | "Crafts And DIY", 15 | "Crypto", 16 | "Culture, Race, And Ethnicity", 17 | "Family And Relationships", 18 | "Fashion", 19 | "Fitness And Nutrition", 20 | "Funny/Humor", 21 | "Food And Drink", 22 | "Gaming", 23 | "Gender", 24 | "History", 25 | "Hobbies", 26 | "Home and Garden", 27 | "Internet Culture and Memes", 28 | "Law", 29 | "Learning and Education", 30 | "Marketplace and Deals", 31 | "Mature Themes and Adults", 32 | "Medical and Mental Health", 33 | "Men's Health", 34 | "Meta/Reddit", 35 | "Military", 36 | "Movies", 37 | "Music", 38 | "Ourdoors and Nature", 39 | "Place", 40 | "Podcasts and Streamers", 41 | "Politics", 42 | "Programming", 43 | "Reading, Writing and Literature", 44 | "Religion and Spirituality", 45 | "Science", 46 | "Sexual Orientation", 47 | "Sports", 48 | "Tabletop Games", 49 | "Technology", 50 | "Television", 51 | "Trauma Support", 52 | "Travel", 53 | "Woman's Health", 54 | "World News", 55 | "None Of These Topics", 56 | ]; 57 | 58 | /** 59 | * This function is used to insert a set of given topics to the database 60 | * if they do not already exist. 61 | * 62 | * @returns {void} 63 | */ 64 | export async function insertTopicsIfNotExists() { 65 | const count = await Topics.countDocuments(); 66 | if (count === 0) { 67 | for (let i = 0; i < topics.length; i++) { 68 | await new Topics({ 69 | topicName: topics[i], 70 | }).save(); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Function used to get all the topics stored in the database that can be used for this subreddit. 77 | * 78 | * @param {Object} subreddit Subreddit that we want to list its suggested topics 79 | * @returns {Object} Response object containing [statusCode, data] 80 | */ 81 | export async function getSuggestedTopicsService(subreddit) { 82 | const allTopics = await Topics.find({}).select("topicName"); 83 | 84 | const readyData = allTopics 85 | .filter( 86 | (el) => 87 | subreddit.mainTopic !== el.topicName && 88 | !subreddit.subTopics.includes(el.topicName) 89 | ) 90 | .map((el) => el.topicName); 91 | 92 | return { 93 | statusCode: 200, 94 | data: { 95 | communityTopics: readyData, 96 | }, 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /test/services/userDetails.test.js: -------------------------------------------------------------------------------- 1 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 2 | import User from "./../../models/User.js"; 3 | import { 4 | getUserDetailsService, 5 | getUserFollowedUsersService, 6 | } from "../../services/userServices.js"; 7 | 8 | describe("Testing user details services", () => { 9 | beforeAll(async () => { 10 | await connectDatabase(); 11 | }); 12 | afterAll(async () => { 13 | await User.deleteMany({}); 14 | closeDatabaseConnection(); 15 | }); 16 | describe("getUserDetailsService", () => { 17 | it("Deleted user", async () => { 18 | const user = await new User({ 19 | username: "zeyad", 20 | createdAt: Date.now(), 21 | deletedAt: Date.now(), 22 | }).save(); 23 | 24 | await expect(getUserDetailsService("zeyad")).rejects.toThrow( 25 | "User not found" 26 | ); 27 | 28 | await User.deleteMany({}); 29 | }); 30 | it("valid user", async () => { 31 | const user = await new User({ 32 | username: "zeyad", 33 | createdAt: Date.now(), 34 | karma: 2, 35 | }).save(); 36 | 37 | const details = await getUserDetailsService("zeyad"); 38 | expect(details).toMatchObject({ karma: 2 }); 39 | 40 | await User.deleteMany({}); 41 | }); 42 | }); 43 | describe("getUserFollowedUsersService", () => { 44 | it("Deleted user", async () => { 45 | const user = await new User({ 46 | username: "zeyad1", 47 | createdAt: Date.now(), 48 | deletedAt: Date.now(), 49 | }).save(); 50 | 51 | await expect(getUserFollowedUsersService(user._id)).rejects.toThrow( 52 | "User isn't found" 53 | ); 54 | 55 | await User.deleteMany({}); 56 | }); 57 | it("valid user", async () => { 58 | const user = await new User({ 59 | username: "zeyad", 60 | createdAt: Date.now(), 61 | karma: 2, 62 | }).save(); 63 | const user3 = await new User({ 64 | username: "zeyad2", 65 | createdAt: Date.now(), 66 | karma: 2, 67 | }).save(); 68 | const user4 = await new User({ 69 | username: "zeyad3", 70 | createdAt: Date.now(), 71 | karma: 2, 72 | }).save(); 73 | user4.followedUsers.push(user3._id); 74 | user4.followedUsers.push(user._id); 75 | 76 | await user4.save(); 77 | 78 | const details = await getUserFollowedUsersService(user4._id); 79 | expect(details.length).toBe(2); 80 | 81 | // await User.deleteMany({}); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/middlewares/verifySignUp.test.js: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import { checkDuplicateUsernameOrEmail } from "../../middleware/verifySignUp"; 3 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 4 | import User from "./../../models/User.js"; 5 | 6 | // eslint-disable-next-line max-statements 7 | describe("Testing verifySignUp middleware", () => { 8 | let mockRequest; 9 | let mockResponse; 10 | let nextFunction = jest.fn(); 11 | 12 | beforeAll(async () => { 13 | await connectDatabase(); 14 | 15 | await new User({ 16 | username: "Beshoy", 17 | email: "besho@gmail.com", 18 | createdAt: Date.now(), 19 | }).save(); 20 | }); 21 | 22 | afterAll(async () => { 23 | await User.deleteMany({}); 24 | await closeDatabaseConnection(); 25 | }); 26 | 27 | beforeEach(() => { 28 | mockRequest = {}; 29 | mockResponse = { 30 | status: () => { 31 | jest.fn(); 32 | return mockResponse; 33 | }, 34 | json: () => { 35 | jest.fn(); 36 | return mockResponse; 37 | }, 38 | }; 39 | }); 40 | 41 | it("should have checkDuplicateUsernameOrEmail function", () => { 42 | expect(checkDuplicateUsernameOrEmail).toBeDefined(); 43 | }); 44 | 45 | it("try to use a username already used", async () => { 46 | mockRequest = { 47 | body: { 48 | username: "Beshoy", 49 | email: "new@gmail.com", 50 | }, 51 | }; 52 | await checkDuplicateUsernameOrEmail( 53 | mockRequest, 54 | mockResponse, 55 | nextFunction 56 | ); 57 | expect(nextFunction).not.toHaveBeenCalled(); 58 | }); 59 | 60 | it("try to use an email already used", async () => { 61 | mockRequest = { 62 | body: { 63 | username: "new", 64 | email: "besho@gmail.com", 65 | }, 66 | }; 67 | await checkDuplicateUsernameOrEmail( 68 | mockRequest, 69 | mockResponse, 70 | nextFunction 71 | ); 72 | expect(nextFunction).not.toHaveBeenCalled(); 73 | }); 74 | 75 | it("try to use a new username and email", async () => { 76 | mockRequest = { 77 | body: { 78 | username: "new", 79 | email: "new@gmail.com", 80 | }, 81 | }; 82 | await checkDuplicateUsernameOrEmail( 83 | mockRequest, 84 | mockResponse, 85 | nextFunction 86 | ); 87 | expect(nextFunction).toHaveBeenCalled(); 88 | }); 89 | 90 | it("try to let checkDuplicateUsernameOrEmail throw an error", async () => { 91 | try { 92 | await checkDuplicateUsernameOrEmail({}, mockResponse, nextFunction); 93 | } catch (error) { 94 | expect(error).toBeDefined(); 95 | } 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/utils/subredditRules.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | validateCreatingRuleBody, 3 | validateEditingRuleBody, 4 | } from "../../utils/subredditRules.js"; 5 | 6 | describe("Testing subreddit rules utils", () => { 7 | it("validateCreatingRuleBody method should exist", () => { 8 | expect(validateCreatingRuleBody).toBeDefined(); 9 | }); 10 | 11 | it("validateEditingRuleBody method should exist", () => { 12 | expect(validateEditingRuleBody).toBeDefined(); 13 | }); 14 | 15 | it("Testing a body with missing required params", () => { 16 | const req = { 17 | body: { 18 | ruleName: "test rule", 19 | }, 20 | }; 21 | expect(validateCreatingRuleBody(req)).toBe(false); 22 | }); 23 | 24 | it("Testing a body with appliesTo invalid value", () => { 25 | const req = { 26 | body: { 27 | ruleName: "test rule", 28 | appliesTo: "any invalid value", 29 | }, 30 | }; 31 | expect(validateCreatingRuleBody(req)).toBe(false); 32 | }); 33 | 34 | it("Testing a body with appliesTo valid value", () => { 35 | const req = { 36 | body: { 37 | ruleName: "test rule", 38 | appliesTo: "comments only", 39 | }, 40 | }; 41 | expect(validateCreatingRuleBody(req)).toBe(true); 42 | }); 43 | 44 | it("Testing a body with all params", () => { 45 | const req = { 46 | body: { 47 | ruleName: "test rule", 48 | appliesTo: "comments only", 49 | description: "test desc", 50 | reportReason: "test reason", 51 | }, 52 | }; 53 | expect(validateCreatingRuleBody(req)).toBe(true); 54 | }); 55 | 56 | it("Testing a edit body without the rule order", () => { 57 | const req = { 58 | body: { 59 | ruleName: "test rule", 60 | appliesTo: "comments only", 61 | description: "test desc", 62 | reportReason: "test reason", 63 | }, 64 | }; 65 | expect(validateEditingRuleBody(req)).toBe(false); 66 | }); 67 | 68 | it("Testing a edit body with the rule order", () => { 69 | const req = { 70 | body: { 71 | ruleName: "test rule", 72 | appliesTo: "comments only", 73 | description: "test desc", 74 | reportReason: "test reason", 75 | ruleOrder: 0, 76 | }, 77 | }; 78 | expect(validateEditingRuleBody(req)).toBe(true); 79 | }); 80 | 81 | it("Testing a edit body with the rule order", () => { 82 | const req = { 83 | body: { 84 | ruleName: "test rule", 85 | appliesTo: "comments only", 86 | description: "test desc", 87 | reportReason: "test reason", 88 | ruleOrder: 1, 89 | }, 90 | }; 91 | expect(validateEditingRuleBody(req)).toBe(true); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/services/topicsServices.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | insertTopicsIfNotExists, 3 | getSuggestedTopicsService, 4 | } from "../../services/topics.js"; 5 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 6 | import Topics from "./../../models/Topics.js"; 7 | import Subreddit from "./../../models/Community.js"; 8 | import User from "../../models/User.js"; 9 | 10 | describe("Testing Topics services", () => { 11 | let user = {}, 12 | subreddit = {}; 13 | beforeAll(async () => { 14 | await connectDatabase(); 15 | 16 | user = await new User({ 17 | username: "Beshoy", 18 | email: "beshoy@gmail.com", 19 | createdAt: Date.now(), 20 | }).save(); 21 | 22 | subreddit = await new Subreddit({ 23 | title: "subreddit", 24 | viewName: "SR", 25 | category: "Sports", 26 | type: "Public", 27 | nsfw: false, 28 | members: 9, 29 | owner: { 30 | username: user.username, 31 | userID: user._id, 32 | }, 33 | dateOfCreation: Date.now(), 34 | }).save(); 35 | }); 36 | 37 | afterAll(async () => { 38 | await Topics.deleteMany({}); 39 | await Subreddit.deleteMany({}); 40 | await User.deleteMany({}); 41 | await closeDatabaseConnection(); 42 | }); 43 | 44 | it("should have insertTopicsIfNotExists function", () => { 45 | expect(insertTopicsIfNotExists).toBeDefined(); 46 | }); 47 | 48 | it("check that topics count is 52", async () => { 49 | await insertTopicsIfNotExists(); 50 | const count = await Topics.countDocuments(); 51 | expect(count).toEqual(52); 52 | }); 53 | 54 | it("try to get the suggested topics for a subreddit with no main topic or subTopics", async () => { 55 | const list = await getSuggestedTopicsService(subreddit); 56 | expect(list.data.communityTopics.length).toEqual(52); 57 | }); 58 | 59 | it("try to get the suggested topics for a subreddit with main topic and without subTopics", async () => { 60 | subreddit.mainTopic = "Activism"; 61 | await subreddit.save(); 62 | const list = await getSuggestedTopicsService(subreddit); 63 | expect(list.data.communityTopics.length).toEqual(51); 64 | }); 65 | 66 | it("try to get the suggested topics for a subreddit with no main topic and with 2 subTopics", async () => { 67 | subreddit.mainTopic = ""; 68 | subreddit.subTopics = ["Travel", "World News"]; 69 | await subreddit.save(); 70 | const list = await getSuggestedTopicsService(subreddit); 71 | expect(list.data.communityTopics.length).toEqual(50); 72 | }); 73 | 74 | it("try to get the suggested topics for a subreddit with main topic and with 2 subTopics", async () => { 75 | subreddit.mainTopic = "Art"; 76 | subreddit.subTopics = ["Travel", "World News"]; 77 | await subreddit.save(); 78 | const list = await getSuggestedTopicsService(subreddit); 79 | expect(list.data.communityTopics.length).toEqual(49); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | apidoc 2 | images 3 | private 4 | videos 5 | # test_output 6 | # test_output2 7 | test-report.html 8 | # Logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | .pnpm-debug.log* 15 | 16 | package-lock.json 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | node_modules/ 51 | jspm_packages/ 52 | 53 | # Snowpack dependency directory (https://snowpack.dev/) 54 | web_modules/ 55 | 56 | # TypeScript cache 57 | *.tsbuildinfo 58 | 59 | # Optional npm cache directory 60 | .npm 61 | 62 | # Optional eslint cache 63 | .eslintcache 64 | 65 | # Optional stylelint cache 66 | .stylelintcache 67 | 68 | # Microbundle cache 69 | .rpt2_cache/ 70 | .rts2_cache_cjs/ 71 | .rts2_cache_es/ 72 | .rts2_cache_umd/ 73 | 74 | # Optional REPL history 75 | .node_repl_history 76 | 77 | # Output of 'npm pack' 78 | *.tgz 79 | 80 | # Yarn Integrity file 81 | .yarn-integrity 82 | 83 | # dotenv environment variable files 84 | .env 85 | .env.development 86 | .env.development.local 87 | .env.test.local 88 | .env.production 89 | .env.production.local 90 | .env.local 91 | .env.testing 92 | 93 | # parcel-bundler cache (https://parceljs.org/) 94 | .cache 95 | .parcel-cache 96 | 97 | # Next.js build output 98 | .next 99 | out 100 | 101 | # Nuxt.js build / generate output 102 | .nuxt 103 | dist 104 | 105 | # Gatsby files 106 | .cache/ 107 | # Comment in the public line in if your project uses Gatsby and not Next.js 108 | # https://nextjs.org/blog/next-9-1#public-directory-support 109 | # public 110 | 111 | # vuepress build output 112 | .vuepress/dist 113 | 114 | # vuepress v2.x temp and cache directory 115 | .temp 116 | .cache 117 | 118 | # Serverless directories 119 | .serverless/ 120 | 121 | # FuseBox cache 122 | .fusebox/ 123 | 124 | # DynamoDB Local files 125 | .dynamodb/ 126 | 127 | # TernJS port file 128 | .tern-port 129 | 130 | # Stores VSCode versions used for testing VSCode extensions 131 | .vscode-test 132 | 133 | # yarn v2 134 | .yarn/cache 135 | .yarn/unplugged 136 | .yarn/build-state.yml 137 | .yarn/install-state.gz 138 | .pnp.* 139 | 140 | # Documentation 141 | docs/ 142 | 143 | 144 | *.env -------------------------------------------------------------------------------- /utils/messagesUtils.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | /** 3 | * This function is used to add a msg to the user's sent message list 4 | * @param {Object} message message that will be sent by the user 5 | * @param {String} userId the id of the user that the msg will be sent from 6 | * @returns {boolean} indicates if the message was sent successfully or not 7 | */ 8 | 9 | export async function addSentMessages(userId, message) { 10 | const user = await User.findById(userId); 11 | for (const msg of user.sentMessages) { 12 | if (msg === message.id) { 13 | let err = new Error("This msg already exists"); 14 | err.statusCode = 400; 15 | throw err; 16 | } 17 | } 18 | user.sentMessages.push(message.id); 19 | await user.save(); 20 | return true; 21 | } 22 | /** 23 | * This function is used to add a msg to the user's received message list 24 | * @param {Object} message message that will be received by the user 25 | * @param {String} userId the id of the user that the msg will be received 26 | * @returns {boolean} indicates if the message was received successfully or not 27 | */ 28 | export async function addReceivedMessages(userId, message) { 29 | const user = await User.findById(userId); 30 | for (const msg of user.receivedMessages) { 31 | if (msg === message.id) { 32 | let err = new Error("This msg already exists"); 33 | err.statusCode = 400; 34 | throw err; 35 | } 36 | } 37 | user.receivedMessages.push(message.id); 38 | await user.save(); 39 | return true; 40 | } 41 | /** 42 | * This function is used to add a msg to the user's mention list 43 | * @param {Object} message the mention that is done for the user 44 | * @param {String} userId the id of the user that got the mention 45 | * @returns {boolean} indicates if the mention was made successfully or not 46 | */ 47 | export async function addUserMention(userId, message) { 48 | const user = await User.findById(userId); 49 | for (const mention of user.usernameMentions) { 50 | if (mention === message.id) { 51 | let err = new Error("This mention already exists"); 52 | err.statusCode = 400; 53 | throw err; 54 | } 55 | } 56 | user.usernameMentions.push(message.id); 57 | await user.save(); 58 | return true; 59 | } 60 | /** 61 | * This function is used to add a msg to the user's post reply list 62 | * @param {Object} message the post reply that the user got 63 | * @param {String} userId the id of the user that got the post reply 64 | * @returns {boolean} indicates if the post reply was made successfully or not 65 | */ 66 | export async function addPostReply(userId, message) { 67 | try { 68 | const user = await User.findById(userId); 69 | for (const postReply of user.usernameMentions) { 70 | if (postReply === message.id) { 71 | let err = new Error("This postReply already exists"); 72 | err.statusCode = 400; 73 | throw err; 74 | } 75 | } 76 | user.postReplies.push(message.id); 77 | await user.save(); 78 | return true; 79 | } catch (err) { 80 | return "Couldn't Add the message"; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /middleware/verifyPostActions.js: -------------------------------------------------------------------------------- 1 | import Post from "../models/Post.js"; 2 | import Subreddit from "../models/Community.js"; 3 | import mongoose from "mongoose"; 4 | 5 | /** 6 | * Middleware used to check if the post that we want to perform the action on 7 | * is exist or not deleted before. 8 | * It also check if the post belong to the user making the request or no. 9 | * 10 | * @param {Object} req Request object 11 | * @param {Object} res Response object 12 | * @param {function} next Next function 13 | * @returns {void} 14 | */ 15 | export async function verifyPostActions(req, res, next) { 16 | try { 17 | let { id } = req.body; 18 | if (!id) { 19 | id = req.query.id; 20 | } 21 | const { userId } = req.payload; 22 | 23 | if (!mongoose.Types.ObjectId.isValid(id)) { 24 | return res.status(400).json({ 25 | error: "In valid id", 26 | }); 27 | } 28 | const post = await Post.findById(id); 29 | 30 | if (!post || post.deletedAt) { 31 | return res.status(404).json("Post not found"); 32 | } 33 | 34 | // check if the post does not belong to the user making the request 35 | if (post.ownerId.toString() !== userId.toString()) { 36 | return res.status(401).json("Access Denied"); 37 | } 38 | 39 | req.post = post; 40 | next(); 41 | } catch (error) { 42 | res.status(500).json("Internal Server Error"); 43 | } 44 | } 45 | 46 | /** 47 | * Middleware used to check the user seeing the post insights is the 48 | * owner or mod of the post subreddit. If it's not a subreddit post, then 49 | * the logged in user has to be the owner of the post or else an error response 50 | * is returned 51 | * 52 | * @param {Object} req Request object 53 | * @param {Object} res Response object 54 | * @param {function} next Next function 55 | * @returns {void} 56 | */ 57 | // eslint-disable-next-line max-statements 58 | export async function verifyPostInsights(req, res, next) { 59 | try { 60 | let { id } = req.query; 61 | const { userId } = req.payload; 62 | 63 | if (!mongoose.Types.ObjectId.isValid(id)) { 64 | return res.status(400).json({ 65 | error: "Invalid ID", 66 | }); 67 | } 68 | const post = await Post.findById(id); 69 | 70 | if (!post || post.deletedAt) { 71 | return res.status(404).json("Post not found"); 72 | } 73 | 74 | if (!post.subredditName) { 75 | if (post.ownerId.toString() !== userId.toString()) { 76 | return res.status(401).json("Access Denied"); 77 | } 78 | } else { 79 | const subreddit = await Subreddit.findOne({ title: post.subredditName }); 80 | if (!subreddit || subreddit.deletedAt) { 81 | return res.status(404).json("Subreddit not found"); 82 | } 83 | if ( 84 | post.ownerId.toString() !== userId.toString() && 85 | !subreddit.moderators.find( 86 | (mod) => mod.userID.toString() === userId.toString() 87 | ) 88 | ) { 89 | return res.status(401).json("Access Denied"); 90 | } 91 | } 92 | 93 | req.post = post; 94 | next(); 95 | } catch (error) { 96 | res.status(500).json("Internal Server Error"); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/endpoints/readMessagesSpec.js: -------------------------------------------------------------------------------- 1 | import supertest from "supertest"; 2 | import app from "../../app.js"; 3 | import User from "../../models/User.js"; 4 | import Message from "../../models/Message.js"; 5 | import { generateJWT } from "../../utils/generateTokens.js"; 6 | import { hashPassword } from "../../utils/passwordUtils.js"; 7 | 8 | const request = supertest(app); 9 | 10 | // eslint-disable-next-line max-statements 11 | fdescribe("Testing Read-all-msgs endpoint", () => { 12 | let user, token; 13 | beforeAll(async () => { 14 | const postReply = await new Message({ 15 | type: "postReply", 16 | text: "Message text", 17 | senderUsername: "Ahmed", 18 | receiverUsername: "Mohamed", 19 | subject: "Message Subject", 20 | }).save(); 21 | const usernameMention = await new Message({ 22 | type: "usernameMention", 23 | text: "Message text", 24 | senderUsername: "Ahmed", 25 | receiverUsername: "Mohamed", 26 | subject: "Message Subject", 27 | }).save(); 28 | const message = await new Message({ 29 | type: "privateMessage", 30 | text: "Message text", 31 | senderUsername: "Ahmed", 32 | receiverUsername: "Mohamed", 33 | subject: "Message Subject", 34 | }).save(); 35 | user = await new User({ 36 | username: "Ahmed", 37 | password: hashPassword("12345678"), 38 | email: "ahmed@gmail.com", 39 | receivedMessages: [message.id], 40 | postReplies: [postReply.id], 41 | usernameMentions: [usernameMention.id], 42 | }).save(); 43 | token = generateJWT(user); 44 | }); 45 | 46 | afterAll(async () => { 47 | await User.deleteMany({}); 48 | await Message.deleteMany({}); 49 | }); 50 | 51 | it("Read messages without token", async () => { 52 | const response = await request.patch("/read-all-msgs").send({ 53 | type: "Messages", 54 | }); 55 | 56 | expect(response.statusCode).toEqual(401); 57 | }); 58 | 59 | it("Read username mentions", async () => { 60 | const response = await request 61 | .patch("/read-all-msgs") 62 | .send({ 63 | type: "Username Mentions", 64 | }) 65 | .set("Authorization", "Bearer " + token); 66 | 67 | expect(response.statusCode).toEqual(200); 68 | const msg = await Message.findById(user.usernameMentions[0]); 69 | expect(msg.isRead).toBeTruthy(); 70 | }); 71 | 72 | it("Read post replies", async () => { 73 | const response = await request 74 | .patch("/read-all-msgs") 75 | .send({ 76 | type: "Post Replies", 77 | }) 78 | .set("Authorization", "Bearer " + token); 79 | 80 | expect(response.statusCode).toEqual(200); 81 | const msg = await Message.findById(user.postReplies[0]); 82 | expect(msg.isRead).toBeTruthy(); 83 | }); 84 | 85 | it("Read received messages", async () => { 86 | const response = await request 87 | .patch("/read-all-msgs") 88 | .send({ 89 | type: "Messages", 90 | }) 91 | .set("Authorization", "Bearer " + token); 92 | 93 | expect(response.statusCode).toEqual(200); 94 | const msg = await Message.findById(user.receivedMessages[0]); 95 | expect(msg.isRead).toBeTruthy(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /utils/prepareUserListing.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import User from "../models/User.js"; 3 | 4 | /** 5 | * Function to prepare the listing parameters for users and set the appropriate condition that will be used with mongoose later. 6 | * Check the limit of the result, and the anchor point of the slice to get the previous or next things 7 | * 8 | * @param {Object} listingParams Listing parameters sent in the request query [before, after, limit] 9 | * @returns {Object} Result object containing the final results after listing [listing, limit] 10 | */ 11 | // eslint-disable-next-line max-statements 12 | export async function prepareListingUsers(listingParams) { 13 | let result = {}; 14 | 15 | // prepare the limit 16 | if (!listingParams.limit) { 17 | result.limit = 25; 18 | } else { 19 | listingParams.limit = parseInt(listingParams.limit); 20 | if (listingParams.limit > 100) { 21 | result.limit = 100; 22 | } else if (listingParams.limit <= 0) { 23 | result.limit = 1; 24 | } else { 25 | result.limit = listingParams.limit; 26 | } 27 | } 28 | 29 | // check if after or before 30 | if (!listingParams.after && !listingParams.before) { 31 | result.listing = null; 32 | } else if (!listingParams.after && listingParams.before) { 33 | if (mongoose.Types.ObjectId.isValid(listingParams.before)) { 34 | // get the wanted value that we will split from 35 | const user = await User.findById(listingParams.before); 36 | if (!user) { 37 | result.listing = null; 38 | } else { 39 | result.listing = { 40 | type: "_id", 41 | value: { $lt: listingParams.before }, 42 | }; 43 | } 44 | } else { 45 | result.listing = null; 46 | } 47 | } else if (listingParams.after && !listingParams.before) { 48 | if (mongoose.Types.ObjectId.isValid(listingParams.after)) { 49 | // get the wanted value that we will split from 50 | const user = await User.findById(listingParams.after); 51 | if (!user) { 52 | result.listing = null; 53 | } else { 54 | result.listing = { 55 | type: "_id", 56 | value: { $gt: listingParams.after }, 57 | }; 58 | } 59 | } else { 60 | result.listing = null; 61 | } 62 | } else { 63 | result.listing = null; 64 | } 65 | 66 | return result; 67 | } 68 | 69 | /** 70 | * Function to create the exact condition that will be used by mongoose directly to list users. 71 | * Used to map every listing parameter to the exact query that mongoose will use later 72 | * 73 | * @param {Object} listingParams Listing parameters sent in the request query [before, after, limit] 74 | * @returns {Object} The final results that will be used by mongoose to list posts 75 | */ 76 | export async function userListing(listingParams) { 77 | let result = {}; 78 | listingParams = await prepareListingUsers(listingParams); 79 | if (listingParams.listing) { 80 | result.find = { deletedAt: null }; 81 | result.find[listingParams.listing.type] = listingParams.listing.value; 82 | } else { 83 | result.find = { deletedAt: null }; 84 | } 85 | 86 | result.limit = listingParams.limit; 87 | 88 | return result; 89 | } 90 | -------------------------------------------------------------------------------- /utils/prepareSubredditListing.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Subreddit from "../models/Community.js"; 3 | 4 | /** 5 | * Function to prepare the listing parameters for subreddits and set the appropriate condition that will be used with mongoose later. 6 | * Check the limit of the result, and the anchor point of the slice to get the previous or next things 7 | * 8 | * @param {Object} listingParams Listing parameters sent in the request query [before, after, limit] 9 | * @returns {Object} Result object containing the final results after listing [listing, limit] 10 | */ 11 | // eslint-disable-next-line max-statements 12 | export async function prepareListingSubreddits(listingParams) { 13 | let result = {}; 14 | 15 | // prepare the limit 16 | if (!listingParams.limit) { 17 | result.limit = 25; 18 | } else { 19 | listingParams.limit = parseInt(listingParams.limit); 20 | if (listingParams.limit > 100) { 21 | result.limit = 100; 22 | } else if (listingParams.limit <= 0) { 23 | result.limit = 1; 24 | } else { 25 | result.limit = listingParams.limit; 26 | } 27 | } 28 | 29 | // check if after or before 30 | if (!listingParams.after && !listingParams.before) { 31 | result.listing = null; 32 | } else if (!listingParams.after && listingParams.before) { 33 | if (mongoose.Types.ObjectId.isValid(listingParams.before)) { 34 | // get the wanted value that we will split from 35 | const subreddit = await Subreddit.findById(listingParams.before); 36 | if (!subreddit) { 37 | result.listing = null; 38 | } else { 39 | result.listing = { 40 | type: "_id", 41 | value: { $lt: listingParams.before }, 42 | }; 43 | } 44 | } else { 45 | result.listing = null; 46 | } 47 | } else if (listingParams.after && !listingParams.before) { 48 | if (mongoose.Types.ObjectId.isValid(listingParams.after)) { 49 | // get the wanted value that we will split from 50 | const subreddit = await Subreddit.findById(listingParams.after); 51 | if (!subreddit) { 52 | result.listing = null; 53 | } else { 54 | result.listing = { 55 | type: "_id", 56 | value: { $gt: listingParams.after }, 57 | }; 58 | } 59 | } else { 60 | result.listing = null; 61 | } 62 | } else { 63 | result.listing = null; 64 | } 65 | 66 | return result; 67 | } 68 | 69 | /** 70 | * Function to create the exact condition that will be used by mongoose directly to list subreddits. 71 | * Used to map every listing parameter to the exact query that mongoose will use later 72 | * 73 | * @param {Object} listingParams Listing parameters sent in the request query [before, after, limit] 74 | * @returns {Object} The final results that will be used by mongoose to list posts 75 | */ 76 | export async function subredditListing(listingParams) { 77 | let result = {}; 78 | listingParams = await prepareListingSubreddits(listingParams); 79 | if (listingParams.listing) { 80 | result.find = { deletedAt: null }; 81 | result.find[listingParams.listing.type] = listingParams.listing.value; 82 | } else { 83 | result.find = { deletedAt: null }; 84 | } 85 | 86 | result.limit = listingParams.limit; 87 | 88 | return result; 89 | } 90 | -------------------------------------------------------------------------------- /services/commentActionsServices.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | import Comment from "../models/Comment.js"; 3 | 4 | /** 5 | * A function used to validate the comment and check if that comment exists and throws error if the comment is not found and returns the comment if it exists 6 | * @param {ObjectId} commentId the id of the comment 7 | * @returns {Comment} the neededComment 8 | */ 9 | export async function validateExistingComment(commentId) { 10 | const neededComment = await Comment.findById(commentId); 11 | if (!neededComment || neededComment.deletedAt) { 12 | const error = new Error("Comment not found"); 13 | error.statusCode = 404; 14 | throw error; 15 | } 16 | return neededComment; 17 | } 18 | 19 | /** 20 | * A function used to add the comment to the user followed comments 21 | * @param {ObjectId} userId the id of the user 22 | * @param {ObjectId} commentId the id of the comment 23 | * @returns {Object} the neededComment and user to make the next step easier 24 | */ 25 | export async function addToUserFollowedComments(userId, commentId) { 26 | const neededUser = await User.findById(userId); 27 | const neededComment = await validateExistingComment(commentId); 28 | const comment = neededUser.followedComments.find( 29 | (comment) => comment.toString() === commentId 30 | ); 31 | if (comment) { 32 | const error = new Error("You are following this comment"); 33 | error.statusCode = 400; 34 | throw error; 35 | } 36 | neededUser.followedComments.push(commentId); 37 | await neededUser.save(); 38 | return { comment: neededComment, user: neededUser }; 39 | } 40 | 41 | /** 42 | * A function used to add the user to the comment following users 43 | * @param {User} user that specific user 44 | * @param {Comment} comment that specific comment 45 | * @returns {void} 46 | */ 47 | export async function addToCommentFollowedUsers(user, comment) { 48 | comment.followingUsers.push({ 49 | username: user.username, 50 | userId: user._id, 51 | }); 52 | await comment.save(); 53 | } 54 | 55 | /** 56 | * A function used to remove the comment from the user followed comments 57 | * @param {ObjectId} userId the id of the user 58 | * @param {ObjectId} commentId the id of the comment 59 | * @returns {Object} the neededComment and user to make the next step easier 60 | */ 61 | /* istanbul ignore next */ 62 | export async function removeFromUserFollowedComments(userId, commentId) { 63 | const neededUser = await User.findById(userId); 64 | const neededComment = await validateExistingComment(commentId); 65 | const commentIndex = neededUser.followedComments.findIndex( 66 | (comment) => comment.toString() === commentId 67 | ); 68 | if (commentIndex === -1) { 69 | const error = new Error("You are not following this comment"); 70 | error.statusCode = 400; 71 | throw error; 72 | } 73 | neededUser.followedComments.splice(commentIndex, 1); 74 | await neededUser.save(); 75 | return { comment: neededComment, user: neededUser }; 76 | } 77 | 78 | /** 79 | * A function used to remove the user from the comment following users 80 | * @param {User} user that specific user 81 | * @param {Comment} comment that specific comment 82 | * @returns {void} 83 | */ 84 | export async function removeFromCommentFollowedUsers(user, comment) { 85 | const userIndex = comment.followingUsers.findIndex( 86 | (userItem) => userItem.userId.toString() === user._id.toString() 87 | ); 88 | comment.followingUsers.splice(userIndex, 1); 89 | await comment.save(); 90 | } 91 | -------------------------------------------------------------------------------- /routes/comment-action.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { verifyAuthToken } from "../middleware/verifyToken.js"; 3 | import { validateRequestSchema } from "../middleware/validationResult.js"; 4 | // eslint-disable-next-line max-len 5 | import commentActionsController from "../controllers/commentActionsController.js"; 6 | // eslint-disable-next-line new-cap 7 | const commentActionsRouter = express.Router(); 8 | /** 9 | * @swagger 10 | * /follow-comment: 11 | * post: 12 | * summary: Follow a comment. 13 | * tags: [Comments] 14 | * requestBody: 15 | * required: true 16 | * content: 17 | * application/json: 18 | * schema: 19 | * type: object 20 | * properties: 21 | * commentId: 22 | * type: string 23 | * description: id of a comment 24 | * responses: 25 | * 200: 26 | * description: Followed comment successfully 27 | * 400: 28 | * description: The request was invalid. You may refer to response for details around why this happened. 29 | * content: 30 | * application/json: 31 | * schema: 32 | * properties: 33 | * error: 34 | * type: string 35 | * description: Type of error 36 | * 401: 37 | * description: User unauthorized to follow this comment 38 | * 404: 39 | * description: Comment not found 40 | * 500: 41 | * description: Server Error 42 | * security: 43 | * - bearerAuth: [] 44 | */ 45 | commentActionsRouter.post( 46 | "/follow-comment", 47 | verifyAuthToken, 48 | commentActionsController.followUnfollowValidator, 49 | validateRequestSchema, 50 | commentActionsController.followComment 51 | ); 52 | 53 | /** 54 | * @swagger 55 | * /unfollow-comment: 56 | * post: 57 | * summary: Unfollow a comment. 58 | * tags: [Comments] 59 | * requestBody: 60 | * required: true 61 | * content: 62 | * application/json: 63 | * schema: 64 | * type: object 65 | * properties: 66 | * commentId: 67 | * type: string 68 | * description: id of a comment 69 | * responses: 70 | * 200: 71 | * description: Unfollowed comment successfully 72 | * 400: 73 | * description: The request was invalid. You may refer to response for details around why this happened. 74 | * content: 75 | * application/json: 76 | * schema: 77 | * properties: 78 | * error: 79 | * type: string 80 | * description: Type of error 81 | * 401: 82 | * description: User unauthorized to unfollow this comment 83 | * 404: 84 | * description: Comment not found 85 | * 500: 86 | * description: Server Error 87 | * security: 88 | * - bearerAuth: [] 89 | */ 90 | commentActionsRouter.post( 91 | "/unfollow-comment", 92 | verifyAuthToken, 93 | commentActionsController.followUnfollowValidator, 94 | validateRequestSchema, 95 | commentActionsController.unfollowComment 96 | ); 97 | 98 | export default commentActionsRouter; 99 | -------------------------------------------------------------------------------- /controllers/loginController.js: -------------------------------------------------------------------------------- 1 | import { body, param } from "express-validator"; 2 | import User from "../models/User.js"; 3 | import { hashPassword } from "../utils/passwordUtils.js"; 4 | import { sendUsernameEmail } from "../utils/sendEmails.js"; 5 | 6 | const loginValidator = [ 7 | body("username") 8 | .not() 9 | .isEmpty() 10 | .withMessage("Username must not be empty") 11 | .trim() 12 | .escape(), 13 | body("password") 14 | .isLength({ min: 8 }) 15 | .withMessage("Password must be at least 8 chars long"), 16 | ]; 17 | 18 | const resetPasswordValidator = [ 19 | param("id").trim().not().isEmpty().withMessage("Id must not be empty"), 20 | param("token").trim().not().isEmpty().withMessage("Token must not be empty"), 21 | body("newPassword") 22 | .isLength({ min: 8 }) 23 | .withMessage("Password must be at least 8 chars long"), 24 | body("verifyPassword") 25 | .isLength({ min: 8 }) 26 | .withMessage("Password must be at least 8 chars long"), 27 | ]; 28 | 29 | const forgetPasswordValidator = [ 30 | body("email") 31 | .trim() 32 | .not() 33 | .isEmpty() 34 | .isEmail() 35 | .withMessage("Email must be a valid email"), 36 | body("username") 37 | .not() 38 | .isEmpty() 39 | .withMessage("Username must not be empty") 40 | .trim() 41 | .escape(), 42 | ]; 43 | 44 | const forgetUsernameValidator = [ 45 | body("email") 46 | .trim() 47 | .not() 48 | .isEmpty() 49 | .isEmail() 50 | .withMessage("Email must be a valid email"), 51 | ]; 52 | 53 | const login = async (req, res) => { 54 | return res.status(200).json({ 55 | username: req.user.username, 56 | token: req.token, 57 | }); 58 | }; 59 | 60 | const forgetPassword = async (req, res) => { 61 | if (req.emailSent) { 62 | return res.status(200).json("Email has been sent"); 63 | } else { 64 | return res.status(400).json({ 65 | error: "An error occured while sending the email", 66 | }); 67 | } 68 | }; 69 | 70 | const resetPassword = async (req, res) => { 71 | const { newPassword, verifyPassword } = req.body; 72 | const user = req.user; 73 | const token = req.token; 74 | try { 75 | if (newPassword !== verifyPassword) { 76 | return res.status(400).json({ 77 | error: "Passwords do not match", 78 | }); 79 | } 80 | user.password = hashPassword(newPassword); 81 | await user.save(); 82 | await token.remove(); 83 | return res.status(200).json("Password updated successfully"); 84 | } catch (err) { 85 | res.status(500).json("Internal server error"); 86 | } 87 | }; 88 | 89 | const forgetUsername = async (req, res) => { 90 | try { 91 | const email = req.body.email; 92 | const user = await User.findOne({ email: email }); 93 | if (!user || user.deletedAt) { 94 | return res.status(400).json({ error: "No user with that email found" }); 95 | } 96 | const sentEmail = sendUsernameEmail(user); 97 | if (!sentEmail) { 98 | return res.status(400).json({ 99 | error: "Could not send the email", 100 | }); 101 | } 102 | res.status(200).json("Email has been sent"); 103 | } catch (error) { 104 | res.status(500).json("Internal server error"); 105 | } 106 | }; 107 | 108 | export default { 109 | loginValidator, 110 | resetPasswordValidator, 111 | forgetPasswordValidator, 112 | forgetUsernameValidator, 113 | login, 114 | forgetPassword, 115 | resetPassword, 116 | forgetUsername, 117 | }; 118 | -------------------------------------------------------------------------------- /test/middlewares/verifySubredditName.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { jest } from "@jest/globals"; 3 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 4 | import User from "../../models/User.js"; 5 | import Subreddit from "../../models/Community.js"; 6 | import Token from "../../models/VerifyToken.js"; 7 | import crypto from "crypto"; 8 | import { hashPassword } from "../../utils/passwordUtils.js"; 9 | import { checkDuplicateSubredditTitle } from "../../middleware/NverifySubredditName.js"; 10 | 11 | // eslint-disable-next-line max-statements 12 | describe("Testing verifySubredditName middleware", () => { 13 | let mockRequest; 14 | let mockResponse; 15 | let nextFunction; 16 | 17 | let user2 = {}; 18 | let subreddit1 = {}; 19 | let verifyToken = {}; 20 | beforeAll(async () => { 21 | await connectDatabase(); 22 | 23 | user2 = await new User({ 24 | username: "Hamdy", 25 | email: "Hamdy@gmail.com", 26 | createdAt: Date.now(), 27 | password: hashPassword("87654321"), 28 | }).save(); 29 | 30 | subreddit1 = await new Subreddit({ 31 | title: "SportsSubreddit", 32 | viewName: "Ahly", 33 | category: "Sports", 34 | type: "Private", 35 | dateOfCreation: Date.now(), 36 | owner: { 37 | username: "Noaman", 38 | }, 39 | members: 20, 40 | numberOfViews: 63, 41 | moderators: [ 42 | { 43 | userID: user2.id, 44 | dateOfModeration: Date.now(), 45 | }, 46 | ], 47 | }).save(); 48 | 49 | verifyToken = await new Token({ 50 | userId: user2._id, 51 | token: crypto.randomBytes(32).toString("hex"), 52 | type: "forgetPassword", 53 | }).save(); 54 | }); 55 | 56 | afterAll(async () => { 57 | await User.deleteMany({}); 58 | await Token.deleteMany({}); 59 | await Subreddit.deleteMany({}); 60 | await closeDatabaseConnection(); 61 | }); 62 | 63 | beforeEach(() => { 64 | nextFunction = jest.fn(); 65 | mockRequest = {}; 66 | mockResponse = { 67 | status: () => { 68 | jest.fn(); 69 | return mockResponse; 70 | }, 71 | json: () => { 72 | jest.fn(); 73 | return mockResponse; 74 | }, 75 | }; 76 | }); 77 | //-------------------------------------------------------------------- 78 | it("should have checkDuplicateSubredditTitle function", () => { 79 | expect(checkDuplicateSubredditTitle).toBeDefined(); 80 | }); 81 | //---------------------------------------------------------------------- 82 | it("Test checkDuplicateSubredditTitle without req.body", async () => { 83 | mockRequest = {}; 84 | await checkDuplicateSubredditTitle(mockRequest, mockResponse, nextFunction); 85 | expect(nextFunction).not.toHaveBeenCalled(); 86 | }); 87 | 88 | it("Test checkDuplicateSubredditTitle with a repeated subreddit name", async () => { 89 | mockRequest = { 90 | body: { 91 | subredditName: subreddit1.title, 92 | }, 93 | }; 94 | await checkDuplicateSubredditTitle(mockRequest, mockResponse, nextFunction); 95 | expect(nextFunction).not.toHaveBeenCalled(); 96 | }); 97 | it("Test checkDuplicateSubredditTitle with a moderator user", async () => { 98 | mockRequest = { 99 | body: { 100 | subredditName: "no3manYgd3an", 101 | }, 102 | }; 103 | await checkDuplicateSubredditTitle(mockRequest, mockResponse, nextFunction); 104 | expect(nextFunction).toHaveBeenCalled(); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import express from "express"; 3 | import multer from "multer"; 4 | import path from "path"; 5 | import fs from "fs"; 6 | import { fileURLToPath } from "url"; 7 | import mongoose from "mongoose"; 8 | import swaggerUI from "swagger-ui-express"; 9 | import swaggerJsDoc from "swagger-jsdoc"; 10 | import mainRouter from "./routes/routes.js"; 11 | import bodyParser from "body-parser"; 12 | import morgan from "morgan"; 13 | import { fileStorage, fileFilter } from "./utils/files.js"; 14 | import rateLimit from "express-rate-limit"; 15 | const app = express(); 16 | 17 | const REQUEST_LIMIT = parseInt(process.env.REQUEST_LIMIT) || 100; 18 | 19 | dotenv.config(); 20 | 21 | app.use(bodyParser.json({ limit: "200mb" })); 22 | const __filename = fileURLToPath(import.meta.url); 23 | 24 | const __dirname = path.dirname(__filename); 25 | app.use( 26 | multer({ storage: fileStorage, fileFilter: fileFilter }).fields([ 27 | { name: "images", maxCount: 100 }, 28 | { name: "video", maxCount: 1 }, 29 | { name: "avatar", maxCount: 1 }, 30 | { name: "banner", maxCount: 1 }, 31 | ]) 32 | ); 33 | 34 | // That's morgan for tracking the api in the terminal 35 | // Will be removed later 36 | app.use(morgan("dev")); 37 | 38 | const limiter = rateLimit({ 39 | windowMs: 15 * 60 * 1000, // 15 minutes 40 | max: REQUEST_LIMIT, // Limit each IP to REQUEST_LIMIT requests per `window` (here, per 15 minutes) 41 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 42 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 43 | }); 44 | 45 | // Apply the rate limiting middleware to all requests 46 | app.use(limiter); 47 | 48 | // Log stream for morgan to make the log file in the server 49 | const accessLogStream = fs.createWriteStream( 50 | path.join(__dirname, "logs/access.log"), 51 | { 52 | flags: "a", 53 | } 54 | ); 55 | 56 | app.use( 57 | morgan("combined", { 58 | stream: accessLogStream, 59 | }) 60 | ); 61 | 62 | app.use("/images", express.static(path.join(__dirname, "images"))); 63 | app.use("/videos", express.static(path.join(__dirname, "videos"))); 64 | 65 | const port = process.env.PORT || 3000; 66 | 67 | app.use((_req, res, next) => { 68 | res.setHeader("Access-Control-Allow-Origin", "*"); 69 | res.setHeader( 70 | "Access-Control-Allow-Methods", 71 | "GET,POST,PUT,DELETE,PATCH,OPTIONS" 72 | ); 73 | res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization"); 74 | next(); 75 | }); 76 | 77 | // app.use(cors()); 78 | 79 | let DB_URL; 80 | // eslint-disable-next-line max-len 81 | if (process.env.NODE_ENV.trim() === "testing") { 82 | DB_URL = process.env.MONGO_URL_TESTING.trim(); 83 | } else { 84 | DB_URL = process.env.MONGO_URL.trim(); 85 | } 86 | 87 | mongoose 88 | .connect(DB_URL, { useNewUrlParser: true }) 89 | .then(() => { 90 | console.log("connected to mongo nnew version"); 91 | }) 92 | .catch((error) => { 93 | console.log("unable to connect to mongoDB : ", error); 94 | }); 95 | 96 | // swagger options 97 | const options = { 98 | definition: { 99 | openapi: "3.0.0", 100 | info: { 101 | title: "Read-it", 102 | version: "1.0.0", 103 | description: "API-Documentation", 104 | }, 105 | }, 106 | apis: ["./routes/*.js"], 107 | }; 108 | const specs = swaggerJsDoc(options); 109 | app.use("/api-docs", swaggerUI.serve, swaggerUI.setup(specs)); 110 | 111 | app.use(mainRouter); 112 | app.listen(port, () => { 113 | console.log(`Started on port ${port}`); 114 | }); 115 | 116 | export default app; 117 | -------------------------------------------------------------------------------- /test/services/subredditModerationServices.test.js: -------------------------------------------------------------------------------- 1 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 2 | import User from "./../../models/User.js"; 3 | import Subreddit from "./../../models/Community.js"; 4 | import { getTrafficService } from "../../services/subredditModerationServices"; 5 | 6 | // eslint-disable-next-line max-statements 7 | describe("Testing subreddit moderation actions services functions", () => { 8 | let owner = {}, 9 | normalUser1 = {}, 10 | normalUser2 = {}, 11 | normalUser3 = {}, 12 | normalUser4 = {}, 13 | subreddit = {}; 14 | // eslint-disable-next-line max-statements 15 | beforeAll(async () => { 16 | await connectDatabase(); 17 | 18 | owner = await new User({ 19 | username: "Beshoy", 20 | email: "beshoy@gmail.com", 21 | createdAt: Date.now(), 22 | }).save(); 23 | 24 | normalUser1 = await new User({ 25 | username: "Normal1", 26 | email: "normal1@gmail.com", 27 | createdAt: Date.now(), 28 | }).save(); 29 | 30 | normalUser2 = await new User({ 31 | username: "Normal2", 32 | email: "normal2@gmail.com", 33 | createdAt: Date.now(), 34 | }).save(); 35 | 36 | normalUser3 = await new User({ 37 | username: "Normal3", 38 | email: "normal3@gmail.com", 39 | createdAt: Date.now(), 40 | }).save(); 41 | 42 | normalUser4 = await new User({ 43 | username: "Normal4", 44 | email: "normal4@gmail.com", 45 | createdAt: Date.now(), 46 | }).save(); 47 | 48 | subreddit = await new Subreddit({ 49 | title: "Manga", 50 | viewName: "MangaReddit", 51 | category: "Art", 52 | type: "Public", 53 | nsfw: false, 54 | owner: { 55 | username: owner.username, 56 | userID: owner._id, 57 | }, 58 | moderators: [ 59 | { 60 | userID: owner._id, 61 | dateOfModeration: Date.now(), 62 | }, 63 | ], 64 | joinedUsers: [ 65 | { 66 | userId: normalUser1._id, 67 | joinDate: Date.now(), 68 | }, 69 | { 70 | userId: normalUser2._id, 71 | // 6 days ago 72 | joinDate: new Date().setDate(new Date().getDate() - 6), 73 | }, 74 | ], 75 | leftUsers: [ 76 | { 77 | userId: normalUser3._id, 78 | leaveDate: Date.now(), 79 | }, 80 | { 81 | userId: normalUser4._id, 82 | // 8 days ago 83 | leaveDate: new Date().setDate(new Date().getDate() - 8), 84 | }, 85 | ], 86 | dateOfCreation: Date.now(), 87 | }).save(); 88 | }); 89 | afterAll(async () => { 90 | await User.deleteMany({}); 91 | await Subreddit.deleteMany({}); 92 | await closeDatabaseConnection(); 93 | }); 94 | 95 | it("should have getTrafficService function", () => { 96 | expect(getTrafficService).toBeDefined(); 97 | }); 98 | // eslint-disable-next-line max-len 99 | it("try to let normal user request traffic stats of a subreddit", async () => { 100 | try { 101 | await getTrafficService(normalUser1, subreddit); 102 | } catch (error) { 103 | expect(error).toBeDefined(); 104 | } 105 | }); 106 | it("try to let the owner request traffic stats of a subreddit", async () => { 107 | const result = await getTrafficService(owner, subreddit); 108 | expect(result.data.numberOfJoinedLastDay).toEqual(1); 109 | expect(result.data.numberOfJoinedLastWeek).toEqual(2); 110 | expect(result.data.numberOfJoinedLastMonth).toEqual(2); 111 | expect(result.data.numberOfLeftLastDay).toEqual(1); 112 | expect(result.data.numberOfLeftLastWeek).toEqual(1); 113 | expect(result.data.numberOfLeftLastMonth).toEqual(2); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /routes/itemsActions.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import itemsActionController from "../controllers/BitemsActionsController.js"; 3 | import { validateRequestSchema } from "../middleware/validationResult.js"; 4 | import { verifyAuthToken } from "../middleware/verifyToken.js"; 5 | import { checkId } from "./../middleware/checkId.js"; 6 | 7 | // eslint-disable-next-line new-cap 8 | const itemsActionsRouter = express.Router(); 9 | 10 | /** 11 | * @swagger 12 | * /delete: 13 | * delete: 14 | * summary: Delete a Post, Comment or Message 15 | * tags: [Post-comment-message actions] 16 | * requestBody: 17 | * required: true 18 | * content: 19 | * application/json: 20 | * schema: 21 | * type: object 22 | * properties: 23 | * id: 24 | * type: string 25 | * description: id of a thing created by the user 26 | * type: 27 | * type: string 28 | * enum: 29 | * - post 30 | * - comment 31 | * - message 32 | * responses: 33 | * 200: 34 | * description: Successfully deleted 35 | * 400: 36 | * description: The request was invalid. You may refer to response for details around why this happened. 37 | * content: 38 | * application/json: 39 | * schema: 40 | * properties: 41 | * error: 42 | * type: string 43 | * description: Type of error 44 | * 401: 45 | * description: Unauthorized to delete this thing 46 | * 404: 47 | * description: Item already deleted (Not Found) 48 | * 500: 49 | * description: Server Error 50 | * security: 51 | * - bearerAuth: [] 52 | */ 53 | itemsActionsRouter.delete( 54 | "/delete", 55 | verifyAuthToken, 56 | itemsActionController.deleteValidator, 57 | validateRequestSchema, 58 | itemsActionController.deletePoComMes 59 | ); 60 | 61 | /** 62 | * @swagger 63 | * /edit-user-text: 64 | * put: 65 | * summary: Edit the body text of a comment 66 | * tags: [Post-comment actions] 67 | * requestBody: 68 | * required: true 69 | * content: 70 | * application/json: 71 | * schema: 72 | * type: object 73 | * properties: 74 | * content: 75 | * type: Object 76 | * description: New content entered 77 | * id: 78 | * type: string 79 | * description: id of the comment being edited 80 | * responses: 81 | * 200: 82 | * description: Comment edited successfully 83 | * 400: 84 | * description: The request was invalid. You may refer to response for details around why this happened. 85 | * content: 86 | * application/json: 87 | * schema: 88 | * properties: 89 | * error: 90 | * type: string 91 | * description: Type of error 92 | * 401: 93 | * description: Unauthorized to edit this comment 94 | * 404: 95 | * description: Content requested for editing is unavailable 96 | * 500: 97 | * description: Server Error 98 | * security: 99 | * - bearerAuth: [] 100 | */ 101 | itemsActionsRouter.put( 102 | "/edit-user-text", 103 | verifyAuthToken, 104 | itemsActionController.editComValidator, 105 | validateRequestSchema, 106 | checkId, 107 | itemsActionController.editComment 108 | ); 109 | 110 | export default itemsActionsRouter; 111 | -------------------------------------------------------------------------------- /models/Post.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | // eslint-disable-next-line new-cap 4 | const postSchema = mongoose.Schema({ 5 | title: { 6 | type: String, 7 | required: true, 8 | }, 9 | ownerUsername: { 10 | type: String, 11 | required: true, 12 | }, 13 | ownerId: { 14 | type: Schema.Types.ObjectId, 15 | required: true, 16 | ref: "User", 17 | }, 18 | subredditId: { 19 | type: Schema.Types.ObjectId, 20 | ref: "Subreddit", 21 | }, 22 | subredditName: { 23 | type: String, 24 | }, 25 | kind: { 26 | type: String, 27 | enum: ["hybrid", "image", "video", "post", "link"], 28 | default: "hybrid", 29 | required: true, 30 | }, 31 | content: { 32 | type: Object, 33 | }, 34 | images: [ 35 | { 36 | path: { 37 | type: String, 38 | required: true, 39 | }, 40 | caption: { 41 | type: String, 42 | }, 43 | link: { 44 | type: String, 45 | }, 46 | }, 47 | ], 48 | video: { 49 | type: String, 50 | }, 51 | link: { 52 | type: String, 53 | }, 54 | sharePostId: { 55 | type: String, 56 | }, 57 | suggestedSort: { 58 | type: String, 59 | default: "new", 60 | }, 61 | nsfw: { 62 | type: Boolean, 63 | default: false, 64 | }, 65 | spoiler: { 66 | type: Boolean, 67 | default: false, 68 | }, 69 | markedSpam: { 70 | type: Boolean, 71 | default: false, 72 | }, 73 | sendReplies: { 74 | type: Boolean, 75 | default: true, 76 | }, 77 | createdAt: { 78 | type: Date, 79 | required: true, 80 | }, 81 | editedAt: { 82 | type: Date, 83 | }, 84 | deletedAt: { 85 | type: Date, 86 | }, 87 | flair: { 88 | type: Schema.Types.ObjectId, 89 | ref: "Flair", 90 | }, 91 | numberOfUpvotes: { 92 | type: Number, 93 | default: 0, 94 | }, 95 | numberOfDownvotes: { 96 | type: Number, 97 | default: 0, 98 | }, 99 | numberOfComments: { 100 | type: Number, 101 | default: 0, 102 | }, 103 | insights: { 104 | totalViews: { 105 | type: Number, 106 | default: 0, 107 | }, 108 | upvoteRate: { 109 | type: Number, 110 | default: 0, 111 | }, 112 | communityKarma: { 113 | type: Number, 114 | default: 0, 115 | }, 116 | totalShares: { 117 | type: Number, 118 | default: 0, 119 | }, 120 | }, 121 | moderation: { 122 | approve: { 123 | approvedBy: { 124 | type: String, 125 | }, 126 | approvedDate: { 127 | type: Date, 128 | }, 129 | }, 130 | remove: { 131 | removedBy: { 132 | type: String, 133 | }, 134 | removedDate: { 135 | type: Date, 136 | }, 137 | }, 138 | spam: { 139 | spammedBy: { 140 | type: String, 141 | }, 142 | spammedDate: { 143 | type: Date, 144 | }, 145 | }, 146 | lock: { 147 | type: Boolean, 148 | default: false, 149 | }, 150 | }, 151 | usersCommented: [ 152 | { 153 | type: Schema.Types.ObjectId, 154 | ref: "User", 155 | }, 156 | ], 157 | followingUsers: [ 158 | { 159 | username: { 160 | type: String, 161 | required: true, 162 | }, 163 | userId: { 164 | type: Schema.Types.ObjectId, 165 | ref: "User", 166 | }, 167 | }, 168 | ], 169 | scheduleDate: { 170 | type: Date, 171 | }, 172 | scheduleTime: { 173 | type: Date, 174 | }, 175 | scheduleTimeZone: { 176 | type: String, 177 | }, 178 | hotScore: { 179 | type: Number, 180 | default: 0, 181 | }, 182 | hotTimingScore: { 183 | type: Number, 184 | default: 0, 185 | }, 186 | bestScore: { 187 | type: Number, 188 | default: 0, 189 | }, 190 | bestTimingScore: { 191 | type: Number, 192 | default: 0, 193 | }, 194 | numberOfVotes: { 195 | type: Number, 196 | default: 0, 197 | }, 198 | numberOfViews: { 199 | type: Number, 200 | default: 0, 201 | }, 202 | }); 203 | 204 | const Post = mongoose.model("Post", postSchema); 205 | 206 | export default Post; 207 | -------------------------------------------------------------------------------- /controllers/notificationController.js: -------------------------------------------------------------------------------- 1 | import { body } from "express-validator"; 2 | import { 3 | subscribeNotification, 4 | unsubscribeNotification, 5 | markAllNotificationsRead, 6 | markNotificationRead, 7 | markNotificationHidden, 8 | getUserNotifications, 9 | } from "../services/notificationServices.js"; 10 | const notificationSubscribeValidator = [ 11 | body("accessToken") 12 | .trim() 13 | .not() 14 | .isEmpty() 15 | .withMessage("accessToken is required"), 16 | body("type") 17 | .trim() 18 | .not() 19 | .isEmpty() 20 | .withMessage("type is required") 21 | .isIn(["web", "flutter"]) 22 | .withMessage("Invalid type"), 23 | ]; 24 | const notificationUnsubscribeValidator = [ 25 | body("type") 26 | .trim() 27 | .not() 28 | .isEmpty() 29 | .withMessage("type is required") 30 | .isIn(["web", "flutter"]) 31 | .withMessage("Invalid type"), 32 | ]; 33 | 34 | const notificationSubscribe = async (req, res) => { 35 | try { 36 | await subscribeNotification( 37 | req.payload.userId, 38 | req.body.type, 39 | req.body.accessToken 40 | ); 41 | res.status(200).json("Subscribed successfully"); 42 | } catch (err) { 43 | console.log(err.message); 44 | if (err.statusCode) { 45 | res.status(err.statusCode).json({ error: err.message }); 46 | } else { 47 | res.status(500).json("Internal Server Error"); 48 | } 49 | } 50 | }; 51 | 52 | const notificationUnsubscribe = async (req, res) => { 53 | try { 54 | await unsubscribeNotification(req.payload.userId, req.body.type); 55 | res.status(200).json("Unsubscribed successfully"); 56 | } catch (err) { 57 | console.log(err.message); 58 | if (err.statusCode) { 59 | res.status(err.statusCode).json({ error: err.message }); 60 | } else { 61 | res.status(500).json("Internal Server Error"); 62 | } 63 | } 64 | }; 65 | 66 | const markNotificationsAsRead = async (req, res) => { 67 | try { 68 | await markAllNotificationsRead(req.payload.userId); 69 | res.status(200).json("Notifications marked as read successfully"); 70 | } catch (err) { 71 | console.log(err.message); 72 | if (err.statusCode) { 73 | res.status(err.statusCode).json({ error: err.message }); 74 | } else { 75 | res.status(500).json("Internal Server Error"); 76 | } 77 | } 78 | }; 79 | 80 | const markNotificationAsRead = async (req, res) => { 81 | try { 82 | await markNotificationRead(req.payload.userId, req.params.notificationId); 83 | res.status(200).json("Notification marked as read successfully"); 84 | } catch (err) { 85 | console.log(err.message); 86 | if (err.statusCode) { 87 | res.status(err.statusCode).json({ error: err.message }); 88 | } else { 89 | res.status(500).json("Internal Server Error"); 90 | } 91 | } 92 | }; 93 | 94 | const markNotificationAsHidden = async (req, res) => { 95 | try { 96 | await markNotificationHidden(req.payload.userId, req.params.notificationId); 97 | res.status(200).json("Notification marked as read successfully"); 98 | } catch (err) { 99 | console.log(err.message); 100 | if (err.statusCode) { 101 | res.status(err.statusCode).json({ error: err.message }); 102 | } else { 103 | res.status(500).json("Internal Server Error"); 104 | } 105 | } 106 | }; 107 | 108 | const getAllNotifications = async (req, res) => { 109 | try { 110 | const preapredResponse = await getUserNotifications( 111 | req.query.limit, 112 | req.query.before, 113 | req.query.after, 114 | req.payload.userId 115 | ); 116 | res.status(200).json(preapredResponse); 117 | } catch (err) { 118 | console.log(err.message); 119 | if (err.statusCode) { 120 | res.status(err.statusCode).json({ error: err.message }); 121 | } else { 122 | res.status(500).json("Internal Server Error"); 123 | } 124 | } 125 | }; 126 | 127 | export default { 128 | notificationSubscribe, 129 | notificationSubscribeValidator, 130 | notificationUnsubscribe, 131 | notificationUnsubscribeValidator, 132 | markNotificationsAsRead, 133 | markNotificationAsRead, 134 | markNotificationAsHidden, 135 | getAllNotifications, 136 | }; 137 | -------------------------------------------------------------------------------- /test/middlewares/subredditRules.test.js: -------------------------------------------------------------------------------- 1 | import Subreddit from "../../models/Community"; 2 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 3 | import User from "../../models/User.js"; 4 | import subredditRulesMiddleware from "../../middleware/subredditRules.js"; 5 | import { jest } from "@jest/globals"; 6 | 7 | describe("subredditRulesMiddleware", () => { 8 | beforeAll(async () => { 9 | await connectDatabase(); 10 | }); 11 | afterAll(async () => { 12 | await User.deleteMany({}); 13 | await Subreddit.deleteMany({}); 14 | closeDatabaseConnection(); 15 | }); 16 | describe("validateRuleId", () => { 17 | it("Invalid ruleid", () => { 18 | const mockResponse = { 19 | status: () => { 20 | jest.fn(); 21 | return mockResponse; 22 | }, 23 | json: () => { 24 | jest.fn(); 25 | return mockResponse; 26 | }, 27 | }; 28 | const nextFunction = jest.fn(); 29 | const req = { 30 | params: { 31 | ruleId: "2", 32 | }, 33 | }; 34 | subredditRulesMiddleware.validateRuleId(req, mockResponse, nextFunction); 35 | expect(nextFunction).not.toHaveBeenCalled(); 36 | }); 37 | it("Valid ruleid", () => { 38 | const mockResponse = { 39 | status: () => { 40 | jest.fn(); 41 | return mockResponse; 42 | }, 43 | json: () => { 44 | jest.fn(); 45 | return mockResponse; 46 | }, 47 | }; 48 | const nextFunction2 = jest.fn(); 49 | const req = { 50 | params: { 51 | ruleId: "551137c2f9e1fac808a5f572", 52 | }, 53 | }; 54 | subredditRulesMiddleware.validateRuleId(req, mockResponse, nextFunction2); 55 | expect(nextFunction2).toHaveBeenCalled(); 56 | }); 57 | }); 58 | describe("checkRule", () => { 59 | it("existing rule", async () => { 60 | const mockResponse = { 61 | status: () => { 62 | jest.fn(); 63 | return mockResponse; 64 | }, 65 | json: () => { 66 | jest.fn(); 67 | return mockResponse; 68 | }, 69 | }; 70 | 71 | const subredditObject = await new Subreddit({ 72 | title: "title", 73 | viewName: "title", 74 | category: "Sports", 75 | type: "Public", 76 | owner: { 77 | username: "zeyad", 78 | }, 79 | rules: [ 80 | { 81 | ruleTitle: "Test rule", 82 | ruleOrder: 0, 83 | createdAt: Date.now(), 84 | appliesTo: "Posts", 85 | }, 86 | ], 87 | }).save(); 88 | 89 | const nextFunction3 = jest.fn(); 90 | const req = { 91 | params: { 92 | ruleId: subredditObject.rules[0]._id.toString(), 93 | }, 94 | subreddit: subredditObject, 95 | }; 96 | subredditRulesMiddleware.checkRule(req, mockResponse, nextFunction3); 97 | expect(nextFunction3).toHaveBeenCalled(); 98 | }); 99 | it("Deleted rule", async () => { 100 | const mockResponse = { 101 | status: () => { 102 | jest.fn(); 103 | return mockResponse; 104 | }, 105 | json: () => { 106 | jest.fn(); 107 | return mockResponse; 108 | }, 109 | }; 110 | const subredditObject = await new Subreddit({ 111 | title: "title", 112 | viewName: "title", 113 | category: "Sports", 114 | type: "Public", 115 | owner: { 116 | username: "zeyad", 117 | }, 118 | rules: [ 119 | { 120 | ruleTitle: "Test rule", 121 | ruleOrder: 0, 122 | createdAt: Date.now(), 123 | deletedAt: Date.now(), 124 | appliesTo: "Posts", 125 | }, 126 | ], 127 | }).save(); 128 | const nextFunction4 = jest.fn(); 129 | const req = { 130 | params: { 131 | ruleId: subredditObject.rules[0]._id.toString(), 132 | }, 133 | subreddit: subredditObject, 134 | }; 135 | subredditRulesMiddleware.checkRule(req, mockResponse, nextFunction4); 136 | expect(nextFunction4).not.toHaveBeenCalled(); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /middleware/postModeration.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-depth */ 2 | import Post from "../models/Post.js"; 3 | import Comment from "../models/Comment.js"; 4 | import Subreddit from "../models/Community.js"; 5 | 6 | /** 7 | * Middleware used to check if a thing (post/comment) and their subreddit 8 | * are not found (404). If they are, we check for the user requesting to 9 | * change mod settings of this thing if he is a moderator in the 10 | * subreddit containing this thing or not with the appropriate permissions 11 | * for managing posts and comments in this subreddit (401) 12 | * 13 | * @param {Object} req Request object 14 | * @param {Object} res Response object 15 | * @param {function} next Next function 16 | * @returns {void} 17 | */ 18 | 19 | // eslint-disable-next-line max-statements 20 | export async function checkThingMod(req, res, next) { 21 | const id = req.body.id; 22 | const type = req.body.type; 23 | const username = req.payload.username; 24 | const userId = req.payload.userId; 25 | if (type === "post") { 26 | try { 27 | const post = await Post.findById(id); 28 | 29 | if (!post || post.deletedAt) { 30 | return res.status(404).json("Post not found"); 31 | } 32 | if (post.subredditName) { 33 | const subreddit = await Subreddit.findOne({ 34 | title: post.subredditName, 35 | }); 36 | 37 | if (!subreddit || subreddit.deletedAt) { 38 | return res.status(404).json("Subreddit not found"); 39 | } 40 | 41 | const moderatorIndex = subreddit.moderators.findIndex( 42 | (mod) => mod.userID.toString() === userId.toString() 43 | ); 44 | if (moderatorIndex === -1) { 45 | return res.status(401).json({ 46 | error: "Unauthorized Access", 47 | }); 48 | } else { 49 | const permessionIndex = subreddit.moderators[ 50 | moderatorIndex 51 | ].permissions.findIndex( 52 | (permission) => 53 | permission === "Everything" || 54 | permission === "Manage Posts & Comments" 55 | ); 56 | if (permessionIndex === -1) { 57 | return res.status(401).json({ 58 | error: "No permission", 59 | }); 60 | } 61 | } 62 | } else { 63 | if (post.ownerUsername !== username) { 64 | return res.status(401).json("Post doesn't belong to this user"); 65 | } 66 | } 67 | req.post = post; 68 | req.type = type; 69 | next(); 70 | } catch (err) { 71 | res.status(500).json("Internal server error"); 72 | } 73 | } else if (type === "comment") { 74 | try { 75 | const comment = await Comment.findById(id); 76 | 77 | if (!comment || comment.deletedAt) { 78 | return res.status(404).json("Comment not found"); 79 | } 80 | if (comment.subredditName) { 81 | const subreddit = await Subreddit.findOne({ 82 | title: comment.subredditName, 83 | }); 84 | 85 | if (!subreddit || subreddit.deletedAt) { 86 | return res.status(404).json("Subreddit not found"); 87 | } 88 | const moderatorIndex = subreddit.moderators.findIndex( 89 | (mod) => mod.userID.toString() === userId.toString() 90 | ); 91 | if (moderatorIndex === -1) { 92 | return res.status(401).json({ 93 | error: "Unauthorized Access", 94 | }); 95 | } else { 96 | const permessionIndex = subreddit.moderators[ 97 | moderatorIndex 98 | ].permissions.findIndex( 99 | (permission) => 100 | permission === "Everything" || 101 | permission === "Manage Posts & Comments" 102 | ); 103 | if (permessionIndex === -1) { 104 | return res.status(401).json({ 105 | error: "No permission", 106 | }); 107 | } 108 | } 109 | } else { 110 | if (comment.ownerUsername !== username) { 111 | return res.status(401).json("Comment doesn't belong to this user"); 112 | } 113 | } 114 | req.comment = comment; 115 | req.type = type; 116 | next(); 117 | } catch (err) { 118 | return res.status(500).json("Internal server error"); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /services/subredditSettings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A Service function used to prepare the subreddit settings 3 | * @param {Object} subreddit the subreddit object 4 | * @returns {settings} The prepared object 5 | */ 6 | export function prepareSubredditSettings(subreddit) { 7 | const settings = { 8 | communityName: subreddit.viewName, 9 | communityDescription: subreddit.description, 10 | sendWelcomeMessage: subreddit.subredditSettings.sendWelcomeMessage, 11 | language: subreddit.subredditSettings.language, 12 | type: subreddit.type, 13 | region: subreddit.subredditSettings.region, 14 | NSFW: subreddit.nsfw, 15 | mainTopic: subreddit.mainTopic, 16 | subTopics: subreddit.subTopics, 17 | welcomeMessage: subreddit.subredditSettings.welcomeMessage, 18 | }; 19 | 20 | if (subreddit.type === "Private") { 21 | settings.acceptingRequestsToJoin = 22 | subreddit.subredditSettings.acceptingRequestsToJoin; 23 | } else if (subreddit.type === "Restricted") { 24 | settings.acceptingRequestsToPost = 25 | subreddit.subredditSettings.acceptingRequestsToPost; 26 | settings.approvedUsersHaveTheAbilityTo = 27 | subreddit.subredditSettings.approvedUsersHaveTheAbilityTo; 28 | } 29 | 30 | return settings; 31 | } 32 | 33 | /** 34 | * A function used to validate the settings of the subreddit before updating the subreddit settings and throws an error if the settings is invalid 35 | * @param {Object} settings the subreddit settings object 36 | * @returns {void} 37 | */ 38 | export function validateSubredditSettings(settings) { 39 | if ( 40 | settings.Type === "Private" && 41 | !settings.hasOwnProperty("acceptingRequestsToJoin") 42 | ) { 43 | const error = new Error("acceptingRequestsToJoin is required"); 44 | error.statusCode = 400; 45 | throw error; 46 | } 47 | if ( 48 | settings.Type === "Restricted" && 49 | (!settings.hasOwnProperty("acceptingRequestsToPost") || 50 | !settings.hasOwnProperty("approvedUsersHaveTheAbilityTo")) 51 | ) { 52 | const error = new Error( 53 | "acceptingRequestsToPost and approvedUsersHaveTheAbilityTo is required" 54 | ); 55 | error.statusCode = 400; 56 | throw error; 57 | } 58 | 59 | if ( 60 | (settings.sendWelcomeMessage === true || 61 | settings.sendWelcomeMessage === "true") && 62 | !settings.welcomeMessage 63 | ) { 64 | const error = new Error("welcomeMessage is required"); 65 | error.statusCode = 400; 66 | throw error; 67 | } 68 | } 69 | 70 | /** 71 | * A Service function used to update the subreddit settings 72 | * @param {Object} subreddit the subreddit object 73 | * @param {Object} settings the subreddit settings object 74 | * @returns {void} 75 | */ 76 | // eslint-disable-next-line max-statements 77 | export async function updateSubredditSettings(subreddit, settings) { 78 | if (!settings.hasOwnProperty("communityDescription")) { 79 | const error = new Error("communityDescription is required"); 80 | error.statusCode = 400; 81 | throw error; 82 | } 83 | validateSubredditSettings(settings); 84 | const subTopics = [...new Set(settings.subTopics)]; 85 | subreddit.viewName = settings.communityName; 86 | subreddit.mainTopic = settings.mainTopic; 87 | subreddit.subTopics = subTopics; 88 | subreddit.description = settings.communityDescription; 89 | subreddit.nsfw = settings.NSFW; 90 | subreddit.type = settings.Type; 91 | subreddit.subredditSettings.sendWelcomeMessage = settings.sendWelcomeMessage; 92 | subreddit.subredditSettings.language = settings.language; 93 | if ( 94 | settings.sendWelcomeMessage === true || 95 | settings.sendWelcomeMessage === "true" 96 | ) { 97 | subreddit.subredditSettings.welcomeMessage = settings.welcomeMessage; 98 | } 99 | if (settings.Region) { 100 | subreddit.subredditSettings.region = settings.Region; 101 | } 102 | if (settings.Type === "Private") { 103 | subreddit.subredditSettings.acceptingRequestsToJoin = 104 | settings.acceptingRequestsToJoin; 105 | } 106 | if (settings.Type === "Restricted") { 107 | subreddit.subredditSettings.acceptingRequestsToPost = 108 | settings.acceptingRequestsToPost; 109 | subreddit.subredditSettings.approvedUsersHaveTheAbilityTo = 110 | settings.approvedUsersHaveTheAbilityTo; 111 | } 112 | // console.log(subreddit.subredditSettings); 113 | await subreddit.save(); 114 | } 115 | -------------------------------------------------------------------------------- /test/services/subredditRules.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | checkEditRulesOrderService, 3 | checkDublicateRuleOrderService, 4 | editRulesOrderService, 5 | } from "../../services/subredditRules.js"; 6 | 7 | import { jest } from "@jest/globals"; 8 | 9 | describe("Testing Subreddit rules services", () => { 10 | describe("Testing checkEditRulesOrderService all cases", () => { 11 | it("Testing rules number doesn't match", () => { 12 | expect(() => { 13 | const req = { 14 | subreddit: { 15 | numberOfRules: 2, 16 | }, 17 | body: { 18 | rulesOrder: [2], 19 | }, 20 | }; 21 | checkEditRulesOrderService(req); 22 | }).toThrow("Number of rules doesn't match"); 23 | }); 24 | it("Testing rules number match", () => { 25 | expect(() => { 26 | const req = { 27 | subreddit: { 28 | numberOfRules: 2, 29 | }, 30 | body: { 31 | rulesOrder: [2, 1], 32 | }, 33 | }; 34 | checkEditRulesOrderService(req); 35 | }).not.toThrow(); 36 | }); 37 | }); 38 | 39 | describe("Testing checkDublicateRuleOrderService all cases", () => { 40 | it("Testing dublicate rule id", () => { 41 | expect(() => { 42 | const req = { 43 | body: { 44 | rulesOrder: [ 45 | { 46 | ruleId: 2, 47 | ruleOrder: 0, 48 | }, 49 | { 50 | ruleId: 2, 51 | ruleOrder: 1, 52 | }, 53 | ], 54 | }, 55 | }; 56 | checkDublicateRuleOrderService(req); 57 | }).toThrow("dublicate rule id"); 58 | }); 59 | it("Testing dublicate rule order", () => { 60 | expect(() => { 61 | const req = { 62 | body: { 63 | rulesOrder: [ 64 | { 65 | ruleId: 2, 66 | ruleOrder: 1, 67 | }, 68 | { 69 | ruleId: 1, 70 | ruleOrder: 1, 71 | }, 72 | ], 73 | }, 74 | }; 75 | checkDublicateRuleOrderService(req); 76 | }).toThrow("dublicate rule order"); 77 | }); 78 | }); 79 | 80 | describe("Testing editRulesOrderService all cases", () => { 81 | it("Testing valid rules order", () => { 82 | const saveFunction = jest.fn(); 83 | 84 | const req = { 85 | body: { 86 | rulesOrder: [ 87 | { 88 | ruleId: 2, 89 | ruleOrder: 1, 90 | }, 91 | { 92 | ruleId: 1, 93 | ruleOrder: 2, 94 | }, 95 | ], 96 | }, 97 | subreddit: { 98 | rules: [ 99 | { 100 | _id: 1, 101 | ruleOrder: 1, 102 | }, 103 | { 104 | _id: 2, 105 | ruleOrder: 2, 106 | }, 107 | ], 108 | save: saveFunction, 109 | }, 110 | }; 111 | editRulesOrderService(req); 112 | expect(saveFunction).toHaveBeenCalled(); 113 | }); 114 | it("Testing deleted rules order", async () => { 115 | const saveFunction = jest.fn(); 116 | const req = { 117 | body: { 118 | rulesOrder: [ 119 | { 120 | ruleId: "2", 121 | ruleOrder: 2, 122 | }, 123 | { 124 | ruleId: "1", 125 | ruleOrder: 0, 126 | }, 127 | { 128 | ruleId: "0", 129 | ruleOrder: 1, 130 | }, 131 | ], 132 | }, 133 | subreddit: { 134 | rules: [ 135 | { 136 | _id: 2, 137 | ruleOrder: 2, 138 | deletedAt: false, 139 | }, 140 | { 141 | _id: 0, 142 | ruleOrder: 0, 143 | deletedAt: true, 144 | }, 145 | { 146 | _id: 1, 147 | ruleOrder: 1, 148 | deletedAt: false, 149 | }, 150 | ], 151 | save: saveFunction, 152 | }, 153 | }; 154 | await expect(editRulesOrderService(req)).rejects.toThrow( 155 | "Rule not found" 156 | ); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/middlewares/verifyModerator.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import { jest } from "@jest/globals"; 3 | import { connectDatabase, closeDatabaseConnection } from "../database.js"; 4 | import { checkJoinedBefore } from "../../middleware/NJoiningValidation.js"; 5 | import { optionalToken } from "../../middleware/optionalToken.js"; 6 | import { generateJWT } from "../../utils/generateTokens.js"; 7 | import User from "../../models/User.js"; 8 | import Subreddit from "../../models/Community.js"; 9 | import Token from "../../models/VerifyToken.js"; 10 | import crypto from "crypto"; 11 | import { hashPassword } from "../../utils/passwordUtils.js"; 12 | import { checkModerator } from "../../middleware/NverifyModerator.js"; 13 | 14 | // eslint-disable-next-line max-statements 15 | describe("Testing verify moderator middleware", () => { 16 | let mockRequest; 17 | let mockResponse; 18 | let nextFunction; 19 | 20 | let user1 = {}; 21 | let user2 = {}; 22 | let user3 = {}; 23 | let subreddit1 = {}; 24 | let subreddit2 = {}; 25 | let verifyToken = {}; 26 | beforeAll(async () => { 27 | await connectDatabase(); 28 | 29 | user2 = await new User({ 30 | username: "Hamdy", 31 | email: "Hamdy@gmail.com", 32 | createdAt: Date.now(), 33 | password: hashPassword("87654321"), 34 | }).save(); 35 | 36 | user3 = await new User({ 37 | username: "Beshoy", 38 | email: "Beshoy@gmail.com", 39 | createdAt: Date.now(), 40 | password: hashPassword("87654321"), 41 | }).save(); 42 | 43 | subreddit1 = await new Subreddit({ 44 | title: "SportsSubreddit", 45 | viewName: "Ahly", 46 | category: "Sports", 47 | type: "Private", 48 | dateOfCreation: Date.now(), 49 | owner: { 50 | username: "Noaman", 51 | }, 52 | members: 20, 53 | numberOfViews: 63, 54 | moderators: [ 55 | { 56 | userID: user2.id, 57 | dateOfModeration: Date.now(), 58 | }, 59 | ], 60 | }).save(); 61 | 62 | user1 = await new User({ 63 | username: "Noaman", 64 | email: "abdelrahman@gmail.com", 65 | createdAt: Date.now(), 66 | password: hashPassword("12345678"), 67 | joinedSubreddits: [ 68 | { 69 | subredditId: subreddit1._id, 70 | name: subreddit1.title, 71 | }, 72 | ], 73 | }).save(); 74 | 75 | verifyToken = await new Token({ 76 | userId: user2._id, 77 | token: crypto.randomBytes(32).toString("hex"), 78 | type: "forgetPassword", 79 | }).save(); 80 | }); 81 | 82 | afterAll(async () => { 83 | await User.deleteMany({}); 84 | await Token.deleteMany({}); 85 | await Subreddit.deleteMany({}); 86 | await closeDatabaseConnection(); 87 | }); 88 | 89 | beforeEach(() => { 90 | nextFunction = jest.fn(); 91 | mockRequest = {}; 92 | mockResponse = { 93 | status: () => { 94 | jest.fn(); 95 | return mockResponse; 96 | }, 97 | json: () => { 98 | jest.fn(); 99 | return mockResponse; 100 | }, 101 | }; 102 | }); 103 | //-------------------------------------------------------------------- 104 | it("should have checkJoinedBefore function", () => { 105 | expect(checkModerator).toBeDefined(); 106 | }); 107 | //---------------------------------------------------------------------- 108 | it("Test checkModerator without req.params", async () => { 109 | mockRequest = { 110 | payload: { 111 | username: user1.username, 112 | userId: user1.id, 113 | }, 114 | }; 115 | await checkModerator(mockRequest, mockResponse, nextFunction); 116 | expect(nextFunction).not.toHaveBeenCalled(); 117 | }); 118 | 119 | it("Test checkModerator with a not moderator user", async () => { 120 | mockRequest = { 121 | payload: { 122 | username: user1.username, 123 | userId: user1.id, 124 | }, 125 | params: { 126 | subreddit: subreddit1.title, 127 | }, 128 | }; 129 | await checkModerator(mockRequest, mockResponse, nextFunction); 130 | expect(nextFunction).not.toHaveBeenCalled(); 131 | }); 132 | it("Test checkModerator with a moderator user", async () => { 133 | mockRequest = { 134 | payload: { 135 | username: user2.username, 136 | userId: user2.id, 137 | }, 138 | params: { 139 | subreddit: subreddit1.title, 140 | }, 141 | }; 142 | await checkModerator(mockRequest, mockResponse, nextFunction); 143 | expect(nextFunction).toHaveBeenCalled(); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /utils/prepareSubreddit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | /* eslint-disable max-statements */ 3 | import { prepareLimit } from "./prepareLimit.js"; 4 | import Subreddit from "../models/Community.js"; 5 | import Post from "../models/Post.js"; 6 | import { searchForSubredditById } from "../services/communityServices.js"; 7 | import { searchForPost } from "../services/PostActions.js"; 8 | 9 | /** 10 | * This function is used to prepare the subreddit for being listed 11 | * 12 | * @param {string} category the category that we will filter with 13 | * @param {string} before the name of the first element that returned before 14 | * @param {string} after the name of the latest element that returned before 15 | * @param {string} limit The maximum value that they need to get 16 | * @param {Boolean} withCategory defines if we want to filter with category or not 17 | * @returns {Object} contains the results of the listing that we will use 18 | */ 19 | export async function subredditListing( 20 | category, 21 | before, 22 | after, 23 | limit, 24 | withCategory 25 | ) { 26 | const result = {}; 27 | result.sort = { members: -1, title: 1 }; 28 | result.query = {}; 29 | if (withCategory) { 30 | result.query = { category: category }; 31 | } 32 | result.limit = prepareLimit(limit); 33 | let splitterSubreddit; 34 | if (before) { 35 | splitterSubreddit = await searchForSubredditById(before); 36 | result.query.members = { $gt: splitterSubreddit.members }; 37 | } else if (!before && after) { 38 | splitterSubreddit = await searchForSubredditById(after); 39 | result.query.members = { $lt: splitterSubreddit.members }; 40 | } else if (!before && !after) { 41 | splitterSubreddit = await Subreddit.find(result.query) 42 | .limit(1) 43 | .sort(result.sort); 44 | result.query.members = { $lte: splitterSubreddit[0].members }; 45 | } 46 | return result; 47 | } 48 | 49 | export async function extraPostsListing(before, after, limit, type, time) { 50 | const result = {}; 51 | let splitterParam; 52 | if (type === "hot") { 53 | result.sort = { hotScore: -1 }; 54 | splitterParam = "hotScore"; 55 | } else if (type === "best") { 56 | result.sort = { bestScore: -1 }; 57 | splitterParam = "bestScore"; 58 | } else if (type === "new") { 59 | result.sort = { createdAt: -1 }; 60 | splitterParam = "createdAt"; 61 | } else if (type === "top") { 62 | result.sort = { numberOfVotes: -1 }; 63 | splitterParam = "numberOfVotes"; 64 | } else if (type === "trending") { 65 | result.sort = { numberOfViews: -1 }; 66 | splitterParam = "numberOfViews"; 67 | } 68 | result.query = {}; 69 | result.limit = prepareLimit(limit); 70 | let splitterPost; 71 | if (before) { 72 | splitterPost = await searchForPost(before); 73 | result.query[splitterParam] = { $gte: splitterPost[splitterParam] }; 74 | } else if (!before && after) { 75 | splitterPost = await searchForPost(after); 76 | result.query[splitterParam] = { $lte: splitterPost[splitterParam] }; 77 | } else if (!before && !after) { 78 | splitterPost = await Post.find(result.query).limit(1).sort(result.sort); 79 | console.log(splitterPost); 80 | if (splitterPost.length === 0) { 81 | let error = new Error("No Posts found"); 82 | error.statusCode = 404; 83 | throw error; 84 | } 85 | result.query[splitterParam] = { $lte: splitterPost[0][splitterParam] }; 86 | } 87 | if (type === "top") { 88 | let filteringDate = new Date(); 89 | let changed = false; 90 | if (time === "year") { 91 | filteringDate.setFullYear(filteringDate.getFullYear() - 1); 92 | changed = true; 93 | } else if (time === "month") { 94 | filteringDate.setFullYear( 95 | filteringDate.getFullYear(), 96 | filteringDate.getMonth() - 1 97 | ); 98 | changed = true; 99 | } else if (time === "week") { 100 | filteringDate.setFullYear( 101 | filteringDate.getFullYear(), 102 | filteringDate.getMonth(), 103 | filteringDate.getDate() - 7 104 | ); 105 | changed = true; 106 | } else if (time === "day") { 107 | filteringDate.setFullYear( 108 | filteringDate.getFullYear(), 109 | filteringDate.getMonth(), 110 | filteringDate.getDate() - 1 111 | ); 112 | changed = true; 113 | } else if (time === "hour") { 114 | filteringDate.setHours(filteringDate.getHours() - 1); 115 | changed = true; 116 | } 117 | if (changed) { 118 | result.query["createdAt"] = { $gte: filteringDate }; 119 | } 120 | } 121 | return result; 122 | } 123 | -------------------------------------------------------------------------------- /controllers/searchController.js: -------------------------------------------------------------------------------- 1 | import { param, query } from "express-validator"; 2 | import { 3 | getLoggedInUser, 4 | searchComments, 5 | searchPosts, 6 | searchSubreddits, 7 | searchUsers, 8 | } from "../services/search.js"; 9 | import { 10 | searchForComments, 11 | searchForPosts, 12 | } from "../services/searchInSubreddit.js"; 13 | 14 | const searchValidator = [ 15 | query("q").not().isEmpty().withMessage("Query must be given").trim().escape(), 16 | query("type") 17 | .optional() 18 | .isIn(["post", "comment", "user", "subreddit"]) 19 | .withMessage("Invalid value for type"), 20 | ]; 21 | 22 | const searchSubredditValidator = [ 23 | query("q").not().isEmpty().withMessage("Query must be given").trim().escape(), 24 | query("type") 25 | .optional() 26 | .isIn(["post", "comment", "user", "subreddit"]) 27 | .withMessage("Invalid value for type"), 28 | param("subreddit") 29 | .not() 30 | .isEmpty() 31 | .withMessage("Subreddit name should be given") 32 | .trim() 33 | .escape(), 34 | ]; 35 | 36 | // eslint-disable-next-line max-statements 37 | const search = async (req, res) => { 38 | let type = req.query.type; 39 | const query = req.query.q; 40 | const { after, before, limit, sort, time } = req.query; 41 | try { 42 | let loggedInUser = undefined; 43 | if (req.loggedIn) { 44 | const user = await getLoggedInUser(req.userId); 45 | if (user) { 46 | loggedInUser = user; 47 | } 48 | } 49 | let result; 50 | if (!type) { 51 | type = "post"; 52 | } 53 | if (type === "post") { 54 | result = await searchPosts( 55 | query, 56 | { 57 | after, 58 | before, 59 | limit, 60 | sort, 61 | time, 62 | }, 63 | loggedInUser 64 | ); 65 | } else if (type === "comment") { 66 | result = await searchComments( 67 | query, 68 | { 69 | after, 70 | before, 71 | limit, 72 | }, 73 | loggedInUser 74 | ); 75 | } else if (type === "user") { 76 | result = await searchUsers(query, { after, before, limit }, loggedInUser); 77 | } else { 78 | result = await searchSubreddits( 79 | query, 80 | { 81 | after, 82 | before, 83 | limit, 84 | }, 85 | loggedInUser 86 | ); 87 | } 88 | res.status(result.statusCode).json(result.data); 89 | } catch (error) { 90 | console.log(error.message); 91 | if (error.statusCode) { 92 | if (error.statusCode === 400) { 93 | res.status(error.statusCode).json({ error: error.message }); 94 | } else { 95 | res.status(error.statusCode).json(error.message); 96 | } 97 | } else { 98 | res.status(500).json("Internal server error"); 99 | } 100 | } 101 | }; 102 | 103 | // eslint-disable-next-line max-statements 104 | const searchSubreddit = async (req, res) => { 105 | const type = req.query.type; 106 | const subreddit = req.params.subreddit; 107 | const query = req.query.q; 108 | const { after, before, limit, sort, time } = req.query; 109 | try { 110 | let loggedInUser = undefined; 111 | if (req.loggedIn) { 112 | const user = await getLoggedInUser(req.userId); 113 | if (user) { 114 | loggedInUser = user; 115 | } 116 | } 117 | let result; 118 | if (!type) { 119 | type = "post"; 120 | } 121 | if (type === "post") { 122 | result = await searchForPosts( 123 | subreddit, 124 | query, 125 | { 126 | after, 127 | before, 128 | limit, 129 | sort, 130 | time, 131 | }, 132 | loggedInUser 133 | ); 134 | } else { 135 | result = await searchForComments( 136 | subreddit, 137 | query, 138 | { 139 | after, 140 | before, 141 | limit, 142 | }, 143 | loggedInUser 144 | ); 145 | } 146 | res.status(result.statusCode).json(result.data); 147 | } catch (error) { 148 | console.log(error.message); 149 | if (error.statusCode) { 150 | if (error.statusCode === 400) { 151 | res.status(error.statusCode).json({ error: error.message }); 152 | } else { 153 | res.status(error.statusCode).json(error.message); 154 | } 155 | } else { 156 | res.status(500).json("Internal server error"); 157 | } 158 | } 159 | }; 160 | 161 | export default { 162 | searchValidator, 163 | searchSubredditValidator, 164 | search, 165 | searchSubreddit, 166 | }; 167 | -------------------------------------------------------------------------------- /controllers/BcommentController.js: -------------------------------------------------------------------------------- 1 | import { body, param } from "express-validator"; 2 | import { 3 | commentTreeListingService, 4 | checkPostId, 5 | checkCommentId, 6 | checkloggedInUser, 7 | commentTreeOfCommentListingService, 8 | createCommentService, 9 | } from "../services/commentServices.js"; 10 | 11 | const createCommentValidator = [ 12 | body("content") 13 | .not() 14 | .isEmpty() 15 | .withMessage("Content of the comment can not be empty"), 16 | body("parentId").not().isEmpty().withMessage("Parent Id can not be empty"), 17 | body("postId").not().isEmpty().withMessage("Post Id can not be empty"), 18 | body("parentType") 19 | .not() 20 | .isEmpty() 21 | .withMessage("Parent Type can not be empty"), 22 | body("level").not().isEmpty().withMessage("Level can not be empty"), 23 | body("haveSubreddit") 24 | .not() 25 | .isEmpty() 26 | .withMessage("Have subreddit boolean can not be empty"), 27 | ]; 28 | 29 | const getCommentTreeValidator = [ 30 | param("postId") 31 | .trim() 32 | .escape() 33 | .not() 34 | .isEmpty() 35 | .withMessage("Post id can not be empty"), 36 | ]; 37 | 38 | const getCommentTreeOfCommentValidator = [ 39 | param("postId") 40 | .trim() 41 | .escape() 42 | .not() 43 | .isEmpty() 44 | .withMessage("Post id can not be empty"), 45 | param("commentId") 46 | .trim() 47 | .escape() 48 | .not() 49 | .isEmpty() 50 | .withMessage("Comment id can not be empty"), 51 | ]; 52 | 53 | // eslint-disable-next-line max-statements 54 | const createComment = async (req, res) => { 55 | try { 56 | const { 57 | content, 58 | postId, 59 | parentId, 60 | parentType, 61 | level, 62 | subredditName, 63 | haveSubreddit, 64 | } = req.body; 65 | const { username, userId } = req.payload; 66 | 67 | const post = await checkPostId(postId); 68 | const result = await createCommentService( 69 | { 70 | content, 71 | parentId, 72 | postId, 73 | parentType, 74 | level, 75 | subredditName, 76 | haveSubreddit, 77 | username, 78 | userId, 79 | }, 80 | post 81 | ); 82 | 83 | res.status(result.statusCode).json(result.data); 84 | } catch (error) { 85 | console.log(error.message); 86 | if (error.statusCode) { 87 | res.status(error.statusCode).json({ error: error.message }); 88 | } else { 89 | res.status(500).json("Internal server error"); 90 | } 91 | } 92 | }; 93 | 94 | const commentTree = async (req, res) => { 95 | try { 96 | let { sort } = req.query; 97 | const { before, after, limit } = req.query; 98 | const { postId } = req.params; 99 | 100 | const post = await checkPostId(postId); 101 | const user = await checkloggedInUser(req.userId); 102 | 103 | if (!sort) { 104 | sort = post.suggestedSort; 105 | } 106 | 107 | const result = await commentTreeListingService(user, post, { 108 | sort, 109 | before, 110 | after, 111 | limit, 112 | }); 113 | 114 | res.status(result.statusCode).json(result.data); 115 | } catch (error) { 116 | console.log(error.message); 117 | if (error.statusCode) { 118 | res.status(error.statusCode).json({ error: error.message }); 119 | } else { 120 | res.status(500).json("Internal server error"); 121 | } 122 | } 123 | }; 124 | 125 | const commentTreeOfComment = async (req, res) => { 126 | try { 127 | let { sort } = req.query; 128 | const { before, after, limit } = req.query; 129 | const { postId, commentId } = req.params; 130 | 131 | const post = await checkPostId(postId); 132 | const comment = await checkCommentId(commentId); 133 | const user = await checkloggedInUser(req.userId); 134 | 135 | if (!sort) { 136 | sort = post.suggestedSort; 137 | } 138 | 139 | const result = await commentTreeOfCommentListingService( 140 | user, 141 | post, 142 | comment, 143 | { 144 | sort, 145 | before, 146 | after, 147 | limit, 148 | } 149 | ); 150 | 151 | res.status(result.statusCode).json(result.data); 152 | } catch (error) { 153 | console.log(error.message); 154 | if (error.statusCode) { 155 | res.status(error.statusCode).json({ error: error.message }); 156 | } else { 157 | res.status(500).json("Internal server error"); 158 | } 159 | } 160 | }; 161 | 162 | export default { 163 | createCommentValidator, 164 | createComment, 165 | getCommentTreeValidator, 166 | commentTree, 167 | getCommentTreeOfCommentValidator, 168 | commentTreeOfComment, 169 | }; 170 | -------------------------------------------------------------------------------- /services/getPinnedPosts.js: -------------------------------------------------------------------------------- 1 | import User from "../models/User.js"; 2 | 3 | /** 4 | * This function is used to check if there is a loggedIn user and if yes, it makes another 5 | * check of whether there is a username sent with the query parameters to determine whether 6 | * the loggedIn user is viewing his own pinned posts or someone else. If there is no logged 7 | * in user, then a username has to be sent in the query or else there is no user to get his 8 | * pinned posts. In this case the logged in user is the same as the other user object. 9 | * @param {boolean} loggedIn True if there's a logged in user 10 | * @param {string} userId LoggedIn user ID 11 | * @param {string} username Username of the user's pinned posts if found 12 | * @returns {object} The logged in user and the other user if not the same 13 | */ 14 | // eslint-disable-next-line max-statements 15 | export async function checkUserPinnedPosts(loggedIn, userId, username) { 16 | let loggedInUser, user; 17 | if (loggedIn) { 18 | loggedInUser = await User.findById(userId)?.populate("pinnedPosts"); 19 | if (!loggedInUser || loggedInUser.deletedAt) { 20 | const error = new Error("User not found or may be deleted"); 21 | error.statusCode = 400; 22 | throw error; 23 | } 24 | if (!username) { 25 | user = loggedInUser; 26 | } else { 27 | user = await User.findOne({ username: username })?.populate( 28 | "pinnedPosts" 29 | ); 30 | // eslint-disable-next-line max-depth 31 | if (!user || user.deletedAt) { 32 | const error = new Error("User not found or may be deleted"); 33 | error.statusCode = 400; 34 | throw error; 35 | } 36 | } 37 | } else if (!loggedIn && !username) { 38 | const error = new Error("Username is needed"); 39 | error.statusCode = 400; 40 | throw error; 41 | } else { 42 | user = await User.findOne({ username: username })?.populate("pinnedPosts"); 43 | if (!user || user.deletedAt) { 44 | const error = new Error("User not found or may be deleted"); 45 | error.statusCode = 400; 46 | throw error; 47 | } 48 | loggedInUser = user; 49 | } 50 | return { 51 | loggedInUser: loggedInUser, 52 | user: user, 53 | }; 54 | } 55 | 56 | /** 57 | * This function sets necessary flags in case there is a logged in user which include the vote 58 | * made on this post, if the user owns this post and if he is a moderator in the post's subreddit 59 | * @param {object} loggedInUser logged In user object 60 | * @param {object} post Current pinned post 61 | * @returns {object} Contains three flags: vote, yourPost and inYourSubreddit 62 | */ 63 | export function setPinnedPostsFlags(loggedInUser, post) { 64 | let vote = 0, 65 | yourPost = false, 66 | inYourSubreddit = false; 67 | if ( 68 | loggedInUser.upvotedPosts.find( 69 | (postId) => postId.toString() === post.id.toString() 70 | ) 71 | ) { 72 | vote = 1; 73 | } else if ( 74 | loggedInUser.downvotedPosts.find( 75 | (postId) => postId.toString() === post.id.toString() 76 | ) 77 | ) { 78 | vote = -1; 79 | } 80 | if ( 81 | loggedInUser.posts.find( 82 | (postId) => postId.toString() === post.id.toString() 83 | ) 84 | ) { 85 | yourPost = true; 86 | } 87 | if ( 88 | loggedInUser.moderatedSubreddits?.find( 89 | (sr) => sr.name === post.subredditName 90 | ) 91 | ) { 92 | inYourSubreddit = true; 93 | } 94 | return { 95 | vote: vote, 96 | yourPost: yourPost, 97 | inYourSubreddit: inYourSubreddit, 98 | }; 99 | } 100 | 101 | /** 102 | * This function returns a pinned post details along with the flags associated with it 103 | * which were set in the setPinnedPostFlags middleware 104 | * @param {object} post Pinned post object 105 | * @param {object} params three flags: vote, yourPost and inYourSubreddit 106 | * @return {object} Post details object 107 | */ 108 | export function getPinnedPostDetails(post, params) { 109 | return { 110 | id: post.id.toString(), 111 | kind: post.kind, 112 | subreddit: post.subredditName, 113 | link: post.link, 114 | images: post.images, 115 | video: post.video, 116 | content: post.content, 117 | nsfw: post.nsfw, 118 | spoiler: post.spoiler, 119 | title: post.title, 120 | sharePostId: post.sharePostId, 121 | flair: post.flair, 122 | comments: post.numberOfComments, 123 | votes: post.numberOfVotes, 124 | postedAt: post.createdAt, 125 | postedBy: post.ownerUsername, 126 | vote: params.vote, 127 | yourPost: params.yourPost, 128 | inYourSubreddit: params.inYourSubreddit, 129 | locked: post.moderation.lock, 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /controllers/subredditRulesController.js: -------------------------------------------------------------------------------- 1 | import { 2 | validateCreatingRuleBody, 3 | validateEditingRuleBody, 4 | } from "../utils/subredditRules.js"; 5 | 6 | import { 7 | checkEditRulesOrderService, 8 | editRulesOrderService, 9 | checkDublicateRuleOrderService, 10 | } from "../services/subredditRules.js"; 11 | 12 | const getSubredditRules = (req, res) => { 13 | const rules = []; 14 | 15 | const compare = (a, b) => { 16 | if (a.ruleOrder < b.ruleOrder) { 17 | return -1; 18 | } 19 | if (a.ruleOrder > b.ruleOrder) { 20 | return 1; 21 | } 22 | return 0; 23 | }; 24 | 25 | req.subreddit.rules.forEach((el) => { 26 | if (!el.deletedAt) { 27 | rules.push({ 28 | ruleId: el._id, 29 | ruleName: el.ruleTitle, 30 | ruleOrder: el.ruleOrder, 31 | createdAt: new Date(el.createdAt), 32 | appliesTo: el.appliesTo, 33 | reportReason: el.reportReason ? el.reportReason : null, 34 | description: el.ruleDescription ? el.ruleDescription : null, 35 | }); 36 | } 37 | }); 38 | rules.sort(compare); 39 | res.status(200).json({ 40 | rules: rules, 41 | }); 42 | }; 43 | 44 | const addSubredditRule = async (req, res) => { 45 | const validationResult = validateCreatingRuleBody(req); 46 | 47 | if (!validationResult) { 48 | res.status(400).json({ 49 | error: "Bad request", 50 | }); 51 | } else if (req.subreddit.numberOfRules === 15) { 52 | res.status(400).json({ 53 | error: "Maximum number of rules!", 54 | }); 55 | } else { 56 | req.ruleObject.ruleOrder = req.subreddit.numberOfRules; 57 | req.subreddit.numberOfRules++; 58 | 59 | req.ruleObject.createdAt = new Date().toISOString(); 60 | 61 | req.subreddit.rules.push(req.ruleObject); 62 | 63 | try { 64 | await req.subreddit.save(); 65 | const ruleId = req.subreddit.rules.at(-1)._id; 66 | res.status(201).json({ ruleId: ruleId }); 67 | } catch (err) { 68 | console.log(err); 69 | res.status(500).json({ 70 | error: "Internal server error", 71 | }); 72 | } 73 | } 74 | }; 75 | 76 | // eslint-disable-next-line max-statements 77 | const editSubredditRule = async (req, res) => { 78 | const validationResult = validateEditingRuleBody(req); 79 | if (!validationResult) { 80 | res.status(400).json({ 81 | error: "Bad request", 82 | }); 83 | } else if ( 84 | req.neededRule.ruleOrder.toString() !== req.ruleObject.ruleOrder.toString() 85 | ) { 86 | { 87 | res.status(400).json({ 88 | error: "Rule id and rule order don't match", 89 | }); 90 | } 91 | } else { 92 | req.neededRule.ruleTitle = req.ruleObject.ruleTitle; 93 | req.neededRule.appliesTo = req.ruleObject.appliesTo; 94 | if (req.ruleObject.reportReason) { 95 | req.neededRule.reportReason = req.ruleObject.reportReason; 96 | } 97 | if (req.ruleObject.ruleDescription) { 98 | req.neededRule.ruleDescription = req.ruleObject.ruleDescription; 99 | } 100 | try { 101 | req.neededRule.updatedAt = new Date().toISOString(); 102 | // console.log(req.neededRule); 103 | await req.subreddit.save(); 104 | res.status(200).json("Updated successfully"); 105 | } catch (err) { 106 | res.status(500).json({ 107 | error: "Internal server error", 108 | }); 109 | } 110 | } 111 | }; 112 | 113 | const deleteSubredditRule = async (req, res) => { 114 | req.neededRule.deletedAt = new Date().toISOString(); 115 | req.subreddit.numberOfRules--; 116 | const neededRuleOrder = req.neededRule.ruleOrder; 117 | req.subreddit.rules.forEach((element) => { 118 | if (Number(element.ruleOrder) > Number(neededRuleOrder)) { 119 | element.ruleOrder--; 120 | } 121 | }); 122 | try { 123 | await req.subreddit.save(); 124 | res.status(200).json("Deleted successfully"); 125 | } catch (err) { 126 | console.log(err); 127 | res.status(500).json({ 128 | error: "Internal server error", 129 | }); 130 | } 131 | }; 132 | 133 | const editRulesOrder = async (req, res) => { 134 | try { 135 | checkEditRulesOrderService(req); 136 | checkDublicateRuleOrderService(req); 137 | await editRulesOrderService(req); 138 | res.status(200).json("Accepted"); 139 | } catch (err) { 140 | console.log(err.message); 141 | if (err.statusCode) { 142 | res.status(err.statusCode).json({ error: err.message }); 143 | } else { 144 | res.status(500).json("Internal server error"); 145 | } 146 | } 147 | }; 148 | 149 | export default { 150 | getSubredditRules, 151 | addSubredditRule, 152 | editSubredditRule, 153 | deleteSubredditRule, 154 | editRulesOrder, 155 | }; 156 | --------------------------------------------------------------------------------