├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── LICENSE ├── README.md ├── _env ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── config │ ├── appConfig.js │ └── environment.js ├── data │ └── models │ │ ├── index.js │ │ └── schemas │ │ ├── UsersSchema.js │ │ └── index.js ├── gql │ ├── auth │ │ ├── authValidations.js │ │ ├── jwt.js │ │ └── setContext.js │ ├── resolvers │ │ ├── auth.js │ │ ├── index.js │ │ └── users.js │ └── types │ │ ├── auth.js │ │ ├── index.js │ │ ├── shared.js │ │ └── users.js ├── helpers │ ├── getListOfIPV4Address.js │ ├── logger.js │ ├── requestDevLogger.js │ └── validations.js ├── public │ └── favicon.ico ├── routes │ └── routesManager.js └── server.js └── tests ├── package.test.js └── validations.test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "ecmaVersion": 2020, 10 | "sourceType": "module" 11 | }, 12 | "plugins": [ 13 | "@didaquis/no-lenght" 14 | ], 15 | "rules": { 16 | "@didaquis/no-lenght/no-lenght": "error", 17 | "indent": [ 18 | "error", 19 | "tab", 20 | { "SwitchCase": 1 } 21 | ], 22 | "linebreak-style": [ 23 | "error", 24 | "unix" 25 | ], 26 | "quotes": [ 27 | "error", 28 | "single" 29 | ], 30 | "semi": [ 31 | "error", 32 | "always" 33 | ], 34 | "no-console": "warn", 35 | "no-alert": "warn", 36 | "no-unused-vars": "warn", 37 | "keyword-spacing": ["error", { "before": true, "after": true }], 38 | "space-infix-ops": ["error", { "int32Hint": false }], 39 | "comma-spacing": ["error"], 40 | "arrow-spacing": ["error"], 41 | "semi-spacing": ["error"], 42 | "space-before-function-paren": ["error"], 43 | "no-multi-spaces": "error", 44 | "no-magic-numbers": ["warn", { "ignore": [0], "ignoreArrayIndexes": true }], 45 | "valid-typeof": "error", 46 | "object-curly-spacing": ["error", "always"], 47 | "curly": "error", 48 | "no-return-await": "error" 49 | } 50 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,macos,sublimetext,visualstudiocode 2 | # Edit at https://www.gitignore.io/?templates=node,macos,sublimetext,visualstudiocode 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Node ### 33 | # Logs 34 | logs 35 | *.log 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | 40 | # Runtime data 41 | pids 42 | *.pid 43 | *.seed 44 | *.pid.lock 45 | 46 | # Directory for instrumented libs generated by jscoverage/JSCover 47 | lib-cov 48 | 49 | # Coverage directory used by tools like istanbul 50 | coverage 51 | 52 | # nyc test coverage 53 | .nyc_output 54 | 55 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 56 | .grunt 57 | 58 | # Bower dependency directory (https://bower.io/) 59 | bower_components 60 | 61 | # node-waf configuration 62 | .lock-wscript 63 | 64 | # Compiled binary addons (https://nodejs.org/api/addons.html) 65 | build/Release 66 | 67 | # Dependency directories 68 | node_modules/ 69 | jspm_packages/ 70 | 71 | # TypeScript v1 declaration files 72 | typings/ 73 | 74 | # Optional npm cache directory 75 | .npm 76 | 77 | # Optional eslint cache 78 | .eslintcache 79 | 80 | # Optional REPL history 81 | .node_repl_history 82 | 83 | # Output of 'npm pack' 84 | *.tgz 85 | 86 | # Yarn Integrity file 87 | .yarn-integrity 88 | 89 | # dotenv environment variables file 90 | .env 91 | .env.test 92 | 93 | # parcel-bundler cache (https://parceljs.org/) 94 | .cache 95 | 96 | # next.js build output 97 | .next 98 | 99 | # nuxt.js build output 100 | .nuxt 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # Serverless directories 106 | .serverless/ 107 | 108 | # FuseBox cache 109 | .fusebox/ 110 | 111 | # DynamoDB Local files 112 | .dynamodb/ 113 | 114 | ### SublimeText ### 115 | # Cache files for Sublime Text 116 | *.tmlanguage.cache 117 | *.tmPreferences.cache 118 | *.stTheme.cache 119 | 120 | # Workspace files are user-specific 121 | *.sublime-workspace 122 | 123 | # Project files should be checked into the repository, unless a significant 124 | # proportion of contributors will probably not be using Sublime Text 125 | # *.sublime-project 126 | 127 | # SFTP configuration file 128 | sftp-config.json 129 | 130 | # Package control specific files 131 | Package Control.last-run 132 | Package Control.ca-list 133 | Package Control.ca-bundle 134 | Package Control.system-ca-bundle 135 | Package Control.cache/ 136 | Package Control.ca-certs/ 137 | Package Control.merged-ca-bundle 138 | Package Control.user-ca-bundle 139 | oscrypto-ca-bundle.crt 140 | bh_unicode_properties.cache 141 | 142 | # Sublime-github package stores a github token in this file 143 | # https://packagecontrol.io/packages/sublime-github 144 | GitHub.sublime-settings 145 | 146 | ### VisualStudioCode ### 147 | .vscode/* 148 | !.vscode/settings.json 149 | !.vscode/tasks.json 150 | !.vscode/launch.json 151 | !.vscode/extensions.json 152 | 153 | ### VisualStudioCode Patch ### 154 | # Ignore all local history of files 155 | .history 156 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run test 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.15 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present Dídac García and other contributors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚡️ Boilerplate: Backend with Node + GraphQL + MongoDB 2 | 3 | This project is an easy and fast way to start new projects in JavaScript. 4 | The main goal is to provide two repositories: one for the backend and one for the frontend application. 5 | 6 | This repository is for the backend and is designed to work with another boilerplate: [the frontend](https://github.com/didaquis/boilerplate-frontend-react-graphql-apollo) 7 | 8 | If you prefer it, you can use one of these boilerplates without using the other! Feel free to explore any ideas like developing your own backend in PHP or your frontend in Angular, for example. 9 | 10 | ### 🎁 What is included on the backend boilerplate? 11 | Technologies used are: Node.js + GraphQL + Apollo Server + Express + Mongoose. 12 | 13 | **✨ These are some of the highlights:** 14 | 15 | ✅ A server ready to use! 16 | ✅ Users can login and registrate 17 | ✅ You can add the 'administration' rol to some users 18 | ✅ You can set a limit of users registered 19 | ✅ Users data is stored on a database 20 | ✅ The Auth validations are made with JWT 21 | 22 | ### 📝 Backend Requirements 23 | * MongoDB 7.0 or higher 24 | * Node.js 18.15 or higher 25 | 26 | ### 📚 How to run the application? 27 | * Use the command: `npm install`. If you are deploying the app in production, it's better to use this command: `npm install --production` 28 | * Configure the application: 29 | * Duplicate the configuration file `_env` and rename it as `.env` 30 | * Edit the file `.env` 31 | * Then use: `npm run start`. 32 | * That's it! That was fast enough, right? 🚀 33 | 34 | **Do you need help with `.env` file?** 35 | 36 | Do not worry, I have written down some information for you. Here you have a guide: 37 | 38 | | Key | Description | 39 | |-----|-------------| 40 | | PORT | The port for running the backend | 41 | | ENVIRONMENT | The mode of execution of Node.js. Choose between: production or development | 42 | | LIMIT_USERS_REGISTERED | Maximum number of users allowed to register. Set the value to 0 to not use the limit | 43 | | MONGO_FORMAT_CONNECTION | The format of connection with MongoDB service. Choose between: standard or DNSseedlist | 44 | | MONGO_HOST | Set this value only if you are using the standard connection format. Host of MongoDB service | 45 | | MONGO_PORT | Set this value only if you are using the standard connection format. Port of MongoDB service | 46 | | MONGO_DB | Set this value only if you are using the standard connection format. The name of database | 47 | | MONGO_USER | Set this value only if you are using the standard connection format. User name | 48 | | MONGO_PASS | Set this value only if you are using the standard connection format. User password | 49 | | MONGO_DNS_SEEDLIST_CONNECTION | Set this value only if you are using the DNSseedlist connection format. It should be something like this: mongodb+srv://user:password@uri-and-options | 50 | | SECRET | JWT secret key. Remember not to share this key for security reasons | 51 | | DURATION | JWT duration of auth token | 52 | 53 | **❗️How can I configure a user to be an administrator?** 54 | 55 | To make a user an administrator you must access to the database and search its registry. You will see a Boolean who allows the user to have the role of the administrator. Set it to 'true' and in their next authentication that user will have administrator permissions. 56 | 57 | ### 💻 Tricks for development 58 | * Run app in dev mode: `npm run dev` 59 | * Run the linter: `npm run lint` 60 | * Run the tests: `npm run test` or `npm run test:watch` 61 | * Delete all log files: `npm run clean` 62 | 63 | ### Would you like to contribute to this project? 64 | It would be great to receive your help. ♥️ 65 | 66 | You can collaborate in multiple ways, of course. Let me give you some ideas: 67 | * Help me with this documentation. If you think something it's not clear, open an issue and talk to me! 68 | * Share this project, mark it as a favorite (⭐️) or make some suggestions 69 | -------------------------------------------------------------------------------- /_env: -------------------------------------------------------------------------------- 1 | # Server port configuration: 2 | PORT=4000 3 | 4 | # Environment (choose between: "production" or "development"): 5 | ENVIRONMENT=development 6 | 7 | # You can set a limit of users allowed to register. Set the value to 0 to not use the limit 8 | LIMIT_USERS_REGISTERED=0 9 | 10 | # Database format connection (choose between: "standard" or "DNSseedlist"): 11 | MONGO_FORMAT_CONNECTION=DNSseedlist 12 | 13 | # MongoDB: Standard Connection String Format 14 | MONGO_HOST=127.0.0.1 15 | MONGO_PORT=27017 16 | MONGO_DB=boilerplate_database 17 | MONGO_USER= 18 | MONGO_PASS= 19 | 20 | # MongoDB: DNS Seedlist Connection Format 21 | MONGO_DNS_SEEDLIST_CONNECTION=mongodb+srv://:@ 22 | 23 | # JSONWEBTOKEN: 24 | SECRET=yoursecret 25 | DURATION=2h -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | transform: {} 3 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate-backend-node-graphql-mongodb", 3 | "version": "2.0.3", 4 | "description": "boilerplate-backend-node-graphql-mongodb", 5 | "main": "src/server.js", 6 | "engines": { 7 | "node": "^18.15" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "start": "node .", 12 | "dev": "nodemon .", 13 | "test": "node --experimental-vm-modules node_modules/.bin/jest", 14 | "test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watchAll", 15 | "lint": "eslint --ext .js .", 16 | "clean": "rimraf logs", 17 | "prepare": "husky install" 18 | }, 19 | "keywords": [ 20 | "boilerplate", 21 | "GraphQL", 22 | "Apollo", 23 | "Node.js", 24 | "MongoDB", 25 | "server" 26 | ], 27 | "author": "Dídac García (https://didaquis.github.io/)", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@didaquis/eslint-plugin-no-lenght": "1.1.2", 31 | "eslint": "8.37.0", 32 | "husky": "8.0.3", 33 | "jest": "29.5.0", 34 | "jsonfile": "6.1.0", 35 | "nodemon": "2.0.22", 36 | "rimraf": "4.4.1" 37 | }, 38 | "dependencies": { 39 | "@graphql-tools/load-files": "6.6.1", 40 | "@graphql-tools/merge": "8.4.0", 41 | "apollo-server-express": "3.12.0", 42 | "bcrypt": "5.1.0", 43 | "cors": "2.8.5", 44 | "dotenv": "16.0.3", 45 | "express": "4.18.2", 46 | "graphql": "16.6.0", 47 | "helmet": "6.0.1", 48 | "jsonwebtoken": "9.0.0", 49 | "lodash.merge": "4.6.2", 50 | "log4js": "6.9.1", 51 | "mongoose": "7.6.4", 52 | "serve-favicon": "2.5.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/config/appConfig.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config(); 3 | 4 | import { ENVIRONMENT } from './environment.js'; 5 | 6 | 7 | /* Home doc */ 8 | /** 9 | * @file Environment variables configuration for the application 10 | * @see module:appConfig 11 | */ 12 | 13 | /* Module doc */ 14 | /** 15 | * Environment variables configuration for the application 16 | * @module appConfig 17 | */ 18 | 19 | const serverPortByDefault = 4000; 20 | const limitOfUsersRegistered = 0; /* Set the value to 0 to not use the limit. Remember put the same value on the environment variables */ 21 | 22 | /** 23 | * Environment variables configuration 24 | * @readonly 25 | * @type {Object} 26 | * @property {string} formatConnection - The format of connection with MongoDB service 27 | * @property {string} mongoDNSseedlist - The DNSseedlist connection format 28 | * @property {string} dbHost - Host of the database 29 | * @property {string} dbPort - Port of the database 30 | * @property {string} database - Name of the database 31 | * @property {string} mongoUser - Username of MongoDB 32 | * @property {string} mongoPass - Password of MongoDB 33 | * @property {string} environment - Application execution environment 34 | * @property {number} port - The port for running this application 35 | */ 36 | export const environmentVariablesConfig = Object.freeze({ 37 | formatConnection: process.env.MONGO_FORMAT_CONNECTION || 'standard', 38 | mongoDNSseedlist: process.env.MONGO_DNS_SEEDLIST_CONNECTION || '', 39 | dbHost: process.env.MONGO_HOST || '127.0.0.1', 40 | dbPort: process.env.MONGO_PORT || '27017', 41 | database: process.env.MONGO_DB || 'boilerplate_database', 42 | mongoUser: process.env.MONGO_USER || '', 43 | mongoPass: process.env.MONGO_PASS || '', 44 | environment: (process.env.ENVIRONMENT === ENVIRONMENT.DEVELOPMENT) ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION, 45 | port: Number(process.env.PORT) || serverPortByDefault 46 | }); 47 | 48 | /** 49 | * Security variables configuration 50 | * @readonly 51 | * @type {Object} 52 | * @property {string} secret - Secret key for authentication 53 | * @property {string} timeExpiration - Expiration time for authentication tokens 54 | */ 55 | export const securityVariablesConfig = Object.freeze({ 56 | secret: process.env.SECRET || 'yoursecret', 57 | timeExpiration: process.env.DURATION || '2h' 58 | }); 59 | 60 | /** 61 | * Global variables configuration 62 | * @readonly 63 | * @type {Object} 64 | * @property {number} limitOfUsersRegistered - The maximum number of users that can register 65 | */ 66 | export const globalVariablesConfig = Object.freeze({ 67 | limitOfUsersRegistered: Number(process.env.LIMIT_USERS_REGISTERED) || limitOfUsersRegistered 68 | }); 69 | -------------------------------------------------------------------------------- /src/config/environment.js: -------------------------------------------------------------------------------- 1 | /* Home doc */ 2 | /** 3 | * @file Environment options available for the application 4 | * @see module:environment 5 | */ 6 | 7 | /* Module doc */ 8 | /** 9 | * Environment options available for the application 10 | * @module environment 11 | */ 12 | 13 | 14 | /** 15 | * Constant object representing the application environment 16 | * @readonly 17 | * @enum {string} 18 | * @property {string} DEVELOPMENT - Development environment 19 | * @property {string} PRODUCTION - Production environment 20 | */ 21 | export const ENVIRONMENT = Object.freeze({ 22 | DEVELOPMENT: 'development', 23 | PRODUCTION: 'production' 24 | }); -------------------------------------------------------------------------------- /src/data/models/index.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | import { UsersSchema } from './schemas/index.js'; 4 | 5 | export const models = { 6 | Users: mongoose.model('users', UsersSchema), 7 | }; 8 | -------------------------------------------------------------------------------- /src/data/models/schemas/UsersSchema.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import bcrypt from 'bcrypt'; 3 | import { randomUUID } from 'crypto'; 4 | 5 | const Schema = mongoose.Schema; 6 | 7 | /** 8 | * Users schema 9 | * @constructor Users model constructor 10 | * @classdesc User have interesting properties. Some of them are isAdmin (false by default), isActive (true by default. Useful for removing login permission to the registered users), uuid (random and unique token. Created to provided a random identifier token for every user different than _id native MongoDB value) 11 | */ 12 | const UsersSchema = new Schema({ 13 | email: { 14 | type: String, 15 | required: true, 16 | unique: true, 17 | trim: true, 18 | lowercase: true 19 | }, 20 | password: { 21 | type: String, 22 | required: true 23 | }, 24 | isAdmin: { 25 | type: Boolean, 26 | required: true, 27 | default: false 28 | }, 29 | isActive: { 30 | type: Boolean, 31 | required: true, 32 | default: true 33 | }, 34 | uuid: { 35 | type: String, 36 | required: true, 37 | unique: true, 38 | default: randomUUID 39 | }, 40 | registrationDate: { 41 | type: Date, 42 | required: true, 43 | default: Date.now 44 | }, 45 | lastLogin: { 46 | type: Date, 47 | required: true, 48 | default: Date.now 49 | } 50 | }); 51 | 52 | /** 53 | * Hash the password of user before save on database 54 | */ 55 | UsersSchema.pre('save', function (next) { 56 | if (!this.isModified('password')) { 57 | return next(); 58 | } 59 | bcrypt.genSalt((err, salt) => { 60 | if (err) { 61 | return next(err); 62 | } 63 | bcrypt.hash(this.password, salt, (err, hash) => { 64 | if (err) { 65 | return next(err); 66 | } 67 | this.password = hash; 68 | next(); 69 | }); 70 | }); 71 | }); 72 | 73 | export { UsersSchema }; 74 | -------------------------------------------------------------------------------- /src/data/models/schemas/index.js: -------------------------------------------------------------------------------- 1 | import { UsersSchema } from './UsersSchema.js'; 2 | 3 | export { UsersSchema }; -------------------------------------------------------------------------------- /src/gql/auth/authValidations.js: -------------------------------------------------------------------------------- 1 | import { AuthenticationError, ForbiddenError, ValidationError } from 'apollo-server-express'; 2 | import { models } from '../../data/models/index.js'; 3 | import { globalVariablesConfig } from '../../config/appConfig.js'; 4 | 5 | /** 6 | * Auth validations repository 7 | * @typedef {Object} 8 | */ 9 | export const authValidations = { 10 | /** 11 | * Check if the maximum limit of users has been reached. If limit is reached, it throws an error. 12 | * @param {number} numberOfCurrentlyUsersRegistered - The number of users currently registered in the service 13 | */ 14 | ensureLimitOfUsersIsNotReached: (numberOfCurrentlyUsersRegistered) => { 15 | const usersLimit = globalVariablesConfig.limitOfUsersRegistered; 16 | if (usersLimit === 0) { 17 | return; 18 | } 19 | 20 | if (numberOfCurrentlyUsersRegistered >= usersLimit) { 21 | throw new ValidationError('The maximum number of users allowed has been reached. You must contact the administrator of the service in order to register'); 22 | } 23 | }, 24 | 25 | /** 26 | * Check if in Apollo Server context contains a logged user. If user is not in context, it throws an error 27 | * @param {Object} context - The context object of Apollo Server 28 | * @param {Object} [context.user] - The context object data: user data 29 | */ 30 | ensureThatUserIsLogged: (context) => { 31 | if (!context.user) { 32 | throw new AuthenticationError('You must be logged in to perform this action'); 33 | } 34 | }, 35 | 36 | /** 37 | * Check if in Apollo Server context contains an user and is an administrator. If user is not in context or user is not an administrator it throws an error 38 | * @param {Object} context - The context object of Apollo Server 39 | * @param {Object} [context.user] - The context object data: user data 40 | * @param {boolean} [context.user.isAdmin] - The context object data: user data role information 41 | */ 42 | ensureThatUserIsAdministrator: (context) => { 43 | if (!context.user || !context.user.isAdmin) { 44 | throw new ForbiddenError('You must be an administrator to perform this action'); 45 | } 46 | }, 47 | 48 | /** 49 | * Uses the information in the Apollo Server context to retrieve the user's data from the database. If user does not exist, it throws an error. 50 | * @async 51 | * @param {Object} context - The context object of Apollo Server 52 | * @param {Object} [context.user] - The context object data: user data 53 | * @returns {User} 54 | */ 55 | getUser: async (context) => { 56 | if (!context.user) { 57 | return null; 58 | } 59 | 60 | const userUUID = context.user.uuid || null; 61 | const user = await models.Users.findOne({ uuid: userUUID }).lean(); 62 | if (!user) { 63 | throw new AuthenticationError('You must be logged in to perform this action'); 64 | } 65 | 66 | return user; 67 | }, 68 | }; -------------------------------------------------------------------------------- /src/gql/auth/jwt.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | import { securityVariablesConfig } from '../../config/appConfig.js'; 4 | 5 | /** 6 | * Create a new JSON Web Token 7 | * @param {string} email - User email 8 | * @param {boolean} isAdmin - If user is admin or not 9 | * @param {boolean} isActive - If user is active or not 10 | * @param {string} uuid - An uuid token 11 | * @returns {string} - Json Web Token 12 | */ 13 | export const createAuthToken = (email, isAdmin, isActive, uuid) => { 14 | return jwt.sign({ email, isAdmin, isActive, uuid }, securityVariablesConfig.secret, { expiresIn: securityVariablesConfig.timeExpiration }); 15 | }; 16 | 17 | /** 18 | * Validate an existing JSON Web Token and retrieve data from payload 19 | * @param {string} token - A token 20 | * @returns {Object} - User data retrieved from payload 21 | */ 22 | export const validateAuthToken = async (token) => { 23 | const user = await jwt.verify(token, securityVariablesConfig.secret); 24 | return user; 25 | }; 26 | -------------------------------------------------------------------------------- /src/gql/auth/setContext.js: -------------------------------------------------------------------------------- 1 | import { validateAuthToken, createAuthToken } from './jwt.js'; 2 | import { environmentVariablesConfig } from '../../config/appConfig.js'; 3 | import { authValidations } from '../auth/authValidations.js'; 4 | import { ENVIRONMENT } from '../../config/environment.js'; 5 | import { logger } from '../../helpers/logger.js'; 6 | import { models } from '../../data/models/index.js'; 7 | 8 | /** 9 | * Context function from Apollo Server 10 | */ 11 | export const setContext = async ({ req }) => { 12 | const context = { 13 | di: { 14 | model: { 15 | ...models 16 | }, 17 | authValidation: { 18 | ...authValidations 19 | }, 20 | jwt: { 21 | createAuthToken: createAuthToken 22 | } 23 | } 24 | }; 25 | 26 | let token = req.headers['authorization']; 27 | 28 | if (token && typeof token === 'string') { 29 | try { 30 | const authenticationScheme = 'Bearer '; 31 | if (token.startsWith(authenticationScheme)) { 32 | token = token.slice(authenticationScheme.length, token.length); 33 | } 34 | const user = await validateAuthToken(token); 35 | context.user = user; // Add to Apollo Server context the user who is doing the request if auth token is provided and it's a valid token 36 | } catch (error) { 37 | if (environmentVariablesConfig.environment !== ENVIRONMENT.PRODUCTION) { 38 | logger.debug(error.message); 39 | } 40 | } 41 | } 42 | 43 | return context; 44 | }; 45 | -------------------------------------------------------------------------------- /src/gql/resolvers/auth.js: -------------------------------------------------------------------------------- 1 | import { UserInputError } from 'apollo-server-express'; 2 | import bcrypt from 'bcrypt'; 3 | 4 | import { isValidEmail, isStrongPassword } from '../../helpers/validations.js'; 5 | 6 | /** 7 | * All resolvers related to auth 8 | * @typedef {Object} 9 | */ 10 | export default { 11 | Query: { 12 | }, 13 | Mutation: { 14 | /** 15 | * It allows to users to register as long as the limit of allowed users has not been reached 16 | */ 17 | registerUser: async (parent, { email, password }, context) => { 18 | if (!email || !password) { 19 | throw new UserInputError('Data provided is not valid'); 20 | } 21 | 22 | if (!isValidEmail(email)) { 23 | throw new UserInputError('The email is not valid'); 24 | } 25 | 26 | if (!isStrongPassword(password)) { 27 | throw new UserInputError('The password is not secure enough'); 28 | } 29 | 30 | const registeredUsersCount = await context.di.model.Users.find().estimatedDocumentCount(); 31 | 32 | context.di.authValidation.ensureLimitOfUsersIsNotReached(registeredUsersCount); 33 | 34 | const isAnEmailAlreadyRegistered = await context.di.model.Users.findOne({ email }).lean(); 35 | 36 | if (isAnEmailAlreadyRegistered) { 37 | throw new UserInputError('Data provided is not valid'); 38 | } 39 | 40 | await new context.di.model.Users({ email, password }).save(); 41 | 42 | const user = await context.di.model.Users.findOne({ email }).lean(); 43 | 44 | return { 45 | token: context.di.jwt.createAuthToken(user.email, user.isAdmin, user.isActive, user.uuid) 46 | }; 47 | }, 48 | /** 49 | * It allows users to authenticate. Users with property isActive with value false are not allowed to authenticate. When an user authenticates the value of lastLogin will be updated 50 | */ 51 | authUser: async (parent, { email, password }, context) => { 52 | if (!email || !password) { 53 | throw new UserInputError('Invalid credentials'); 54 | } 55 | 56 | const user = await context.di.model.Users.findOne({ email, isActive: true }).lean(); 57 | 58 | if (!user) { 59 | throw new UserInputError('User not found or login not allowed'); 60 | } 61 | 62 | const isCorrectPassword = await bcrypt.compare(password, user.password); 63 | 64 | if (!isCorrectPassword) { 65 | throw new UserInputError('Invalid credentials'); 66 | } 67 | 68 | await context.di.model.Users.findOneAndUpdate({ email }, { lastLogin: new Date().toISOString() }, { new: true }).lean(); 69 | 70 | return { 71 | token: context.di.jwt.createAuthToken(user.email, user.isAdmin, user.isActive, user.uuid) 72 | }; 73 | }, 74 | /** 75 | * It allows to user to delete their account permanently (this action does not delete the records associated with the user, it only deletes their user account) 76 | */ 77 | deleteMyUserAccount: async (parent, args, context) => { 78 | context.di.authValidation.ensureThatUserIsLogged(context); 79 | 80 | const user = await context.di.authValidation.getUser(context); 81 | 82 | return context.di.model.Users.deleteOne({ uuid: user.uuid }); 83 | } 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /src/gql/resolvers/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'lodash.merge'; 2 | 3 | import users from './users.js'; 4 | import auth from './auth.js'; 5 | 6 | export const resolvers = merge( 7 | users, 8 | auth 9 | ); 10 | -------------------------------------------------------------------------------- /src/gql/resolvers/users.js: -------------------------------------------------------------------------------- 1 | /** 2 | * All resolvers related to users 3 | * @typedef {Object} 4 | */ 5 | export default { 6 | Query: { 7 | /** 8 | * It allows to administrators users to list all users registered 9 | */ 10 | listAllUsers: async (parent, args, context) => { 11 | context.di.authValidation.ensureThatUserIsLogged(context); 12 | 13 | context.di.authValidation.ensureThatUserIsAdministrator(context); 14 | 15 | const sortCriteria = { isAdmin: 'desc', registrationDate: 'asc' }; 16 | return context.di.model.Users.find().sort(sortCriteria).lean(); 17 | } 18 | }, 19 | Mutation: { 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/gql/types/auth.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default /* GraphQL */ gql` 4 | type Token { 5 | token: String 6 | } 7 | 8 | type Mutation { 9 | """ It allows users to register """ 10 | registerUser(email: String!, password: String!): Token 11 | 12 | """ It allows users to authenticate """ 13 | authUser(email: String!, password: String!): Token 14 | 15 | """ It allows to user to delete their account permanently """ 16 | deleteMyUserAccount: DeleteResult 17 | } 18 | `; -------------------------------------------------------------------------------- /src/gql/types/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath, pathToFileURL } from 'url'; 3 | 4 | import { loadFiles } from '@graphql-tools/load-files'; 5 | import { mergeTypeDefs } from '@graphql-tools/merge'; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | 9 | export const initTypeDefinition = async () => { 10 | const resolversArray = await loadFiles(__dirname, { 11 | extensions: ['js'], 12 | ignoreIndex: true, 13 | requireMethod: (path) => { 14 | return import(pathToFileURL(path)); 15 | }, 16 | }); 17 | 18 | return mergeTypeDefs(resolversArray); 19 | }; 20 | -------------------------------------------------------------------------------- /src/gql/types/shared.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default /* GraphQL */ gql` 4 | type DeleteResult { 5 | deletedCount: Int! 6 | } 7 | `; -------------------------------------------------------------------------------- /src/gql/types/users.js: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | 3 | export default /* GraphQL */ gql` 4 | type User { 5 | email: String 6 | isAdmin: Boolean 7 | isActive: Boolean 8 | uuid: String 9 | registrationDate: String 10 | lastLogin: String 11 | } 12 | 13 | type Query { 14 | """ Get list of all users registered on database """ 15 | listAllUsers: [User] 16 | } 17 | `; -------------------------------------------------------------------------------- /src/helpers/getListOfIPV4Address.js: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | /** 4 | * Get all IP address of the server 5 | * @param {Object|undefined} [{}] - An object. 6 | * @param {boolean} [obj.skipLocalhost=false] - Determines if the localhost address is returned in the result list 7 | * @returns {Array} Array of IPs 8 | */ 9 | export const getListOfIPV4Address = ({ skipLocalhost = false } = {}) => { 10 | const ifaces = os.networkInterfaces(); 11 | 12 | let result = []; 13 | 14 | Object.keys(ifaces).forEach(function (ifname) { 15 | ifaces[ifname].forEach(function (iface) { 16 | // skip non-ipv4 addresses 17 | if ('IPv4' !== iface.family) { 18 | return; 19 | } 20 | 21 | if (skipLocalhost) { 22 | // skip over internal (i.e. 127.0.0.1) 23 | if (iface.internal !== false) { 24 | return; 25 | } 26 | } 27 | 28 | result.push(iface.address); 29 | }); 30 | }); 31 | 32 | return result; 33 | }; 34 | -------------------------------------------------------------------------------- /src/helpers/logger.js: -------------------------------------------------------------------------------- 1 | /* Home doc */ 2 | /** 3 | * @file Configuration settings for logger module 4 | * @see module:logger 5 | */ 6 | 7 | /* Module doc */ 8 | /** 9 | * Configuration settings for logger module 10 | * @module logger 11 | */ 12 | 13 | import log4js from 'log4js'; 14 | 15 | /** 16 | * log4js configuration data 17 | */ 18 | log4js.configure({ 19 | appenders: { 20 | out:{ type: 'stdout' }, 21 | trace: { type: 'file', filename: 'logs/application_trace.log', maxLogSize: 204800, backups: 3, keepFileExt: true }, 22 | debug: { type: 'file', filename: 'logs/application_debug.log', maxLogSize: 204800, backups: 3, keepFileExt: true }, 23 | info: { type: 'file', filename: 'logs/application.log', maxLogSize: 204800, backups: 3, keepFileExt: true }, 24 | error: { type: 'file', filename: 'logs/application_error.log', maxLogSize: 204800, backups: 3, keepFileExt: true }, 25 | trace_filter: { type: 'logLevelFilter', appender: 'trace', level: 'trace', maxLevel: 'trace' }, 26 | debug_filter: { type: 'logLevelFilter', appender: 'debug', level: 'debug', maxLevel: 'debug' }, 27 | info_filter: { type:'logLevelFilter', appender: 'info', level: 'info' }, 28 | error_filter: { type:'logLevelFilter', appender: 'error', level: 'error' } 29 | }, 30 | categories: { 31 | default: { appenders: [ 'trace_filter', 'debug_filter', 'info_filter', 'error_filter', 'out'], level: 'all' }, 32 | } 33 | }); 34 | 35 | /** 36 | * Logger object that provides the methods to logger data (all logs are printed in console and in logs file). 37 | * @async 38 | * @example Usage of logger: 39 | * logger.trace('trace'); // Log file: application_trace.log 40 | * logger.debug('debug'); // Log file: application_debug.log 41 | * logger.info('info'); // Log file: application.log 42 | * logger.warn('warn'); // Log file: application.log 43 | * logger.error('error'); // Log file: application_error.log and application.log 44 | * logger.fatal('fatal'); // Log file: application_error.log and application.log 45 | */ 46 | export const logger = log4js.getLogger(); 47 | 48 | export const endLogger = log4js.shutdown; 49 | -------------------------------------------------------------------------------- /src/helpers/requestDevLogger.js: -------------------------------------------------------------------------------- 1 | import { logger } from './logger.js'; 2 | 3 | export const requestDevLogger = { 4 | // Fires whenever a GraphQL request is received from a client 5 | requestDidStart (requestContext) { 6 | 7 | /* List of regex to filter queries from logger */ 8 | const excludeThisQueryFromLogger = [/query IntrospectionQuery/]; 9 | 10 | const avoidLog = excludeThisQueryFromLogger.some(excludedQuery => requestContext.request.query.match(excludedQuery)); 11 | 12 | if (avoidLog) { 13 | return; 14 | } 15 | 16 | logger.debug('Query:'); 17 | logger.debug(`\n${requestContext.request.query}`); 18 | 19 | logger.debug('Variables:'); 20 | logger.debug(requestContext.request.variables); 21 | 22 | return { 23 | // Fires whenever Apollo Server is about to send a response for a GraphQL operation 24 | willSendResponse (requestContext) { 25 | logger.debug('Response data:'); 26 | logger.debug(requestContext.response.data); 27 | 28 | if (requestContext.response.errors) { 29 | logger.debug(`Response errors (number of errors: ${requestContext.response.errors.length}):`); 30 | requestContext.response.errors.forEach(err => logger.debug(err.message)); 31 | } 32 | } 33 | }; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/helpers/validations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if email is valid 3 | * @param {string} email 4 | * @returns {boolean} 5 | */ 6 | export const isValidEmail = (email) => { 7 | if (!email) { 8 | return false; 9 | } 10 | const emailValidPattern = new RegExp(/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/); 11 | return emailValidPattern.test(email); 12 | }; 13 | 14 | /** 15 | * Check if password is secure. Rules: At least 8 characters. It must contain numbers, lowercase letters and uppercase letters. The spaces are not allowed. Only english characters are allowed. This characters are not allowed: { } ( ) | ~ € ¿ ¬ 16 | * @param {string} password 17 | * @returns {boolean} 18 | */ 19 | export const isStrongPassword = (password) => { 20 | if (!password) { 21 | return false; 22 | } 23 | const passwordValidPattern = new RegExp(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[0-9a-zA-Z!*^?+-_@#$%&]{8,}$/); 24 | return passwordValidPattern.test(password); 25 | }; 26 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didaquis/boilerplate-backend-node-graphql-mongodb/d20e730336963b6744349a087f612060cfff8e74/src/public/favicon.ico -------------------------------------------------------------------------------- /src/routes/routesManager.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | const routesManager = Router(); 3 | 4 | /** 5 | * Example of route 6 | */ 7 | routesManager.get('/', (req, res) => { 8 | const status = 200; 9 | res.status(status).end(); 10 | }); 11 | 12 | 13 | export default routesManager; 14 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import express from 'express'; 3 | import helmet from 'helmet'; 4 | import favicon from 'serve-favicon'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | import cors from 'cors'; 8 | import { ApolloServerPluginLandingPageGraphQLPlayground, ApolloServerPluginLandingPageDisabled } from 'apollo-server-core'; 9 | import { ApolloServer } from 'apollo-server-express'; 10 | import { UserInputError } from 'apollo-server-errors'; 11 | 12 | import { ENVIRONMENT } from './config/environment.js'; 13 | import { environmentVariablesConfig } from './config/appConfig.js'; 14 | import { logger, endLogger } from './helpers/logger.js'; 15 | import { requestDevLogger } from './helpers/requestDevLogger.js'; 16 | import { setContext } from './gql/auth/setContext.js'; 17 | import { initTypeDefinition } from './gql/types/index.js'; 18 | import { resolvers } from './gql/resolvers/index.js'; 19 | import { getListOfIPV4Address } from './helpers/getListOfIPV4Address.js'; 20 | import routesManager from './routes/routesManager.js'; 21 | 22 | 23 | mongoose.set('strictQuery', true); 24 | 25 | if (environmentVariablesConfig.formatConnection === 'DNSseedlist' && environmentVariablesConfig.mongoDNSseedlist !== '') { 26 | mongoose.connect(environmentVariablesConfig.mongoDNSseedlist); 27 | } else { 28 | if (environmentVariablesConfig.mongoUser !== '' && environmentVariablesConfig.mongoPass !== '') { 29 | mongoose.connect(`mongodb://${environmentVariablesConfig.mongoUser}:${environmentVariablesConfig.mongoPass}@${environmentVariablesConfig.dbHost}:${environmentVariablesConfig.dbPort}/${environmentVariablesConfig.database}`); 30 | } else { 31 | mongoose.connect(`mongodb://${environmentVariablesConfig.dbHost}:${environmentVariablesConfig.dbPort}/${environmentVariablesConfig.database}`); 32 | } 33 | } 34 | 35 | const db = mongoose.connection; 36 | db.on('error', (err) => { 37 | logger.error(`Connection error with database. ${err}`); 38 | }); 39 | 40 | db.once('open', () => { 41 | if (environmentVariablesConfig.environment !== ENVIRONMENT.DEVELOPMENT) { 42 | logger.info(`Connected with MongoDB service (${ENVIRONMENT.PRODUCTION} mode)`); 43 | } else { 44 | if (environmentVariablesConfig.formatConnection === 'DNSseedlist' && environmentVariablesConfig.mongoDNSseedlist !== '') { 45 | logger.info(`Connected with MongoDB service at "${environmentVariablesConfig.mongoDNSseedlist}" using database "${environmentVariablesConfig.database}" (${ENVIRONMENT.DEVELOPMENT} mode)`); 46 | } else { 47 | logger.info(`Connected with MongoDB service at "${environmentVariablesConfig.dbHost}" in port "${environmentVariablesConfig.dbPort}" using database "${environmentVariablesConfig.database}" (${ENVIRONMENT.DEVELOPMENT} mode)`); 48 | } 49 | } 50 | 51 | initApplication(); 52 | }); 53 | 54 | const initApplication = async () => { 55 | const app = express(); 56 | if (environmentVariablesConfig.environment === ENVIRONMENT.PRODUCTION) { 57 | app.use(helmet()); 58 | } else { 59 | // Allow GraphQL Playground on development environments 60 | app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false })); 61 | } 62 | app.use(cors({ credentials: true })); 63 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 64 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 65 | app.use('', routesManager); 66 | 67 | const typeDefs = await initTypeDefinition(); 68 | 69 | const server = new ApolloServer({ 70 | typeDefs, 71 | resolvers, 72 | context: setContext, 73 | introspection: (environmentVariablesConfig.environment === ENVIRONMENT.PRODUCTION) ? false : true, // Set to "true" only in development mode 74 | plugins: (environmentVariablesConfig.environment === ENVIRONMENT.PRODUCTION) ? [ApolloServerPluginLandingPageDisabled()] : [requestDevLogger, ApolloServerPluginLandingPageGraphQLPlayground()], // Log all querys and their responses. Show playground (do not use in production) 75 | formatError (error) { 76 | if ( !(error.originalError instanceof UserInputError) ) { 77 | logger.error(error.message); 78 | } 79 | 80 | return error; 81 | }, 82 | }); 83 | 84 | await server.start(); 85 | 86 | server.applyMiddleware({ app }); 87 | 88 | app.use((req, res) => { 89 | res.status(404).send('404'); // eslint-disable-line no-magic-numbers 90 | }); 91 | 92 | app.listen(environmentVariablesConfig.port, () => { 93 | getListOfIPV4Address().forEach(ip => { 94 | logger.info(`Application running on: http://${ip}:${environmentVariablesConfig.port}`); 95 | if (environmentVariablesConfig.environment !== ENVIRONMENT.PRODUCTION) { 96 | logger.info(`GraphQL Playground running on: http://${ip}:${environmentVariablesConfig.port}${server.graphqlPath}`); 97 | } 98 | }); 99 | }); 100 | 101 | // Manage application shutdown 102 | process.on('SIGINT', () => { 103 | logger.info('Stopping application...'); 104 | endLogger(); 105 | process.exit(); 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /tests/package.test.js: -------------------------------------------------------------------------------- 1 | import jsonfile from 'jsonfile'; 2 | 3 | let packageJSONData; 4 | 5 | describe('package.json file', () => { 6 | 7 | beforeAll(() => { 8 | const file = './package.json'; 9 | packageJSONData = jsonfile.readFileSync(file); 10 | }); 11 | 12 | test('Should have all dependencies with semver version fixed', () => { 13 | if (packageJSONData.dependencies) { 14 | const validPattern = /^(\d+\.)(\d+\.)(\d+)$/; 15 | const regex = RegExp(validPattern); 16 | 17 | let allDependenciesAreFixed = true; 18 | for (let key in packageJSONData.dependencies){ 19 | if (Object.prototype.hasOwnProperty.call(packageJSONData.dependencies, key)) { 20 | if (!regex.test(packageJSONData.dependencies[key])) { 21 | allDependenciesAreFixed = false; 22 | } 23 | } 24 | } 25 | 26 | expect(allDependenciesAreFixed).toBe(true); 27 | } 28 | }); 29 | 30 | test('Should have all devDependencies with semver version fixed', () => { 31 | if (packageJSONData.devDependencies) { 32 | const validPattern = /^(\d+\.)(\d+\.)(\d+)/; 33 | const regex = RegExp(validPattern); 34 | 35 | let allDevDependenciesAreFixed = true; 36 | 37 | for (let key in packageJSONData.devDependencies){ 38 | if (Object.prototype.hasOwnProperty.call(packageJSONData.devDependencies, key)) { 39 | if (!regex.test(packageJSONData.devDependencies[key])) { 40 | allDevDependenciesAreFixed = false; 41 | } 42 | } 43 | } 44 | 45 | expect(allDevDependenciesAreFixed).toBe(true); 46 | } 47 | }); 48 | }); -------------------------------------------------------------------------------- /tests/validations.test.js: -------------------------------------------------------------------------------- 1 | import { isValidEmail, isStrongPassword } from '../src/helpers/validations.js'; 2 | 3 | describe('validations', () => { 4 | 5 | describe('isValidEmail', () => { 6 | test('Should return false if no receive params', () => { 7 | expect(isValidEmail()).toBe(false); 8 | }); 9 | 10 | test('Should return false if no receive a valid data', () => { 11 | const fakeData = ['foo', '', 'foo@@foo.foo', 'bar@bar..com', 'biz@biz.', '@foo.mail']; 12 | 13 | fakeData.forEach( data => { 14 | expect(isValidEmail(data)).toBe(false); 15 | }); 16 | }); 17 | 18 | test('Should return true if receive a valid data', () => { 19 | const fakeData = ['maria@mail.com', 'john.doe@gmail.com', 'santi72@hotmail.es', 'foo_@yourcompany.it']; 20 | 21 | fakeData.forEach( data => { 22 | expect(isValidEmail(data)).toBe(true); 23 | }); 24 | }); 25 | }); 26 | 27 | describe('isStrongPassword', () => { 28 | test('Should return false if no receive params', () => { 29 | expect(isStrongPassword()).toBe(false); 30 | }); 31 | 32 | test('Should return false if no receive a valid data', () => { 33 | const fakeData = ['foo', '', '123', 'patata', 'PATATA.', '*', 'Pa1*', 'A2*aaaa', 'abcdefghijkl', 'My-Password', '82569285927659']; 34 | 35 | fakeData.forEach( data => { 36 | expect(isStrongPassword(data)).toBe(false); 37 | }); 38 | }); 39 | 40 | test('Should return true if receive a valid data', () => { 41 | const fakeData = ['abcABC123', 'Pat*ta_72', 'Lol#u&8$', 'My-P4ssw0rd', 'w!w_393_MiM', '*ZGw2sEse!uE', 'ByRN*e&M4%xD', 'syW_ntrq-^LEW8D7', 'ExNbNJa8^mtp3f#e', 'gsY*AL-J9A7?-h2c', 'y6XQw+3H4Cef#QCtudX8ZPF5', '34mDnZTPecVpnxmvwGMW9mWY5g', 'jw2AFhSB']; 42 | 43 | fakeData.forEach( data => { 44 | expect(isStrongPassword(data)).toBe(true); 45 | }); 46 | }); 47 | }); 48 | }); --------------------------------------------------------------------------------