├── .envexample ├── .gitignore ├── app.js ├── config.js ├── controllers ├── authController.js └── userController.js ├── db ├── connectDB.js ├── models │ └── userModel.js └── repositories │ └── userRepo.js ├── middlewares ├── apiError.js ├── apiResponse.js ├── globalErrorHandler.js └── schemaValidator.js ├── package-lock.json ├── package.json ├── readme.md ├── routes └── v1 │ ├── authRoutes.js │ ├── index.js │ ├── schemas │ ├── authSchemas.js │ └── userSchemas.js │ └── userRoutes.js └── utils ├── apiFeatures.js ├── catchAsync.js ├── email.js └── seeder.js /.envexample: -------------------------------------------------------------------------------- 1 | BASE_URL=http://localhost:5011/api/v1 2 | DB_NAME=starter-js 3 | DB_HOST=localhost 4 | DB_PORT=27017 5 | PORT=5011 6 | SECRET=starterjs1234secret 7 | EXPIRE_IN=1d 8 | JWT_COOKIE_EXPIRES_IN=30 9 | NODE_ENV=developpement 10 | EMAIL_USERNAME=8ae5df1d4de111 11 | EMAIL_PASSWORD=4ccc5ebee74b3c 12 | EMAIL_HOST=smtp.mailtrap.io 13 | EMAIL_PORT=2525 14 | ADMIN_NAME=admin-name 15 | ADMIN_LAST_NAME=admin-last-name 16 | ADMIN_EMAIL=admin@gmail.com 17 | ADMIN_PASS=admin1234 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const path = require("path"); 3 | require("dotenv").config(); 4 | const config = require("./config") 5 | const PORT = config.port || 5001; 6 | const app = express(); 7 | const cors = require("cors"); 8 | const bodyParser = require("body-parser"); 9 | const { connect } = require("./db/connectDB"); 10 | const { NotFoundError } = require("./middlewares/apiError"); 11 | const globalErrorHandler = require("./middlewares/globalErrorHandler"); 12 | const { baseUrl } = require("./config") 13 | 14 | const swaggerUI = require("swagger-ui-express"); 15 | const swaggerJsDoc = require("swagger-jsdoc"); 16 | app.use(express.json()); 17 | app.use(bodyParser.urlencoded({ extended: false })); 18 | app.use(cors()); 19 | app.use(bodyParser.json()); 20 | app.use(express.static(path.resolve("./assets"))); 21 | 22 | connect(); 23 | 24 | const options = { 25 | definition: { 26 | openapi: "3.0.0", 27 | info: { 28 | title: "Library API", 29 | version: "1.0.0", 30 | description: "A simple Express Library API", 31 | }, 32 | servers: [ 33 | { 34 | url: baseUrl, 35 | }, 36 | ], 37 | }, 38 | apis: ["./routes/v1/*.js", "./routes/v1/schemas/*.js"], 39 | }; 40 | 41 | const specs = swaggerJsDoc(options); 42 | 43 | app.use("/api-docs", swaggerUI.serve, swaggerUI.setup(specs)); 44 | 45 | const routes=require('./routes/v1/index'); 46 | 47 | app.use("/api/v1", routes); 48 | 49 | app.all("*", (req, res, next) => next(new NotFoundError())); 50 | 51 | app.use(globalErrorHandler); 52 | 53 | app.listen(PORT, () => { 54 | console.log(`app running on port ${PORT}`); 55 | }); 56 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports.environment = process.env.NODE_ENV; 2 | module.exports.baseUrl = process.env.BASE_URL; 3 | module.exports.port = process.env.PORT; 4 | 5 | module.exports.db = { 6 | name: process.env.DB_NAME || '', 7 | host: process.env.DB_HOST || '', 8 | port: process.env.DB_PORT || '' 9 | }; 10 | 11 | module.exports.corsUrl = process.env.CORS_URL; 12 | 13 | module.exports.tokenInfo = { 14 | expireIn: process.env.EXPIRE_IN, 15 | secret: process.env.SECRET, 16 | jwtCookieExpireIn: process.env.JWT_COOKIE_EXPIRES_IN 17 | }; 18 | 19 | module.exports.logDirectory = process.env.LOG_DIR; 20 | 21 | module.exports.seeder = { 22 | adminName: process.env.ADMIN_NAME || 'admin', 23 | adminLastName: process.env.ADMIN_LAST_NAME || 'adminlastname', 24 | adminEmail: process.env.ADMIN_EMAIL || 'admin@gmail.com', 25 | adminPass: process.env.ADMIN_PASS || 'admin1234' 26 | } 27 | 28 | module.exports.mailing = { 29 | host:process.env.EMAIL_HOST, 30 | port:process.env.EMAIL_PORT, 31 | user:process.env.EMAIL_USERNAME, 32 | pass:process.env.EMAIL_PASSWORD 33 | /*auth:{ 34 | user:process.env.EMAIL_USERNAME, 35 | pass:process.env.EMAIL_PASSWORD 36 | }*/ 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const { promisify } = require("util"); 3 | const asyncHandler = require("express-async-handler"); 4 | const crypto = require('crypto'); 5 | const { 6 | BadRequestError, 7 | UnauthorizedError, 8 | AccessTokenError, 9 | TokenExpiredError, 10 | NotFoundError, 11 | } = require("../middlewares/apiError"); 12 | const { 13 | SuccessResponse, 14 | SuccessMsgDataResponse, 15 | SuccessMsgResponse, 16 | } = require("../middlewares/apiResponse"); 17 | 18 | const UserRepo = require("../db/repositories/userRepo"); 19 | const { tokenInfo } = require("./../config"); 20 | const sendEmail = require("./../utils/email") 21 | 22 | const signToken = (id) => { 23 | return jwt.sign({ id }, tokenInfo.secret, { 24 | expiresIn: tokenInfo.expireIn, 25 | }); 26 | }; 27 | const createSendToken = (user, req, res) => { 28 | const token = signToken(user._id); 29 | 30 | res.cookie("jwt", token, { 31 | expires: new Date( 32 | Date.now() + tokenInfo.jwtCookieExpireIn * 24 * 60 * 60 * 1000 33 | ), 34 | httpOnly: true, 35 | secure: req.secure || req.headers["x-forwarded-proto"] === "https", 36 | }); 37 | 38 | user.password = undefined; 39 | 40 | return new SuccessResponse({ 41 | user, 42 | token, 43 | }).send(res); 44 | }; 45 | 46 | exports.login = asyncHandler(async (req, res) => { 47 | const { email, password } = req.body; 48 | const user = await UserRepo.findOneByObjSelect({ email }, "password"); 49 | 50 | if(!user || ! (await user.correctPassword(password, user.password))) { 51 | throw new BadRequestError("Incorrect email or password"); 52 | } 53 | createSendToken(user, req, res); 54 | }); 55 | 56 | exports.signUp = asyncHandler(async (req, res) => { 57 | 58 | const checkUser = await UserRepo.findOneByObj({ email: req.body.email }); 59 | if (checkUser) { 60 | throw new BadRequestError("A user with this email already exists"); 61 | } 62 | const user = await UserRepo.create(req.body) 63 | user.password = undefined; 64 | const token = signToken(user._id); 65 | 66 | return new SuccessResponse({ 67 | user, 68 | token, 69 | }).send(res); 70 | 71 | }); 72 | 73 | exports.getMe = asyncHandler(async (req, res) => { 74 | 75 | const freshUser = await UserRepo.findById(req.user.id); 76 | 77 | if (!freshUser) { 78 | throw new TokenExpiredError("Token expired"); 79 | } 80 | return new SuccessResponse(freshUser).send(res); 81 | 82 | }); 83 | 84 | exports.protect = asyncHandler(async (req, res, next) => { 85 | let token; 86 | 87 | if (req.headers.authorization) { 88 | token = req.headers.authorization.split(" ")[1]; 89 | } 90 | if (!token) { 91 | throw new AccessTokenError("No token provided"); 92 | } 93 | 94 | const decoded = await promisify(jwt.verify)(token, tokenInfo.secret); 95 | const freshUser = await UserRepo.findById(decoded.id); 96 | if (!freshUser) { 97 | throw new UnauthorizedError( 98 | "The user belonging to this token does no longer exist." 99 | ); 100 | } 101 | 102 | req.user = freshUser; 103 | 104 | next(); 105 | 106 | }); 107 | 108 | exports.restrictTo = (...roles) => { 109 | return (req, res, next) => { 110 | if (!roles.includes(req.user.role)) { 111 | throw new UnauthorizedError( 112 | "You do not have permission to perform this action" 113 | ); 114 | } 115 | next(); 116 | }; 117 | }; 118 | 119 | exports.updatePassword = asyncHandler(async (req, res) => { 120 | const user = await UserRepo.findOneByObjSelect(req.user._id, "password"); 121 | if (!user) 122 | { 123 | throw new NotFoundError("User not found"); 124 | } 125 | if (!(await user.correctPassword( req.body.currentPassword , user.password ))) { 126 | throw new BadRequestError("Your current password is wrong"); 127 | } 128 | user.password = req.body.newPassword; 129 | user.passwordConfirm = req.body.newPasswordConfirm; 130 | await user.save(); 131 | const token = signToken(user._id); 132 | 133 | return new SuccessMsgDataResponse(token, "Password updated sucessfully").send( 134 | res 135 | ); 136 | }); 137 | 138 | exports.updateMe = asyncHandler(async (req, res) => { 139 | 140 | const checkUser = await UserRepo.findOneByObj({_id: { $ne: req.user._id }, email: req.body.email }); 141 | if (checkUser) 142 | { 143 | throw new BadRequestError('A user with this email already exists') 144 | } 145 | 146 | const user = await UserRepo.findByIdAndUpdate(req.user._id, req.body); 147 | if (!user) 148 | { 149 | throw new NotFoundError("User not found"); 150 | } 151 | 152 | return new SuccessMsgDataResponse(user, "Profile updated sucessfully").send( 153 | res 154 | ); 155 | }); 156 | 157 | exports.forgotPassword = asyncHandler(async (req, res, next) => { 158 | // 1) Get user based on POSTed email 159 | const user = await UserRepo.findOneByObj({ email: req.body.email }); 160 | if (!user) { 161 | throw new NotFoundError('There is no user with email address.'); 162 | } 163 | 164 | // 2) Generate the random reset token 165 | const resetToken = user.createPasswordResetToken(); 166 | await user.save({ validateBeforeSave: false }); 167 | 168 | // 3) Send it to user's email 169 | const resetURL = req.protocol + '://' + req.get('host') + '/api/v1/users/resetPassword/' + resetToken; 170 | 171 | const message = 'Forgot your password? \nSubmit a PATCH request with your new password and passwordConfirm to: ' + resetURL + '.\nIf you didn\'t forget your password, please ignore this email!'; 172 | 173 | try { 174 | await sendEmail({ 175 | email: user.email, 176 | subject: 'Your password reset token (valid for 10 min)', 177 | message 178 | }); 179 | 180 | return new SuccessMsgResponse('Token sent to email!').send(res); 181 | 182 | } catch (err) { 183 | user.passwordResetToken = undefined; 184 | user.passwordResetExpires = undefined; 185 | await user.save({ validateBeforeSave: false }); 186 | 187 | throw new BadRequestError('There was an error sending the email. Try again later!'); 188 | } 189 | }); 190 | 191 | exports.resetPassword = asyncHandler(async (req, res, next) => { 192 | // 1) Get user based on the token 193 | const hashedToken = crypto 194 | .createHash('sha256') 195 | .update(req.params.token) 196 | .digest('hex'); 197 | 198 | const user = await UserRepo.findOneByObj({ 199 | passwordResetToken: hashedToken, 200 | passwordResetExpires: { $gt: Date.now() } 201 | }); 202 | 203 | // 2) If token has not expired, and there is user, set the new password 204 | if (!user) { 205 | throw new TokenExpiredError('Token is invalid or has expired'); 206 | } 207 | user.password = req.body.password; 208 | user.passwordConfirm = req.body.passwordConfirm; 209 | user.passwordResetToken = undefined; 210 | user.passwordResetExpires = undefined; 211 | await user.save(); 212 | 213 | // 3) Update changedPasswordAt property for the user 214 | // 4) Log the user in, send JWT 215 | createSendToken(user, req, res); 216 | }); 217 | -------------------------------------------------------------------------------- /controllers/userController.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = require("express-async-handler"); 2 | const userModel = require("../db/models/userModel"); 3 | const ApiError = require("../middlewares/apiError"); 4 | const { BadRequestError , NotFoundError } = require("../middlewares/apiError"); 5 | const { 6 | SuccessMsgResponse, 7 | SuccessResponse, 8 | SuccessMsgDataResponse, 9 | SuccessResponsePagination, 10 | } = require("../middlewares/apiResponse"); 11 | 12 | const UserRepo = require("../db/repositories/userRepo"); 13 | 14 | 15 | 16 | exports.getUser = asyncHandler(async (req, res) => { 17 | 18 | const user = await UserRepo.findById(req.params.id); 19 | if (!user) { 20 | throw new NotFoundError("No user found with that id"); 21 | } 22 | return new SuccessResponse(user).send(res); 23 | 24 | }); 25 | exports.createUser = asyncHandler(async (req, res) => { 26 | 27 | const checkUser = await UserRepo.findOneByObj({ email: req.body.email }); 28 | if (checkUser) { 29 | throw new BadRequestError("A user with this email already exists"); 30 | } 31 | 32 | let user = await UserRepo.create(req.body); 33 | let { password, ...data } = user._doc; 34 | return new SuccessMsgDataResponse(data, "User created successfully").send( 35 | res 36 | ); 37 | }); 38 | exports.getUsers = asyncHandler(async (req, res) => { 39 | 40 | const { page, perPage } = req.query; 41 | const options = { 42 | page: parseInt(page, 10) || 1, 43 | limit: parseInt(perPage, 10) || 10, 44 | }; 45 | const users = await UserRepo.findByObjPaginate({}, options, req.query); 46 | if (!users) { 47 | return new SuccessMsgResponse("No users found").send(res); 48 | } 49 | const { docs, ...meta } = users; 50 | 51 | return new SuccessResponsePagination(docs, meta).send(res); 52 | 53 | }); 54 | exports.deleteUser = asyncHandler(async (req, res) => { 55 | const user = await UserRepo.findOneByObj({ _id: req.params.id, deletedAt: null}); 56 | 57 | if (!user) { 58 | throw new NotFoundError("User not registered or deleted"); 59 | } 60 | let deletedUser = await UserRepo.deleteUser(user); 61 | return new SuccessMsgDataResponse(deletedUser, "User deleted successfully").send( 62 | res 63 | ); 64 | 65 | }); 66 | exports.updateUser = asyncHandler(async (req, res) => { 67 | 68 | if(req.body.email){ 69 | const checkUser = await UserRepo.findOneByObj({_id: { $ne: req.params.id }, email: req.body.email }); 70 | if (checkUser) { 71 | throw new BadRequestError("A user with this email already exists"); 72 | } 73 | } 74 | const user = await UserRepo.findByIdAndUpdate( req.params.id, req.body); 75 | if (!user) { 76 | throw new NotFoundError("No user found with that id"); 77 | } 78 | return new SuccessMsgDataResponse(user, "User updated successfully").send( 79 | res 80 | ); 81 | 82 | }); 83 | 84 | 85 | -------------------------------------------------------------------------------- /db/connectDB.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { db } = require("./../config") 3 | 4 | const dbURI = 'mongodb://' + db.host + ':' + db.port + '/' + db.name; 5 | console.log(dbURI); 6 | module.exports.connect = async () => { 7 | try { 8 | const conn = await mongoose.connect(dbURI) 9 | console.log('Mongo db connected', conn.connection.host) 10 | } catch (error) { 11 | console.log(error) 12 | process.exit(1) 13 | } 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /db/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const mongoosePaginate = require('mongoose-paginate-v2'); 3 | const crypto = require("crypto"); 4 | const bcrypt = require("bcryptjs"); 5 | const userSchema = new mongoose.Schema( 6 | { 7 | name: { 8 | type: String 9 | }, 10 | lastName: { 11 | type: String 12 | }, 13 | email: { 14 | type: String, 15 | unique: true, 16 | trim: true, 17 | lowercase: true, 18 | }, 19 | password: { 20 | type: String, 21 | }, 22 | passwordConfirm: { 23 | type: String, 24 | }, 25 | tel: { 26 | type: String, 27 | }, 28 | work: { 29 | type: String, 30 | }, 31 | status: { 32 | type: String, 33 | enum: ["active", "banned"], 34 | default: "active", 35 | }, 36 | role: { 37 | type: String, 38 | enum: ["admin", "user"], 39 | default: "user", 40 | }, 41 | deletedAt: Date, 42 | passwordChangedAt: Date, 43 | passwordResetToken: String, 44 | passwordResetExpires: Date, 45 | }, 46 | { 47 | timestamps: true, 48 | } 49 | ); 50 | 51 | 52 | userSchema.plugin(mongoosePaginate) 53 | 54 | userSchema.pre(/^find/, function (next) { 55 | this.select("-__v -createdAt -updatedAt -password -passwordConfirm"); 56 | next(); 57 | }); 58 | 59 | userSchema.pre("save", function (next) { 60 | if(this.isModified("email")) 61 | this.email = this.email.toLocaleLowerCase(); 62 | if (!this.isModified("password") || this.isNew) return next(); 63 | this.passwordChangedAt = Date.now() - 1000; 64 | next(); 65 | }); 66 | userSchema.methods.correctPassword = async function ( 67 | otherPassword, 68 | userPassword 69 | ) { 70 | return await bcrypt.compare(otherPassword, userPassword); 71 | }; 72 | userSchema.methods.changedPasswordAfter = function (jwtTimestamp) { 73 | if (this.passwordChangedAt) { 74 | const changedTimestamp = parseInt( 75 | this.passwordChangedAt.getTime() / 1000, 76 | 10 77 | ); 78 | return jwtTimestamp < changedTimestamp; 79 | } 80 | return false; 81 | }; 82 | userSchema.pre("save", async function (next) { 83 | if (!this.isModified("password")) return next(); 84 | this.password = await bcrypt.hash(this.password, 12); 85 | this.passwordConfirm = undefined; 86 | next(); 87 | }); 88 | 89 | userSchema.methods.createPasswordResetToken = function () { 90 | const resetToken = crypto.randomBytes(32).toString("hex"); 91 | this.passwordResetToken = crypto 92 | .createHash("sha256") 93 | .update(resetToken) 94 | .digest("hex"); 95 | this.passwordResetExpires = Date.now() + 10 * 60 * 1000; 96 | return resetToken; 97 | }; 98 | 99 | 100 | module.exports = mongoose.model("User", userSchema); 101 | -------------------------------------------------------------------------------- /db/repositories/userRepo.js: -------------------------------------------------------------------------------- 1 | const userModel = require("../models/userModel"); 2 | const APIFeatures = require ('../../utils/apiFeatures') 3 | 4 | module.exports = class UserRepo { 5 | static findOneByObjSelect(obj, select){ 6 | return userModel.findOne(obj).select(select); 7 | } 8 | 9 | static findOneByObj(obj){ 10 | return userModel.findOne(obj); 11 | } 12 | 13 | static create(payload){ 14 | return userModel.create(payload); 15 | } 16 | 17 | static findById(id){ 18 | return userModel.findById(id); 19 | } 20 | 21 | static findByIdAndUpdate(id, payload){ 22 | return userModel.findByIdAndUpdate(id, { $set : payload } , { new :true } ); 23 | } 24 | 25 | static async findByObjPaginate(obj, options, query){ 26 | let deleted = query.deleted == "true" ? true : false; 27 | 28 | const features = new APIFeatures( 29 | deleted ? userModel.find({...obj, deletedAt: { $ne: null} }) 30 | : userModel.find({...obj, deletedAt: null}), 31 | query 32 | ) 33 | .filter() 34 | .sort() 35 | .limitFields() 36 | .search(["name", "lastName", "role", "tel", "work"]) 37 | 38 | 39 | let optionsPaginate = { 40 | limit: options.limit ? options.limit : null, 41 | page: options.page ? options.page : null, 42 | }; 43 | 44 | const pagination = userModel.paginate(features.query, optionsPaginate); 45 | return await pagination; 46 | } 47 | 48 | static async deleteUser(user) { 49 | let email = user.email; 50 | let regex = '^old[0-9]+' + email; 51 | const deletedUsers = await userModel.count({ email: { $regex: regex } }); 52 | return userModel.findByIdAndUpdate(user._id, { $set: { email: `old${deletedUsers}${email}`, deletedAt: Date.now() } }, { new: true }) 53 | .lean() 54 | .exec(); 55 | } 56 | } -------------------------------------------------------------------------------- /middlewares/apiError.js: -------------------------------------------------------------------------------- 1 | const { 2 | BadRequestResponse, 3 | NotFoundResponse, 4 | ValidationFailResponse, 5 | InternelResponse, 6 | AuthFailureResponse, 7 | } = require("../middlewares/apiResponse"); 8 | const environment = require("../config"); 9 | 10 | const ErrorType = { 11 | BAD_TOKEN: "BadTokenError", 12 | TOKEN_EXPIRED: "TokenExpiredError", 13 | UNAUTHORIZED: "AuthFailureError", 14 | ACCESS_TOKEN: "AccessTokenError", 15 | INTERNAL: "InternalError", 16 | NOT_FOUND: "NotFoundError", 17 | NO_ENTRY: "NoEntryError", 18 | NO_DATA: "NoDataError", 19 | BAD_REQUEST: "BadRequestError", 20 | FORBIDDEN: "ForbiddenError", 21 | VALIDATION_FAIL: "ValidationFail", 22 | }; 23 | 24 | class ApiError extends Error { 25 | constructor(type, message) { 26 | super(message); 27 | this.type = type; 28 | } 29 | 30 | static handle(err, res) { 31 | switch (err.type) { 32 | case ErrorType.UNAUTHORIZED: 33 | return new AuthFailureResponse(err.message).send(res); 34 | case ErrorType.NOT_FOUND: 35 | case ErrorType.NO_DATA: 36 | return new NotFoundResponse(err.message).send(res); 37 | case ErrorType.BAD_REQUEST: 38 | return new BadRequestResponse(err.message).send(res); 39 | case ErrorType.VALIDATION_FAIL: 40 | return new ValidationFailResponse(err.message).send(res); 41 | default: { 42 | let message = err.message; 43 | if (environment === "production") 44 | message = "Something wrong happened."; 45 | return new InternelResponse(message).send(res); 46 | } 47 | } 48 | } 49 | } 50 | 51 | class BadRequestError extends ApiError { 52 | constructor(message = "Bad Request") { 53 | super(ErrorType.BAD_REQUEST, message); 54 | } 55 | } 56 | 57 | class NotFoundError extends ApiError { 58 | constructor(message = "Not Found") { 59 | super(ErrorType.NOT_FOUND, message); 60 | } 61 | } 62 | 63 | class NoDataError extends ApiError { 64 | constructor(message = "No data available") { 65 | super(ErrorType.NO_DATA, message); 66 | } 67 | } 68 | class ValidationError extends ApiError { 69 | constructor(message = "Validation Fail") { 70 | super(ErrorType.VALIDATION_FAIL, message); 71 | } 72 | } 73 | class UnauthorizedError extends ApiError { 74 | constructor(message = "Login Failed") { 75 | super(ErrorType.UNAUTHORIZED, message); 76 | } 77 | } 78 | class AccessTokenError extends ApiError { 79 | constructor(message = "Token Error") { 80 | super(ErrorType.ACCESS_TOKEN, message); 81 | } 82 | } 83 | class TokenExpiredError extends ApiError { 84 | constructor(message = "Access token error") { 85 | super(ErrorType.TOKEN_EXPIRED, message); 86 | } 87 | } 88 | 89 | 90 | 91 | 92 | module.exports = { 93 | ApiError, 94 | NoDataError, 95 | NotFoundError, 96 | BadRequestError, 97 | UnauthorizedError, 98 | ValidationError, 99 | AccessTokenError, 100 | TokenExpiredError 101 | }; 102 | -------------------------------------------------------------------------------- /middlewares/apiResponse.js: -------------------------------------------------------------------------------- 1 | const ResponseStatus = { 2 | SUCCESS: 200, 3 | BAD_REQUEST: 400, 4 | UNAUTHORIZED: 401, 5 | NOT_FOUND: 404, 6 | VALIDATION_ERROR: 422, 7 | INTERNAL_ERROR: 500, 8 | }; 9 | 10 | 11 | const StatusCode = { 12 | SUCCESS : "success", 13 | FAILURE : "fail" 14 | } 15 | 16 | class ApiResponse { 17 | constructor(statusCode, status, message) { 18 | this.statusCode = statusCode; 19 | this.status = status; 20 | this.message = message; 21 | } 22 | 23 | send(res) { 24 | return res 25 | .status(this.statusCode) 26 | .json({ status: this.status, message: this.message }); 27 | } 28 | } 29 | 30 | class ApiResponseData { 31 | constructor(statusCode, status, data, message) { 32 | this.statusCode = statusCode; 33 | this.status = status; 34 | this.data = data; 35 | 36 | } 37 | 38 | send(res) { 39 | return res 40 | .status(this.statusCode) 41 | .json({ status: this.status, data: this.data }); 42 | } 43 | } 44 | 45 | class ApiResponseDataMsg { 46 | constructor(statusCode, status, data, message) { 47 | this.statusCode = statusCode; 48 | this.status = status; 49 | this.data = data; 50 | this.message = message 51 | } 52 | 53 | send(res) { 54 | return res 55 | .status(this.statusCode) 56 | .json({ status: this.status, message: this.message, data: this.data }); 57 | } 58 | } 59 | 60 | 61 | class ApiResponseDataMeta{ 62 | constructor(statusCode, status, data, meta) { 63 | this.statusCode = statusCode; 64 | this.status = status; 65 | this.data = data; 66 | this.meta = meta 67 | } 68 | 69 | send(res) { 70 | return res 71 | .status(this.statusCode) 72 | .json({ status: this.status, data: this.data, meta: this.meta }); 73 | } 74 | } 75 | 76 | class BadRequestResponse extends ApiResponse { 77 | constructor(message = "Bad Parameters") { 78 | super(ResponseStatus.BAD_REQUEST, "fail", message); 79 | } 80 | } 81 | 82 | class NotFoundResponse extends ApiResponse { 83 | constructor(message = "Not Found") { 84 | super(ResponseStatus.NOT_FOUND, StatusCode.FAILURE, message); 85 | } 86 | } 87 | class ValidationFailResponse extends ApiResponse { 88 | constructor(message = "Validation Error") { 89 | super(ResponseStatus.VALIDATION_ERROR, StatusCode.FAILURE, message); 90 | } 91 | } 92 | class InternelResponse extends ApiResponse { 93 | constructor(message = "Server Error") { 94 | super(ResponseStatus.INTERNAL_ERROR, StatusCode.FAILURE, message); 95 | } 96 | } 97 | class AuthFailureResponse extends ApiResponse { 98 | constructor(message = "Login Error") { 99 | super(ResponseStatus.UNAUTHORIZED, StatusCode.FAILURE, message); 100 | } 101 | } 102 | class SuccessResponse extends ApiResponseData { 103 | constructor(data) { 104 | super(ResponseStatus.SUCCESS, StatusCode.SUCCESS, data); 105 | } 106 | } 107 | 108 | 109 | class SuccessMsgDataResponse extends ApiResponseDataMsg { 110 | constructor(data, message) { 111 | super(ResponseStatus.SUCCESS, StatusCode.SUCCESS, data, message); 112 | } 113 | } 114 | class SuccessResponsePagination extends ApiResponseDataMeta { 115 | constructor(data, meta) { 116 | super(ResponseStatus.SUCCESS, StatusCode.SUCCESS, data, meta); 117 | } 118 | } 119 | class SuccessMsgResponse extends ApiResponse { 120 | constructor(message = "Successful") { 121 | super(ResponseStatus.SUCCESS, StatusCode.SUCCESS, message); 122 | } 123 | } 124 | 125 | 126 | module.exports = { 127 | SuccessResponse, 128 | SuccessMsgDataResponse , 129 | SuccessMsgResponse, 130 | BadRequestResponse, 131 | NotFoundResponse, 132 | ValidationFailResponse, 133 | InternelResponse, 134 | AuthFailureResponse, 135 | SuccessResponsePagination 136 | 137 | }; 138 | -------------------------------------------------------------------------------- /middlewares/globalErrorHandler.js: -------------------------------------------------------------------------------- 1 | const { ApiError } = require("./apiError"); 2 | 3 | module.exports = (err, req, res, next) => { 4 | ApiError.handle(err, res); 5 | }; 6 | -------------------------------------------------------------------------------- /middlewares/schemaValidator.js: -------------------------------------------------------------------------------- 1 | const Joi = require("joi"); 2 | const { Types } = require("mongoose"); 3 | const { ValidationError } = require("./apiError"); 4 | 5 | exports.isJson = (data) => { 6 | try { 7 | return JSON.parse(data); 8 | } catch (e) { 9 | return false; 10 | } 11 | }; 12 | 13 | exports.schemaValidator = ((schema, source = "body") => (req , res, next ) => { 14 | const { _, error } = schema.validate(req[source]); 15 | 16 | if (!error) return next(); 17 | 18 | if (error) { 19 | const { details } = error; 20 | const JoiError = details 21 | .map((i) => i.message.replace(/['"]+/g, "")) 22 | .join(" , "); 23 | throw new ValidationError(JoiError); 24 | } 25 | }); 26 | 27 | exports.JoiObjectId = () => 28 | Joi.string().custom((value, helpers) => { 29 | if (!Types.ObjectId.isValid(value)) return helpers.error("any.invalid"); 30 | return value; 31 | }, "Object Id Validation"); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "start:dev": "nodemon app.js", 9 | "seed": "node utils/seeder.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "bcrypt": "^5.0.1", 16 | "bcryptjs": "^2.4.3", 17 | "body-parser": "^1.20.0", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.0.0", 20 | "express": "^4.17.3", 21 | "express-async-handler": "^1.2.0", 22 | "joi": "^17.6.0", 23 | "jsonwebtoken": "^8.5.1", 24 | "mongoose": "^6.2.11", 25 | "mongoose-paginate-v2": "^1.7.0", 26 | "multer": "^1.4.5-lts.1", 27 | "nodemailer": "^6.8.0", 28 | "nodemon": "^2.0.15", 29 | "path": "^0.12.7", 30 | "swagger-jsdoc": "^6.2.5", 31 | "swagger-ui-express": "^4.5.0", 32 | "util": "^0.12.4", 33 | "validator": "^13.7.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Starter Node JS architecture and improvements 2 | 3 | 1- Best architecture refactored from open sources projects like 4 | https://github.com/jonasschmedtmann/complete-node-bootcamp 5 | 6 | 2- ApiFeature refactored. 7 | 8 | 3- Refactor Error Handling. 9 | 10 | 4- Add repository layer. 11 | 12 | 5- Add seeder. 13 | 14 | 6- Add pagination on model on get all apis. 15 | 16 | 7- Add custom response for pagination data. 17 | 18 | 8- Add schema validator middleware and use of joi npm module. 19 | 20 | 9- Add Swagger documention with express. 21 | 22 | 10- Add config file that regroups .env variables. 23 | 24 | 11- Implement manual soft delete. 25 | 26 | EXECUTION : 27 | 28 | 1- npm install 29 | 30 | 2- copy .envexample to .env 31 | 32 | 3- npm run seed 33 | 34 | 4- npm startfor run / npm run start:dev for run on dev mode 35 | -------------------------------------------------------------------------------- /routes/v1/authRoutes.js: -------------------------------------------------------------------------------- 1 | const authController = require("../../controllers/authController"); 2 | const { schemaValidator } = require("../../middlewares/schemaValidator"); 3 | const { 4 | signup, 5 | login, 6 | updatePassword, 7 | updateMe, 8 | } = require("./schemas/authSchemas"); 9 | const express = require("express"); 10 | const router = express.Router(); 11 | /** 12 | * @swagger 13 | * tags: 14 | * name: User 15 | * description: The User managing API 16 | */ 17 | 18 | /** 19 | * @swagger 20 | * /register: 21 | * post: 22 | * summary: Register 23 | * tags: [Auth] 24 | * requestBody: 25 | * required: true 26 | * content: 27 | * application/json: 28 | * schema: 29 | * $ref: '#/components/schemas/CreateUser' 30 | * responses: 31 | * 200: 32 | * description: The list of the register 33 | * content: 34 | * application/json: 35 | * schema: 36 | * type: object 37 | * $ref: '#/components/schemas/User' 38 | * 39 | * 40 | */ 41 | 42 | router.post("/register", schemaValidator(signup), authController.signUp); 43 | 44 | 45 | /** 46 | * @swagger 47 | * /login: 48 | * post: 49 | * summary: User login 50 | * requestBody: 51 | * required: true 52 | * content: 53 | * application/json: 54 | * schema: 55 | * $ref: '#/components/schemas/LoginSchema' 56 | * tags: [Auth] 57 | * responses: 58 | * 200: 59 | * description: Login 60 | * content: 61 | * application/json: 62 | * schema: 63 | * type: object 64 | * $ref: '#/components/schemas/User' 65 | * 66 | */ 67 | 68 | router.post("/login", schemaValidator(login), authController.login); 69 | 70 | /** 71 | * @swagger 72 | * /me: 73 | * get: 74 | * summary: Get me 75 | * tags: [Auth] 76 | * responses: 77 | * 200: 78 | * description: Get me 79 | * content: 80 | * application/json: 81 | * schema: 82 | * type: object 83 | * $ref: '#/components/schemas/User' 84 | * security: 85 | * - bearerAuth: [] 86 | */ 87 | 88 | router.route("/me").get(authController.protect, authController.getMe); 89 | 90 | /** 91 | * @swagger 92 | * /updateMyPassword: 93 | * put: 94 | * summary: Password update 95 | * tags: [Auth] 96 | * requestBody: 97 | * required: true 98 | * content: 99 | * application/json: 100 | * schema: 101 | * $ref: '#/components/schemas/UpdatePassword' 102 | * responses: 103 | * 200: 104 | * description: Password update 105 | * content: 106 | * application/json: 107 | * schema: 108 | * type: object 109 | * $ref: '#/components/schemas/Password' 110 | * security: 111 | * - bearerAuth: [] 112 | */ 113 | 114 | router 115 | .route("/updateMyPassword") 116 | .put( 117 | authController.protect, 118 | schemaValidator(updatePassword), 119 | authController.updatePassword 120 | ); 121 | 122 | /** 123 | * @swagger 124 | * /updateMe: 125 | * put: 126 | * summary: Update user's profile 127 | * tags: [Auth] 128 | * requestBody: 129 | * required: true 130 | * content: 131 | * application/json: 132 | * schema: 133 | * $ref: '#/components/schemas/UpdateUser' 134 | * responses: 135 | * 200: 136 | * description: Update user's profile 137 | * content: 138 | * application/json: 139 | * schema: 140 | * type: object 141 | * $ref: '#/components/schemas/User' 142 | * security: 143 | * - bearerAuth: [] 144 | */ 145 | router 146 | .route("/updateMe") 147 | .put( 148 | authController.protect, 149 | schemaValidator(updateMe), 150 | authController.updateMe 151 | ); 152 | 153 | /** 154 | * @swagger 155 | * /forgotPassword: 156 | * post: 157 | * summary: forget password api 158 | * tags: [Auth] 159 | * requestBody: 160 | * required: true 161 | * content: 162 | * application/json: 163 | * schema: 164 | * $ref: '#/components/schemas/ForgetPassword' 165 | * responses: 166 | * 200: 167 | * content: 168 | * application/json: 169 | * schema: 170 | * type: object 171 | * properties: 172 | * status: 173 | * default : success 174 | * message: 175 | * default: Token sent to email! 176 | * 400: 177 | * content: 178 | * application/json: 179 | * schema: 180 | * type: object 181 | * properties: 182 | * status: 183 | * default : fail 184 | * message: 185 | * default: There was an error sending the email. Try again later! 186 | * security: 187 | * - bearerAuth: [] 188 | */ 189 | router.post('/forgotPassword', authController.forgotPassword); 190 | 191 | /** 192 | * @swagger 193 | * /resetPassword/{token}: 194 | * patch: 195 | * summary: reset password api 196 | * tags: [Auth] 197 | * requestBody: 198 | * required: true 199 | * content: 200 | * application/json: 201 | * schema: 202 | * $ref: '#/components/schemas/ResetPassword' 203 | * parameters: 204 | * - in: path 205 | * name: token 206 | * responses: 207 | * 200: 208 | * content: 209 | * application/json: 210 | * schema: 211 | * type: object 212 | * $ref: '#/components/schemas/User' 213 | */ 214 | router.patch('/resetPassword/:token', authController.resetPassword); 215 | 216 | 217 | 218 | 219 | module.exports = router; 220 | -------------------------------------------------------------------------------- /routes/v1/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const authRoutes = require("./authRoutes"); 5 | const userRoutes = require("./userRoutes"); 6 | 7 | router.use("/",authRoutes); 8 | router.use("/users",userRoutes) 9 | 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /routes/v1/schemas/authSchemas.js: -------------------------------------------------------------------------------- 1 | const Joi = require("joi"); 2 | 3 | /** 4 | * @swagger 5 | * components: 6 | * securitySchemes: 7 | * bearerAuth: 8 | * type: http 9 | * scheme: bearer 10 | * bearerFormat: JWT 11 | */ 12 | /** 13 | * @swagger 14 | * components: 15 | * schemas: 16 | * Password: 17 | * type: object 18 | * properties: 19 | * currentPassword: 20 | * type: string 21 | * description: user's current password 22 | * newPassword: 23 | * type: string 24 | * description: user's new password 25 | * newPasswordConfirm: 26 | * type: string 27 | * description: user's new password confirmation 28 | * LoginSchema: 29 | * type: object 30 | * required: 31 | * - email 32 | * - password 33 | * properties: 34 | * email: 35 | * type: string 36 | * description: Email address 37 | * password: 38 | * type: string 39 | * description: Minimum of 8 characters long 40 | * UpdatePassword: 41 | * type: object 42 | * properties: 43 | * currentPassword: 44 | * type: string 45 | * description: user's current password 46 | * newPassword: 47 | * type: string 48 | * description: user's new password 49 | * newPasswordConfirm: 50 | * type: string 51 | * description: user's new password confirmation 52 | * ForgetPassword: 53 | * type: object 54 | * properties: 55 | * email: 56 | * type: string 57 | * description: user's email 58 | * ResetPassword: 59 | * type: object 60 | * properties: 61 | * password: 62 | * type: string 63 | * description: user's new password 64 | * psswordConfirm: 65 | * type: string 66 | * description: user's new password 67 | */ 68 | 69 | exports.signup = Joi.object().keys({ 70 | name: Joi.string().min(3).max(30).required(), 71 | lastName: Joi.string().min(3).max(30).required(), 72 | email: Joi.string().min(3).required().email(), 73 | password: Joi.string().min(8).required().regex(/^[a-zA-Z0-9]{8,30}$/), 74 | passwordConfirm:Joi.string().min(8).required().valid(Joi.ref('password')).regex(/^[a-zA-Z0-9]{8,30}$/), 75 | tel:Joi.string().length(8).required(), 76 | work:Joi.string().min(3).required() 77 | }); 78 | 79 | exports.login=Joi.object({ 80 | email: Joi.string().min(3).required().email(), 81 | password: Joi.string().min(8).required().regex(/^[a-zA-Z0-9]{8,30}$/) 82 | }) 83 | 84 | exports.updatePassword=Joi.object().keys({ 85 | currentPassword:Joi.string().required().regex(/^[a-zA-Z0-9]{8,30}$/), 86 | newPassword: Joi.string().min(8).required().regex(/^[a-zA-Z0-9]{8,30}$/), 87 | newPasswordConfirm:Joi.string().min(8).required().valid(Joi.ref('newPassword')).regex(/^[a-zA-Z0-9]{8,30}$/) 88 | }) 89 | exports.checkoutSchema = Joi.object({ 90 | payment_token:Joi.string().required(), 91 | transaction:Joi.string().required() 92 | }) 93 | 94 | exports.updateMe=Joi.object().keys({ 95 | name: Joi.string().trim().min(3).max(30).optional(), 96 | lastName: Joi.string().trim().min(3).max(30).optional(), 97 | tel:Joi.string().length(8).optional(), 98 | work:Joi.string().min(3).optional(), 99 | email:Joi.string().email().optional() 100 | }) 101 | 102 | -------------------------------------------------------------------------------- /routes/v1/schemas/userSchemas.js: -------------------------------------------------------------------------------- 1 | const Joi = require("joi"); 2 | const { JoiObjectId } = require("../../../middlewares/schemaValidator"); 3 | 4 | /** 5 | * @swagger 6 | * components: 7 | * securitySchemes: 8 | * bearerAuth: 9 | * type: http 10 | * scheme: bearer 11 | * bearerFormat: JWT 12 | */ 13 | 14 | 15 | /** 16 | * @swagger 17 | * components: 18 | * schemas: 19 | * User: 20 | * type: object 21 | * properties: 22 | * name: 23 | * type: string 24 | * description: The username 25 | * lastName: 26 | * type: string 27 | * description: The last name 28 | * email: 29 | * type: string 30 | * description: Email address 31 | * tel: 32 | * type: string 33 | * description: Should be 8 characters long 34 | * work: 35 | * type: string 36 | * description: User's work 37 | * status: 38 | * type: string 39 | * description: user status active or not 40 | * role: 41 | * type: string 42 | * description: user role admin or user 43 | * UpdateUser: 44 | * type: object 45 | * properties: 46 | * name: 47 | * type: string 48 | * description: The username 49 | * lastName: 50 | * type: string 51 | * description: The last name 52 | * email: 53 | * type: string 54 | * description: Email address 55 | * tel: 56 | * type: string 57 | * description: Should be 8 characters long 58 | * work: 59 | * type: string 60 | * description: User's work 61 | * status: 62 | * type: string 63 | * description: user status active or not 64 | * CreateUser: 65 | * type: object 66 | * required: 67 | * - name 68 | * - lastName 69 | * - email 70 | * - password 71 | * - passwordConfirm 72 | * - tel 73 | * - work 74 | * properties: 75 | * name: 76 | * type: string 77 | * description: The username 78 | * lastName: 79 | * type: string 80 | * description: The last name 81 | * email: 82 | * type: string 83 | * description: Email address 84 | * password: 85 | * type: string 86 | * description: Minimum of 8 characters long 87 | * passwordConfirm: 88 | * type: string 89 | * description: Should match with the password 90 | * tel: 91 | * type: string 92 | * description: Should be 8 characters long 93 | * work: 94 | * type: string 95 | * description: User's work 96 | */ 97 | 98 | 99 | exports.createUser = Joi.object({ 100 | name: Joi.string().trim().min(3).max(30).required(), 101 | lastName: Joi.string().trim().min(3).max(30).required(), 102 | email: Joi.string().min(3).required().email(), 103 | password: Joi.string().min(8).required().regex(/^[a-zA-Z0-9]{8,30}$/), 104 | passwordConfirm:Joi.string().min(8).required().valid(Joi.ref('password')).regex(/^[a-zA-Z0-9]{8,30}$/), 105 | tel:Joi.string().length(8).required(), 106 | work:Joi.string().min(3).required() 107 | 108 | }); 109 | exports.updateUser = Joi.object({ 110 | name: Joi.string().trim().min(3).max(30).optional(), 111 | lastName: Joi.string().trim().min(3).max(30).optional(), 112 | email: Joi.string().min(3).email().optional(), 113 | password: Joi.string().min(8).regex(/^[a-zA-Z0-9]{8,30}$/).optional(), 114 | passwordConfirm:Joi.string().min(8).valid(Joi.ref('password')).regex(/^[a-zA-Z0-9]{8,30}$/).optional(), 115 | tel:Joi.string().length(8).optional(), 116 | work:Joi.string().min(3).optional() 117 | 118 | }); 119 | 120 | exports.getUsers = Joi.object({ 121 | name: Joi.string().trim().min(3).max(30).optional(), 122 | lastName: Joi.string().trim().min(3).max(30).optional(), 123 | email: Joi.string().min(3).optional().email(), 124 | tel: Joi.string().optional(), 125 | work: Joi.string().min(3).optional(), 126 | deleted: Joi.boolean().optional(), 127 | page: Joi.number().optional(), 128 | perPage: Joi.number().optional(), 129 | search: Joi.string().optional(), 130 | sort: Joi.string().optional() 131 | 132 | }) 133 | exports.checkUserId=Joi.object({ 134 | id:JoiObjectId().required() 135 | }) 136 | 137 | exports.forgetPassword = Joi.object({ 138 | email:Joi.string().required() 139 | }) 140 | 141 | exports.resetPasswordToken = Joi.object({ 142 | token:Joi.string().required() 143 | }) 144 | 145 | exports.resetPassword = Joi.object({ 146 | password: Joi.string().min(8).regex(/^[a-zA-Z0-9]{8,30}$/).required(), 147 | passwordConfirm:Joi.string().min(8).valid(Joi.ref('password')).regex(/^[a-zA-Z0-9]{8,30}$/).required() 148 | }) 149 | -------------------------------------------------------------------------------- /routes/v1/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const userController = require("../../controllers/userController"); 4 | const authController = require("../../controllers/authController"); 5 | const { 6 | createUser, 7 | updateUser, 8 | getUsers, 9 | checkUserId, 10 | } = require("./schemas/userSchemas"); 11 | const { schemaValidator } = require("../../middlewares/schemaValidator"); 12 | 13 | 14 | /** 15 | * @swagger 16 | * /users: 17 | * get: 18 | * summary: Returns the list of all the users 19 | * tags: [User] 20 | * parameters: 21 | * - in: query 22 | * name: search 23 | * schema: 24 | * type: string 25 | * - in: query 26 | * name: name 27 | * schema: 28 | * type: string 29 | * - in: query 30 | * name: work 31 | * schema: 32 | * type: string 33 | * - in: query 34 | * name: email 35 | * schema: 36 | * type: string 37 | * - in: query 38 | * name: lastName 39 | * schema: 40 | * type: string 41 | * - in: query 42 | * name: tel 43 | * schema: 44 | * type: integer 45 | * - in: query 46 | * name: deleted 47 | * schema: 48 | * type: string 49 | * - in: query 50 | * name: sort 51 | * schema: 52 | * type: string 53 | * - in: query 54 | * name: page 55 | * schema: 56 | * type: integer 57 | * - in: query 58 | * name: perPage 59 | * schema: 60 | * type: integer 61 | * responses: 62 | * 200: 63 | * description: The list of the users 64 | * content: 65 | * application/json: 66 | * schema: 67 | * type: array 68 | * items: 69 | * $ref: '#/components/schemas/User' 70 | * security: 71 | * - bearerAuth: [] 72 | */ 73 | 74 | router.get( 75 | "/", 76 | authController.protect, 77 | authController.restrictTo("admin"), 78 | schemaValidator(getUsers, "query"), 79 | userController.getUsers 80 | ); 81 | 82 | 83 | /** 84 | * @swagger 85 | * /users/{id}: 86 | * get: 87 | * summary: Get one user by id 88 | * tags: [User] 89 | * parameters: 90 | * - in: path 91 | * name: id 92 | * responses: 93 | * 200: 94 | * content: 95 | * application/json: 96 | * schema: 97 | * type: object 98 | * $ref: '#/components/schemas/User' 99 | * security: 100 | * - bearerAuth: [] 101 | */ 102 | 103 | router 104 | .route("/:id") 105 | .get( 106 | authController.protect, 107 | authController.restrictTo("admin"), 108 | schemaValidator(checkUserId, "params"), 109 | userController.getUser 110 | ); 111 | 112 | 113 | /** 114 | * @swagger 115 | * /users: 116 | * post: 117 | * summary: User Creation 118 | * requestBody: 119 | * required: true 120 | * content: 121 | * application/json: 122 | * schema: 123 | * $ref: '#/components/schemas/CreateUser' 124 | * tags: [User] 125 | * responses: 126 | * 200: 127 | * description: User creation 128 | * content: 129 | * application/json: 130 | * schema: 131 | * type: object 132 | * $ref: '#/components/schemas/User' 133 | * security: 134 | * - bearerAuth: [] 135 | * 136 | */ 137 | 138 | router.post( 139 | "/", 140 | authController.protect, 141 | authController.restrictTo("admin"), 142 | schemaValidator(createUser), 143 | userController.createUser 144 | ); 145 | 146 | 147 | /** 148 | * @swagger 149 | * /users/{id}: 150 | * put: 151 | * summary: update one user by id 152 | * tags: [User] 153 | * parameters: 154 | * - in: path 155 | * name: id 156 | * requestBody: 157 | * required: true 158 | * content: 159 | * application/json: 160 | * schema: 161 | * $ref: '#/components/schemas/UpdateUser' 162 | * responses: 163 | * 200: 164 | * content: 165 | * application/json: 166 | * schema: 167 | * type: object 168 | * $ref: '#/components/schemas/User' 169 | * security: 170 | * - bearerAuth: [] 171 | */ 172 | 173 | router.put( 174 | "/:id", 175 | authController.protect, 176 | authController.restrictTo("admin"), 177 | schemaValidator(checkUserId, "params"), 178 | schemaValidator(updateUser), 179 | userController.updateUser 180 | ); 181 | 182 | 183 | /** 184 | * @swagger 185 | * /users/{id}: 186 | * delete: 187 | * summary: delete one user by id 188 | * tags: [User] 189 | * parameters: 190 | * - in: path 191 | * name: id 192 | * responses: 193 | * 200: 194 | * content: 195 | * application/json: 196 | * schema: 197 | * type: object 198 | * $ref: '#/components/schemas/User' 199 | * security: 200 | * - bearerAuth: [] 201 | */ 202 | 203 | router.delete( 204 | "/:id", 205 | authController.protect, 206 | authController.restrictTo("admin"), 207 | schemaValidator(checkUserId, "params"), 208 | userController.deleteUser 209 | ); 210 | 211 | 212 | 213 | module.exports = router; 214 | -------------------------------------------------------------------------------- /utils/apiFeatures.js: -------------------------------------------------------------------------------- 1 | class APIFeatures { 2 | constructor(query, queryString) { 3 | this.query = query; 4 | this.queryString = queryString; 5 | } 6 | 7 | filter() { 8 | const queryObj = { ...this.queryString }; 9 | const excludedFields = ['page', 'sort', 'limit', 'fields']; 10 | excludedFields.forEach(el => delete queryObj[el]); 11 | 12 | // 1B) Advanced filtering 13 | let queryStr = JSON.stringify(queryObj); 14 | 15 | queryStr = queryStr.replace(/\b(gte|gt|lte|lt)\b/g, match => `$${match}`); 16 | 17 | queryStr = JSON.parse(queryStr); 18 | if(Object.keys(queryStr).length){ 19 | let queryOption = Object.keys(queryStr).map((field) => ({ 20 | [field]: { $regex: queryStr[field], $options: 'i' }, 21 | })); 22 | 23 | this.query = this.query.find({ $or: queryOption }); 24 | 25 | } 26 | return this; 27 | 28 | } 29 | 30 | sort() { 31 | if (this.queryString.sort) { 32 | const sortBy = this.queryString.sort.split(',').join(' '); 33 | this.query = this.query.sort(sortBy); 34 | } else { 35 | this.query = this.query.sort('-createdAt'); 36 | } 37 | 38 | return this; 39 | } 40 | 41 | limitFields() { 42 | if (this.queryString.fields) { 43 | const fields = this.queryString.fields.split(',').join(' '); 44 | this.query = this.query.select(fields); 45 | } else { 46 | this.query = this.query.select('-__v'); 47 | } 48 | 49 | return this; 50 | } 51 | 52 | paginate() { 53 | const page = this.queryString.page * 1 || 1; 54 | const limit = this.queryString.limit * 1 || 100; 55 | const skip = (page - 1) * limit; 56 | 57 | this.query = this.query.skip(skip).limit(limit); 58 | 59 | return this; 60 | } 61 | 62 | search(searchFields) { 63 | if (this.queryString?.search) { 64 | const queryOption = searchFields.map((field) => ({ 65 | [field]: { $regex: this.queryString.search, $options: 'i' }, 66 | })); 67 | 68 | this.query = this.query.find({ $or: queryOption }); 69 | } 70 | return this; 71 | } 72 | } 73 | module.exports = APIFeatures; -------------------------------------------------------------------------------- /utils/catchAsync.js: -------------------------------------------------------------------------------- 1 | module.exports = fn => { 2 | return (req, res, next) => { 3 | fn(req, res, next).catch(next); 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /utils/email.js: -------------------------------------------------------------------------------- 1 | const nodemailer =require('nodemailer'); 2 | const { mailing } = require('../config'); 3 | 4 | const sendEmail = async options => { 5 | 6 | //let testAccount = await nodemailer.createTestAccount(); 7 | 8 | const transporter = nodemailer.createTransport({ 9 | host: mailing.host, 10 | port: mailing.port, 11 | auth:{ 12 | user: mailing.user, 13 | pass: mailing.pass 14 | } 15 | }) 16 | 17 | 18 | 19 | const mailOptions = { 20 | from: "", 21 | to:options.email, 22 | subject:options.subject, 23 | text:options.message 24 | } 25 | 26 | await transporter.sendMail(mailOptions) 27 | 28 | } 29 | 30 | module.exports = sendEmail; -------------------------------------------------------------------------------- /utils/seeder.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | const { connect } = require("../db/connectDB"); 3 | const mongoose = require('mongoose'); 4 | const bcryptjs = require('bcryptjs'); 5 | const { seeder } = require("./../config"); 6 | const userModel = require("./../db/models/userModel"); 7 | 8 | 9 | let seedDelete = async () => { 10 | const collections = mongoose.modelNames(); 11 | const deletedCollections = collections.map((collection) => 12 | mongoose.models[collection].deleteMany({}) 13 | ); 14 | await Promise.all(deletedCollections); 15 | console.log("Collections empty successfuly !"); 16 | } 17 | 18 | 19 | let seedAdmin = async () => { 20 | let admins = await userModel.find({ role: "admin"}); 21 | if(admins.length > 0){ 22 | console.log("Admin user exist"); 23 | } else { 24 | try{ 25 | let admin = { name: seeder.adminName, lastName: seeder.adminLastName, email: seeder.adminEmail, password : seeder.adminPass, passwordConfirm: seeder.adminPass, role: "admin", createdAt: new Date(), updatedAt: new Date() }; 26 | 27 | await userModel.create(admin); 28 | 29 | console.log("Admin user added sucessfuly !"); 30 | }catch(error){ 31 | console.log("error : ", error); 32 | } 33 | } 34 | 35 | } 36 | 37 | let seed = async() => { 38 | await connect(); 39 | await seedDelete(); 40 | await seedAdmin(); 41 | process.exit(1); 42 | } 43 | 44 | seed(); 45 | 46 | --------------------------------------------------------------------------------