├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── lerna.json ├── package.json └── workspaces └── api ├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .nvmrc ├── .prettierrc ├── Dockerfile ├── Dockerfile.dev ├── docker-compose.dev.yml ├── docker-compose.yml ├── package.json └── src ├── graphql ├── index.js ├── schema.js └── types.js ├── i18next ├── index.js └── locales │ ├── en.json │ └── ge.json ├── index.js ├── middleware ├── authMiddleware.js ├── authentication.js └── index.js ├── module ├── auth │ ├── index.js │ ├── mail │ │ ├── index.js │ │ └── userMail.js │ ├── resolvers.js │ ├── service │ │ ├── index.js │ │ └── userService.js │ ├── types.js │ └── user.js └── index.js ├── mongoose.js ├── redis.js ├── service ├── logger.js └── nodemailer.js ├── validator ├── index.js └── userValidator.js └── view └── template ├── reset-password └── html.ejs ├── verify-request └── html.ejs └── verify └── html.ejs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | yarn.lock 4 | .DS_Store -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v15.4.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 watscho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [express-graphql-mongodb-boilerplate](https://github.com/watscho/express-graphql-mongodb-boilerplate) 2 | 3 | **Also [express-mongodb-rest-api-boilerplate](https://github.com/watscho/express-mongodb-rest-api-boilerplate) - REST-API Boilerplate** 4 | 5 | [![](https://img.shields.io/badge/author-@watscho-blue.svg)](https://www.linkedin.com/in/watscho) 6 | [![](https://api.codacy.com/project/badge/Grade/0b49c33797cc49b98502e249f27326de)](https://www.codacy.com/manual/watscho/express-graphql-mongodb-boilerplate?utm_source=github.com&utm_medium=referral&utm_content=watscho/express-graphql-mongodb-boilerplate&utm_campaign=Badge_Grade) 7 | [![GitHub license](https://img.shields.io/github/license/watscho/express-graphql-mongodb-boilerplate)](https://github.com/watscho/express-graphql-mongodb-boilerplate/blob/master/LICENSE) 8 | 9 | ## Authentication from scratch 10 | 11 | ### Sign In, Sign Up, Reset Password, Change Password, Update User 12 | 13 | ### E-mail verification, Multi language, Redis for token blacklisting 14 | 15 | ### Package list 16 | 17 | | Package | Description | 18 | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 19 | | bcryptjs | Optimized bcrypt in JavaScript with zero dependencies. Compatible to the C++ bcrypt binding on node.js and also working in the browser. | 20 | | cors | CORS is a node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options. | 21 | | crypto-random-string | Generate a cryptographically strong random string | 22 | | dotenv | Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env. Storing configuration in the environment separate from code is based on The Twelve-Factor App methodology. | 23 | | ejs | Embedded JavaScript templates | 24 | | email-templates | Create, preview, and send custom email templates for Node.js. Highly configurable and supports automatic inline CSS, stylesheets, embedded images and fonts, and much more! Made for sending beautiful emails with Lad. | 25 | | express | Fast, unopinionated, minimalist web framework for node. | 26 | | express-graphql | Create a GraphQL HTTP server with any HTTP web framework that supports connect styled middleware, including Connect itself, Express and Restify. | 27 | | graphql | The JavaScript reference implementation for GraphQL, a query language for APIs created by Facebook. | 28 | | graphql-compose | GraphQL – is a query language for APIs. graphql-js is the reference implementation of GraphQL for nodejs which introduce GraphQL type system for describing schema (definition over configuration) and executes queries on the server side. express-graphql is a HTTP server which gets request data, passes it to graphql-js and returned result passes to response. | 29 | | graphql-compose-mongoose | This is a plugin for graphql-compose, which derives GraphQLType from your mongoose model. Also derives bunch of internal GraphQL Types. Provide all CRUD resolvers, including graphql connection, also provided basic search via operators ($lt, $gt and so on). | 30 | | i18next | i18next is a very popular internationalization framework for browser or any other javascript environment (eg. node.js). | 31 | | i18next-express-middleware | This is a middleware to use i18next in express.js. | 32 | | ioredis | A robust, performance-focused and full-featured Redis client for Node.js. | 33 | | jsonwebtoken | This was developed against draft-ietf-oauth-json-web-token-08. It makes use of node-jws | 34 | | module-alias | Create aliases of directories and register custom module paths in NodeJS like a boss! | 35 | | moment | A lightweight JavaScript date library for parsing, validating, manipulating, and formatting dates. | 36 | | mongoose | Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. Mongoose supports both promises and callbacks. | 37 | | nodemailer | Send e-mails from Node.js – easy as cake! | 38 | | validator | A library of string validators and sanitizers. | 39 | | winston | A logger for just about everything. | 40 | 41 | ### Redis 42 | 43 | _Mac (using [homebrew](http://brew.sh/)):_ 44 | 45 | ```bash 46 | brew install redis 47 | ``` 48 | 49 | _Linux:_ 50 | 51 | ```bash 52 | sudo apt-get install redis-server 53 | ``` 54 | 55 | ### First, we use [yarn](https://yarnpkg.com/) - [workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) with [lerna](https://www.npmjs.com/package/lerna) 56 | 57 | [Introducing Yarn Workspaces](https://classic.yarnpkg.com/blog/2017/08/02/introducing-workspaces/) 58 | 59 | ```bash 60 | cd workspaces/api 61 | ``` 62 | 63 | ### COPY .env.example to .env 64 | 65 | ```bash 66 | cp .env.example .env 67 | ``` 68 | 69 | **Note:** I highly recommend installing [nodemon](https://github.com/remy/nodemon). 70 | 71 | nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected. 72 | nodemon does not require any additional changes to your code or method of development. nodemon is a replacement wrapper for `node`, to use `nodemon` replace the word `node` on the command line when executing your script. 73 | `yarn global add nodemon`. 74 | 75 | ### API Start 76 | 77 | ```bash 78 | yarn start 79 | yarn start:local # with nodemon 80 | ``` 81 | 82 | ### Docker compose 83 | 84 | ```bash 85 | docker-compose up -d --build 86 | docker-compose -f docker-compose.dev.yml up --build # with nodemon 87 | ``` 88 | 89 | ### ESlint Start 90 | 91 | ```bash 92 | yarn lint 93 | yarn lint:write # with prefix --fix 94 | ``` 95 | 96 | ### API Structure 97 | 98 | ```bash 99 | ├─ src 100 | │ ├─ graphql 101 | │ │ ├─ index.js 102 | │ │ ├─ schema.js 103 | │ │ └─ types.js 104 | │ ├─ i18next 105 | │ │ ├─ locales 106 | │ │ │ ├─ en.json 107 | │ │ │ └─ ge.json 108 | │ │ └─ index.js 109 | │ ├─ middleware 110 | │ │ ├─ authentication.js 111 | │ │ ├─ authMiddleware.js 112 | │ │ └─ index.js 113 | │ ├─ module 114 | │ │ ├─ auth 115 | │ │ │ ├─ mail 116 | │ │ │ │ ├─ index.js 117 | │ │ │ │ └─ userMail.js 118 | │ │ │ ├─ service 119 | │ │ │ │ ├─ index.js 120 | │ │ │ │ └─ userService.js 121 | │ │ │ ├─ index.js 122 | │ │ │ ├─ resolvers.js 123 | │ │ │ ├─ types.js 124 | │ │ │ └─ user.js 125 | │ │ └─ index.js 126 | │ ├─ service 127 | │ │ ├─ logger.js 128 | │ │ └─ nodemailer.js 129 | │ ├─ validator 130 | │ │ ├─ index.js 131 | │ │ └─ userValidator.js 132 | │ ├─ view 133 | │ │ └─ template 134 | │ │ ├─ reset-password 135 | │ │ │ └─ html.ejs 136 | │ │ ├─ verify 137 | │ │ │ └─ html.ejs 138 | │ │ └─ verify-request 139 | │ │ └─ html.ejs 140 | │ ├─ index.js 141 | │ ├─ mongoose.js 142 | │ └─ redis.js 143 | ├─ .dockerignore 144 | ├─ .env.example 145 | ├─ .eslintignore 146 | ├─ .eslint 147 | ├─ .gitignore 148 | ├─ Dockerfile 149 | ├─ Dockerfile.dev 150 | ├─ LICENSE 151 | ├─ README.md 152 | ├─ docker-compose.dev.yml 153 | ├─ docker-compose.yml 154 | └─ package.json 155 | ``` 156 | 157 | **Note:** To continue development, you should learn about [graphql-compose](https://graphql-compose.github.io) - this is the library that I use to write the API, you can read about it at the link: [docs](https://graphql-compose.github.io/docs/intro/quick-start.html) 158 | 159 | ## Queries 160 | 161 | ```graphql 162 | query user { 163 | user { 164 | _id 165 | email 166 | firstName 167 | lastName 168 | locale 169 | account { 170 | verification { 171 | verified 172 | } 173 | } 174 | updatedAt 175 | createdAt 176 | } 177 | } 178 | ``` 179 | 180 | ## Mutations 181 | 182 | ```graphql 183 | mutation signIn($email: String!, $password: String!) { 184 | signIn(email: $email, password: $password) { 185 | accessToken 186 | } 187 | } 188 | 189 | mutation signUp($email: String!, $password: String!) { 190 | signUp(email: $email, password: $password) { 191 | accessToken 192 | } 193 | } 194 | 195 | mutation logout { 196 | logout { 197 | succeed 198 | } 199 | } 200 | 201 | mutation verifyRequest { 202 | verifyRequest { 203 | succeed 204 | } 205 | } 206 | 207 | mutation verify($token: String!) { 208 | verify(token: $token) { 209 | accessToken 210 | } 211 | } 212 | 213 | mutation resetPassword($email: String!) { 214 | resetPassword(email: $email) { 215 | succeed 216 | } 217 | } 218 | 219 | mutation newPassword($token: String!, $newPassword: String!) { 220 | newPassword(token: $token, newPassword: $newPassword) { 221 | accessToken 222 | } 223 | } 224 | 225 | mutation changePassword($currentPassword: String!, $newPassword: String!) { 226 | changePassword(currentPassword: $currentPassword, newPassword: $newPassword) { 227 | succeed 228 | } 229 | } 230 | 231 | mutation updateUser($email: String!, $firstName: String!, $lastName: String!) { 232 | updateUser(email: $email, firstName: $firstName, lastName: $lastName) { 233 | _id 234 | email 235 | firstName 236 | lastName 237 | locale 238 | account { 239 | verification { 240 | verified 241 | } 242 | } 243 | updatedAt 244 | createdAt 245 | } 246 | } 247 | 248 | mutation switchLocale($locale: Locale!) { 249 | switchLocale(locale: $locale) { 250 | _id 251 | email 252 | firstName 253 | lastName 254 | locale 255 | account { 256 | verification { 257 | verified 258 | } 259 | } 260 | updatedAt 261 | createdAt 262 | } 263 | } 264 | ``` 265 | 266 | **Note:** For any question [issues](https://github.com/watscho/express-graphql-mongodb-boilerplate/issues) 267 | 268 | ## License 269 | 270 | This project is an open-source with an [MIT License](https://github.com/watscho/express-graphql-mongodb-boilerplate/blob/master/LICENSE) 271 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "packages": ["workspaces/*"], 4 | "useWorkspaces": true, 5 | "version": "9.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "9.0.0", 3 | "license": "MIT", 4 | "private": true, 5 | "scripts": { 6 | "update:version": "lerna version --no-push" 7 | }, 8 | "workspaces": [ 9 | "workspaces/*" 10 | ], 11 | "devDependencies": { 12 | "lerna": "^3.22.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /workspaces/api/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | /.vscode 3 | node_modules 4 | npm-debug 5 | *.log -------------------------------------------------------------------------------- /workspaces/api/.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="express-graphql-mongodb-boilerplate" 2 | APP_URL="http://localhost" 3 | APP_PORT=8000 4 | 5 | CLIENT_URL="http://localhost:3000" 6 | 7 | REDIS_HOST="redis" 8 | REDIS_PORT=6379 9 | REDIS_TOKEN_EXPIRY=86400 10 | 11 | DB_HOST="mongodb://mongo" 12 | DB_PORT="27017" 13 | DB_NAME="express-graphql-mongodb-boilerplate" 14 | 15 | JWT_SECRET="secret" 16 | JWT_EXPIRATION="24h" 17 | 18 | MAIL_HOST="smtp.mailtrap.io" 19 | MAIL_PORT=2525 20 | MAIL_USER="" 21 | MAIL_PASSWORD="" 22 | 23 | API_LOG_FILENAME="api-logs.log" -------------------------------------------------------------------------------- /workspaces/api/.eslintignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.vscode 3 | node_modules 4 | *.log -------------------------------------------------------------------------------- /workspaces/api/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "prettier"], 7 | "parserOptions": { 8 | "ecmaVersion": 12, 9 | "sourceType": "module" 10 | }, 11 | "plugins": ["prettier"], 12 | "rules": { 13 | "prettier/prettier": "error", 14 | "arrow-parens": ["error", "as-needed"], 15 | "space-in-parens": ["error", "never"], 16 | "no-spaced-func": "error", 17 | "object-curly-spacing": ["error", "always"], 18 | "computed-property-spacing": ["error", "never"], 19 | "array-bracket-spacing": ["error", "never"], 20 | "space-unary-ops": "error", 21 | "comma-spacing": ["error", { "before": false, "after": true }], 22 | "comma-style": ["error", "last"], 23 | "space-infix-ops": ["error", { "int32Hint": false }], 24 | "keyword-spacing": ["error", { "before": true }], 25 | "prefer-arrow-callback": "error", 26 | "no-promise-executor-return": "error", 27 | "comma-dangle": ["error", "never"], 28 | "no-sequences": "error", 29 | "max-len": ["error", { "code": 100, "ignoreComments": true }], 30 | "quote-props": ["error", "as-needed"], 31 | "jsx-quotes": ["error", "prefer-double"], 32 | "no-console": "error", 33 | "no-trailing-spaces": "error", 34 | "no-multi-spaces": "error", 35 | "padded-blocks": ["error", "never"], 36 | "arrow-spacing": ["error", { "before": true, "after": true }], 37 | "key-spacing": ["error", { "beforeColon": false }], 38 | "no-multiple-empty-lines": [ 39 | "error", 40 | { 41 | "max": 1, 42 | "maxEOF": 1 43 | } 44 | ], 45 | "no-await-in-loop": "error", 46 | "indent": ["error", 2], 47 | "linebreak-style": ["error", "unix"], 48 | "quotes": ["error", "single"], 49 | "semi": ["error", "never"] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /workspaces/api/.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | node_modules 3 | package-lock.json 4 | .env 5 | *.log 6 | yarn.lock -------------------------------------------------------------------------------- /workspaces/api/.nvmrc: -------------------------------------------------------------------------------- 1 | v15.4.0 -------------------------------------------------------------------------------- /workspaces/api/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "arrowParens": "avoid", 4 | "semi": false, 5 | "tabWidth": 2, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /workspaces/api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add yarn 6 | 7 | COPY package.json yarn.lock ./ 8 | 9 | RUN yarn install --frozen-lockfile 10 | 11 | COPY ./ ./ 12 | 13 | EXPOSE 8000 14 | 15 | CMD ["yarn", "start"] 16 | -------------------------------------------------------------------------------- /workspaces/api/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk add yarn 6 | 7 | RUN yarn global add nodemon 8 | 9 | COPY package.json yarn.lock ./ 10 | 11 | RUN yarn install 12 | 13 | COPY ./ ./ 14 | 15 | EXPOSE 8000 16 | 17 | CMD ["yarn", "start:local"] 18 | -------------------------------------------------------------------------------- /workspaces/api/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | container_name: express-graphql-mongodb-boilerplate 5 | restart: always 6 | build: 7 | dockerfile: Dockerfile.dev 8 | context: . 9 | ports: 10 | - '80:8000' 11 | volumes: 12 | - ./:/usr/src/app 13 | mongo: 14 | container_name: mongo 15 | image: mongo 16 | ports: 17 | - '27017:27017' 18 | redis: 19 | container_name: redis 20 | image: redis:alpine 21 | ports: 22 | - '6379:6379' -------------------------------------------------------------------------------- /workspaces/api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | container_name: express-graphql-mongodb-boilerplate 5 | restart: always 6 | build: . 7 | ports: 8 | - '80:8000' 9 | mongo: 10 | container_name: mongo 11 | image: mongo 12 | ports: 13 | - '27017:27017' 14 | redis: 15 | container_name: redis 16 | image: redis:alpine 17 | ports: 18 | - '6379:6379' -------------------------------------------------------------------------------- /workspaces/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-graphql-mongodb-boilerplate", 3 | "version": "9.0.0", 4 | "description": "A boilerplate for Node.js apps / GraphQL-API / Authentication from scratch - express, graphql - (graphql compose), mongodb (mongoose).", 5 | "main": "./src", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/watscho/express-graphql-mongodb-boilerplate" 9 | }, 10 | "bugs": "https://github.com/watscho/express-graphql-mongodb-boilerplate/issues", 11 | "keywords": [ 12 | "express", 13 | "graphql", 14 | "graphql-compose", 15 | "mongodb", 16 | "mongoose", 17 | "node", 18 | "nodejs", 19 | "authentication", 20 | "scratch", 21 | "boilerplate", 22 | "authorization", 23 | "docker", 24 | "api" 25 | ], 26 | "scripts": { 27 | "start": "cross-env NODE_PATH=./src node ./src", 28 | "start:local": "cross-env NODE_PATH=./src nodemon ./src", 29 | "lint": "eslint --debug ./src", 30 | "lint:fix": "eslint --debug ./src --fix", 31 | "prettier": "prettier \"**/*.+(js|jsx|json|css|md)\"", 32 | "prettier:fix": "prettier --write \"**/*.+(js|jsx|json|css|md)\"" 33 | }, 34 | "author": "Watscho Aiwasov", 35 | "license": "MIT", 36 | "dependencies": { 37 | "bcryptjs": "^2.4.3", 38 | "cors": "^2.8.5", 39 | "crypto-random-string": "^3.3.0", 40 | "ejs": "^3.1.5", 41 | "email-templates": "^8.0.2", 42 | "express": "^5.0.0-alpha.8", 43 | "express-graphql": "^0.12.0", 44 | "graphql": "^15.4.0", 45 | "graphql-compose": "^7.23.0", 46 | "graphql-compose-mongoose": "^9.0.0", 47 | "i18next": "^19.8.4", 48 | "i18next-express-middleware": "^2.0.0", 49 | "ioredis": "^4.19.2", 50 | "jsonwebtoken": "^8.5.1", 51 | "module-alias": "^2.2.2", 52 | "moment": "^2.29.1", 53 | "mongoose": "^5.11.6", 54 | "nodemailer": "^6.4.16", 55 | "validator": "^13.5.2", 56 | "winston": "^3.3.3" 57 | }, 58 | "devDependencies": { 59 | "cross-env": "^7.0.3", 60 | "dotenv": "^8.2.0", 61 | "eslint": "^7.15.0", 62 | "prettier": "^2.2.1", 63 | "eslint-config-prettier": "^6.14.0", 64 | "eslint-plugin-prettier": "^3.1.4" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /workspaces/api/src/graphql/index.js: -------------------------------------------------------------------------------- 1 | const { graphqlHTTP } = require('express-graphql') 2 | 3 | const schema = require('@app/graphql/schema') 4 | 5 | module.exports = graphqlHTTP(async request => ({ 6 | schema, 7 | graphiql: true, 8 | context: { 9 | user: request.user, 10 | headers: request.headers, 11 | accessToken: request.accessToken, 12 | i18n: request.i18n 13 | } 14 | })) 15 | -------------------------------------------------------------------------------- /workspaces/api/src/graphql/schema.js: -------------------------------------------------------------------------------- 1 | const { schemaComposer } = require('graphql-compose') 2 | 3 | require('@app/graphql/types') 4 | 5 | const { authMiddleware: middleware } = require('@app/middleware') 6 | const { userValidator: validator } = require('@app/validator') 7 | const { UserTC } = require('@app/module') 8 | 9 | schemaComposer.Query.addFields({ 10 | user: UserTC.getResolver('user', [middleware.isAuth]) 11 | }) 12 | 13 | schemaComposer.Mutation.addFields({ 14 | signIn: UserTC.getResolver('signIn', [middleware.isGuest, validator.signIn]), 15 | signUp: UserTC.getResolver('signUp', [middleware.isGuest, validator.signUp]), 16 | logout: UserTC.getResolver('logout', [middleware.isAuth]), 17 | verifyRequest: UserTC.getResolver('verifyRequest', [middleware.isAuth, middleware.isUnverfied]), 18 | verify: UserTC.getResolver('verify'), 19 | resetPassword: UserTC.getResolver('resetPassword', [middleware.isGuest, validator.resetPassword]), 20 | newPassword: UserTC.getResolver('newPassword', [middleware.isGuest, validator.newPassword]), 21 | changePassword: UserTC.getResolver('changePassword', [ 22 | middleware.isAuth, 23 | validator.changePassword 24 | ]), 25 | updateUser: UserTC.getResolver('updateUser', [middleware.isAuth, validator.updateUser]), 26 | switchLocale: UserTC.getResolver('switchLocale', [middleware.isAuth]) 27 | }) 28 | 29 | const schema = schemaComposer.buildSchema() 30 | 31 | module.exports = schema 32 | -------------------------------------------------------------------------------- /workspaces/api/src/graphql/types.js: -------------------------------------------------------------------------------- 1 | const { schemaComposer } = require('graphql-compose') 2 | 3 | schemaComposer.createObjectTC({ 4 | name: 'Succeed', 5 | fields: { succeed: 'Boolean!' } 6 | }) 7 | -------------------------------------------------------------------------------- /workspaces/api/src/i18next/index.js: -------------------------------------------------------------------------------- 1 | const i18next = require('i18next') 2 | const i18nextMiddleware = require('i18next-express-middleware') 3 | 4 | const localeEN = require('@app/i18next/locales/en.json') 5 | const localeGE = require('@app/i18next/locales/ge.json') 6 | 7 | i18next.use(i18nextMiddleware.LanguageDetector).init({ 8 | detection: { 9 | order: ['header'], 10 | lookupHeader: 'accept-language' 11 | }, 12 | preload: ['en', 'ge'], 13 | whitelist: ['en', 'ge'], 14 | fallbackLng: 'en', 15 | resources: { 16 | en: { translation: localeEN }, 17 | ge: { translation: localeGE } 18 | } 19 | }) 20 | 21 | module.exports = { i18next, i18nextMiddleware } 22 | -------------------------------------------------------------------------------- /workspaces/api/src/i18next/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "verification": "Verification", 3 | "resetPassword": "Reset password", 4 | "followUrl": "Follow URL", 5 | "yourAccountHasBeenSuccessfullyVerified": "Your account has been successfully verified" 6 | } 7 | -------------------------------------------------------------------------------- /workspaces/api/src/i18next/locales/ge.json: -------------------------------------------------------------------------------- 1 | { 2 | "verification": "ვერიფიკაცია", 3 | "resetPassword": "პაროლის აღდგენა", 4 | "followUrl": "გაჰყევით ბმულს", 5 | "yourAccountHasBeenSuccessfullyVerified": "თქვენ წარმატებით გაიარეთ ვერიფიკაცია" 6 | } 7 | -------------------------------------------------------------------------------- /workspaces/api/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const cors = require('cors') 3 | 4 | require('module-alias').addAlias('@app', `${__dirname}/`) 5 | require('dotenv').config() 6 | 7 | require('@app/service/logger') 8 | require('@app/redis') 9 | 10 | const { i18next, i18nextMiddleware } = require('@app/i18next') 11 | const authentication = require('@app/middleware/authentication') 12 | const graphql = require('@app/graphql') 13 | 14 | const app = express() 15 | 16 | app.use( 17 | '/graphql', 18 | express.json(), 19 | cors({ 20 | origin: process.env.CLIENT_URL, 21 | optionsSuccessStatus: 200 22 | }), 23 | i18nextMiddleware.handle(i18next), 24 | authentication, 25 | graphql 26 | ) 27 | 28 | app.use('*', (req, res) => { 29 | res.status(404).send('404 Not Found') 30 | }) 31 | 32 | app.listen(process.env.APP_PORT) 33 | -------------------------------------------------------------------------------- /workspaces/api/src/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const authMiddleware = { 2 | isAuth: async (resolve, source, args, context, info) => { 3 | const { user } = context 4 | 5 | if (!user) { 6 | return Promise.reject(new Error('You must be authorized.')) 7 | } 8 | 9 | return resolve(source, args, context, info) 10 | }, 11 | 12 | isGuest: async (resolve, source, args, context, info) => { 13 | const { user } = context 14 | 15 | if (user) { 16 | return Promise.reject(new Error('You have already authorized.')) 17 | } 18 | 19 | return resolve(source, args, context, info) 20 | }, 21 | 22 | isVerified: async (resolve, source, args, context, info) => { 23 | const { 24 | user: { 25 | account: { 26 | verification: { verified } 27 | } 28 | } 29 | } = context 30 | 31 | if (!verified) { 32 | return Promise.reject(new Error('You must be verified.')) 33 | } 34 | 35 | return resolve(source, args, context, info) 36 | }, 37 | 38 | isUnverfied: async (resolve, source, args, context, info) => { 39 | const { 40 | user: { 41 | account: { 42 | verification: { verified } 43 | } 44 | } 45 | } = context 46 | 47 | if (verified) { 48 | return Promise.reject(new Error('You have already verified.')) 49 | } 50 | 51 | return resolve(source, args, context, info) 52 | } 53 | } 54 | 55 | module.exports = { authMiddleware } 56 | -------------------------------------------------------------------------------- /workspaces/api/src/middleware/authentication.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | 3 | const redis = require('@app/redis') 4 | const UserModel = require('@app/module/auth/user') 5 | 6 | const authentication = async (req, res, next) => { 7 | try { 8 | const { 9 | headers: { authorization } 10 | } = req 11 | if (!authorization) { 12 | return next() 13 | } 14 | 15 | const accessToken = authorization.split(' ')[1] 16 | 17 | const decoded = jwt.verify(accessToken, process.env.JWT_SECRET) 18 | if (!decoded) { 19 | return next() 20 | } 21 | 22 | const isExpired = await redis.get(`expiredToken:${accessToken}`) 23 | if (isExpired) { 24 | return next() 25 | } 26 | 27 | const user = await UserModel.findById(decoded.userId) 28 | if (!user) { 29 | return next() 30 | } 31 | 32 | Object.assign(req, { 33 | user, 34 | accessToken 35 | }) 36 | 37 | return next() 38 | } catch (error) { 39 | return next() 40 | } 41 | } 42 | 43 | module.exports = authentication 44 | -------------------------------------------------------------------------------- /workspaces/api/src/middleware/index.js: -------------------------------------------------------------------------------- 1 | const { authMiddleware } = require('@app/middleware/authMiddleware') 2 | 3 | module.exports = { authMiddleware } 4 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/index.js: -------------------------------------------------------------------------------- 1 | const UserTC = require('@app/module/auth/types') 2 | const resolvers = require('@app/module/auth/resolvers') 3 | 4 | for (const resolver in resolvers) { 5 | UserTC.addResolver(resolvers[resolver]) 6 | } 7 | 8 | module.exports = UserTC 9 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/mail/index.js: -------------------------------------------------------------------------------- 1 | const { userMail } = require('@app/module/auth/mail/userMail') 2 | 3 | module.exports = { userMail } 4 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/mail/userMail.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | 3 | const { mail } = require('@app/service/nodemailer') 4 | 5 | const userMail = { 6 | verifyRequest: (user, token) => { 7 | mail 8 | .send({ 9 | template: 'verify-request', 10 | message: { 11 | from: '"Verification request" ', 12 | to: user.email, 13 | subject: 'Verification request' 14 | }, 15 | locals: { 16 | locale: user.locale, 17 | token 18 | } 19 | }) 20 | .catch(error => winston.error(error)) 21 | }, 22 | 23 | verify: user => { 24 | mail 25 | .send({ 26 | template: 'verify', 27 | message: { 28 | from: '"Verification" ', 29 | to: user.email, 30 | subject: 'Verification' 31 | }, 32 | locals: { locale: user.locale } 33 | }) 34 | .catch(error => winston.error(error)) 35 | }, 36 | 37 | resetPassword: (user, token) => { 38 | mail 39 | .send({ 40 | template: 'reset-password', 41 | message: { 42 | from: '"Reset Password" ', 43 | to: user.email, 44 | subject: 'Reset Password' 45 | }, 46 | locals: { 47 | locale: user.locale, 48 | token 49 | } 50 | }) 51 | .catch(error => winston.error(error)) 52 | } 53 | } 54 | 55 | module.exports = { userMail } 56 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/resolvers.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs') 2 | const jwt = require('jsonwebtoken') 3 | const crypto = require('crypto-random-string') 4 | const moment = require('moment') 5 | 6 | const redis = require('@app/redis') 7 | const { userMail } = require('@app/module/auth/mail') 8 | const { userService } = require('@app/module/auth/service') 9 | const UserModel = require('@app/module/auth/user') 10 | 11 | const user = { 12 | name: 'user', 13 | type: 'User!', 14 | resolve: ({ context: { user } }) => user 15 | } 16 | 17 | const signIn = { 18 | name: 'signIn', 19 | type: 'AccessToken!', 20 | args: { 21 | email: 'String!', 22 | password: 'String!' 23 | }, 24 | resolve: async ({ args: { email, password } }) => { 25 | try { 26 | const user = await UserModel.emailExist(email) 27 | if (!user) { 28 | return Promise.reject(new Error('User not found.')) 29 | } 30 | 31 | const comparePassword = await user.comparePassword(password) 32 | if (!comparePassword) { 33 | return Promise.reject(new Error('Password is incorrect.')) 34 | } 35 | 36 | const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { 37 | expiresIn: process.env.JWT_EXPIRATION 38 | }) 39 | 40 | return { accessToken } 41 | } catch (error) { 42 | return Promise.reject(error) 43 | } 44 | } 45 | } 46 | 47 | const signUp = { 48 | name: 'signUp', 49 | type: 'AccessToken!', 50 | args: { 51 | email: 'String!', 52 | password: 'String!' 53 | }, 54 | resolve: async ({ args: { email, password }, context: { i18n } }) => { 55 | try { 56 | let user = await UserModel.emailExist(email) 57 | if (user) { 58 | return Promise.reject(new Error('Email has already been taken.')) 59 | } 60 | 61 | const hash = bcrypt.hashSync(password, 10) 62 | 63 | user = await new UserModel({ 64 | email, 65 | password: hash, 66 | locale: i18n.language 67 | }).save() 68 | 69 | const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { 70 | expiresIn: process.env.JWT_EXPIRATION 71 | }) 72 | 73 | const token = await userService.verifyRequest(user) 74 | 75 | userMail.verifyRequest(user, token) 76 | 77 | return { accessToken } 78 | } catch (error) { 79 | return Promise.reject(error) 80 | } 81 | } 82 | } 83 | 84 | const logout = { 85 | name: 'logout', 86 | type: 'Succeed!', 87 | resolve: async ({ context: { user, accessToken } }) => { 88 | try { 89 | await redis.set(`expiredToken:${accessToken}`, user._id, 'EX', process.env.REDIS_TOKEN_EXPIRY) 90 | 91 | return { succeed: true } 92 | } catch (error) { 93 | return Promise.reject(error) 94 | } 95 | } 96 | } 97 | 98 | const verifyRequest = { 99 | name: 'verifyRequest', 100 | type: 'Succeed!', 101 | resolve: async ({ context: { user } }) => { 102 | try { 103 | const token = await userService.verifyRequest(user) 104 | 105 | userMail.verifyRequest(user, token) 106 | 107 | return { succeed: true } 108 | } catch (error) { 109 | return Promise.reject(error) 110 | } 111 | } 112 | } 113 | 114 | const verify = { 115 | name: 'verify', 116 | type: 'AccessToken!', 117 | args: { token: 'String!' }, 118 | resolve: async ({ args: { token } }) => { 119 | try { 120 | const user = await UserModel.findOne({ 121 | 'account.verification.token': token 122 | }) 123 | if (!user) { 124 | return Promise.reject(new Error('Access Token is not valid or has expired.')) 125 | } 126 | 127 | user.set({ 128 | account: { 129 | verification: { 130 | verified: true, 131 | token: null, 132 | expiresIn: null 133 | } 134 | } 135 | }) 136 | 137 | await user.save() 138 | 139 | const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { 140 | expiresIn: process.env.JWT_EXPIRATION 141 | }) 142 | 143 | userMail.verify(user) 144 | 145 | return { accessToken } 146 | } catch (error) { 147 | return Promise.reject(error) 148 | } 149 | } 150 | } 151 | 152 | const resetPassword = { 153 | name: 'resetPassword', 154 | type: 'Succeed!', 155 | args: { email: 'String!' }, 156 | resolve: async ({ args: { email } }) => { 157 | try { 158 | const user = await UserModel.findOne({ email }) 159 | if (!user) { 160 | return Promise.reject(new Error('User not found.')) 161 | } 162 | 163 | const token = crypto({ length: 48, type: 'url-safe' }) 164 | const expiresIn = moment().add(7, 'days') 165 | 166 | user.set({ 167 | account: { 168 | resetPassword: { 169 | token, 170 | expiresIn 171 | } 172 | } 173 | }) 174 | 175 | await user.save() 176 | 177 | userMail.resetPassword(user, token) 178 | 179 | return { succeed: true } 180 | } catch (error) { 181 | return Promise.reject(error) 182 | } 183 | } 184 | } 185 | 186 | const newPassword = { 187 | name: 'newPassword', 188 | type: 'AccessToken!', 189 | args: { token: 'String!', newPassword: 'String!' }, 190 | resolve: async ({ args: { token, newPassword } }) => { 191 | try { 192 | const user = await UserModel.findOne({ 193 | 'account.resetPassword.token': token 194 | }) 195 | if (!user) { 196 | return Promise.reject(new Error('Access Token is not valid or has expired.')) 197 | } 198 | 199 | const hash = bcrypt.hashSync(newPassword, 10) 200 | 201 | user.set({ 202 | password: hash, 203 | account: { 204 | resetPassword: { 205 | token: null, 206 | expiresIn: null 207 | } 208 | } 209 | }) 210 | 211 | await user.save() 212 | 213 | const accessToken = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { 214 | expiresIn: process.env.JWT_EXPIRATION 215 | }) 216 | 217 | return { accessToken } 218 | } catch (error) { 219 | return Promise.reject(error) 220 | } 221 | } 222 | } 223 | 224 | const changePassword = { 225 | name: 'changePassword', 226 | type: 'Succeed!', 227 | args: { currentPassword: 'String!', newPassword: 'String!' }, 228 | resolve: async ({ args: { currentPassword, newPassword }, context: { user } }) => { 229 | try { 230 | const comparePassword = await user.comparePassword(currentPassword) 231 | if (!comparePassword) { 232 | return Promise.reject(new Error('Current password is incorrect.')) 233 | } 234 | 235 | const hash = bcrypt.hashSync(newPassword, 10) 236 | 237 | user.set({ password: hash }) 238 | 239 | await user.save() 240 | 241 | return { succeed: true } 242 | } catch (error) { 243 | return Promise.reject(error) 244 | } 245 | } 246 | } 247 | 248 | const updateUser = { 249 | name: 'updateUser', 250 | type: 'User!', 251 | args: { email: 'String!', firstName: 'String!', lastName: 'String!' }, 252 | resolve: async ({ args: { email, firstName, lastName }, context: { user } }) => { 253 | try { 254 | let { 255 | account: { 256 | verification: { verified } 257 | } 258 | } = user, 259 | verifyRequest = false 260 | 261 | if (user.email !== email) { 262 | const userExist = await UserModel.findOne({ email }) 263 | if (userExist) { 264 | return Promise.reject(new Error('Email has already been taken.')) 265 | } 266 | verified = false 267 | verifyRequest = true 268 | } 269 | 270 | user.set({ 271 | email, 272 | firstName, 273 | lastName, 274 | account: { 275 | verification: { 276 | verified 277 | } 278 | } 279 | }) 280 | 281 | await user.save() 282 | 283 | if (verifyRequest) { 284 | const token = await userService.verifyRequest(user) 285 | 286 | userMail.verifyRequest(user, token) 287 | } 288 | 289 | return user 290 | } catch (error) { 291 | return Promise.reject(error) 292 | } 293 | } 294 | } 295 | 296 | const switchLocale = { 297 | name: 'switchLocale', 298 | type: 'User!', 299 | args: { locale: 'Locale!' }, 300 | resolve: async ({ args: { locale }, context: { user } }) => { 301 | try { 302 | user.set({ locale }) 303 | 304 | await user.save() 305 | 306 | return user 307 | } catch (error) { 308 | return Promise.reject(error) 309 | } 310 | } 311 | } 312 | 313 | module.exports = { 314 | user, 315 | signIn, 316 | signUp, 317 | logout, 318 | verifyRequest, 319 | verify, 320 | resetPassword, 321 | newPassword, 322 | changePassword, 323 | updateUser, 324 | switchLocale 325 | } 326 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/service/index.js: -------------------------------------------------------------------------------- 1 | const { userService } = require('@app/module/auth/service/userService') 2 | 3 | module.exports = { userService } 4 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/service/userService.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto-random-string') 2 | const moment = require('moment') 3 | 4 | const userService = { 5 | verifyRequest: async user => { 6 | const token = crypto({ length: 48, type: 'url-safe' }) 7 | const expiresIn = moment().add(7, 'days') 8 | 9 | user.set({ 10 | account: { 11 | verification: { 12 | token, 13 | expiresIn 14 | } 15 | } 16 | }) 17 | 18 | await user.save() 19 | 20 | return token 21 | } 22 | } 23 | 24 | module.exports = { userService } 25 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/types.js: -------------------------------------------------------------------------------- 1 | const { schemaComposer } = require('graphql-compose') 2 | const { composeWithMongoose } = require('graphql-compose-mongoose') 3 | 4 | const UserModel = require('@app/module/auth/user') 5 | 6 | const UserTC = composeWithMongoose(UserModel).removeField('password') 7 | 8 | const userAccountTC = UserTC.getFieldTC('account') 9 | 10 | userAccountTC.getFieldTC('verification').removeField(['token', 'expiresIn']) 11 | 12 | userAccountTC.removeField('resetPassword') 13 | 14 | schemaComposer.createObjectTC({ 15 | name: 'AccessToken', 16 | fields: { accessToken: 'String!' } 17 | }) 18 | 19 | schemaComposer.createEnumTC({ 20 | name: 'Locale', 21 | values: { 22 | en: { value: 'en' }, 23 | ge: { value: 'ge' } 24 | } 25 | }) 26 | 27 | module.exports = UserTC 28 | -------------------------------------------------------------------------------- /workspaces/api/src/module/auth/user.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs') 2 | 3 | const mongoose = require('@app/mongoose') 4 | 5 | const { Schema } = mongoose 6 | 7 | const userSchema = new Schema( 8 | { 9 | email: String, 10 | password: String, 11 | firstName: String, 12 | lastName: String, 13 | locale: String, 14 | account: { 15 | verification: { 16 | verified: { 17 | type: Boolean, 18 | default: false 19 | }, 20 | token: String, 21 | expiresIn: Date 22 | }, 23 | resetPassword: { 24 | token: String, 25 | expiresIn: Date 26 | } 27 | } 28 | }, 29 | { timestamps: true } 30 | ) 31 | 32 | userSchema.statics.emailExist = function (email) { 33 | return this.findOne({ email }) 34 | } 35 | 36 | userSchema.methods.comparePassword = function (password) { 37 | return bcrypt.compareSync(password, this.password) 38 | } 39 | 40 | const User = mongoose.model('User', userSchema) 41 | 42 | module.exports = User 43 | -------------------------------------------------------------------------------- /workspaces/api/src/module/index.js: -------------------------------------------------------------------------------- 1 | const UserTC = require('@app/module/auth') 2 | 3 | module.exports = { UserTC } 4 | -------------------------------------------------------------------------------- /workspaces/api/src/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const winston = require('winston') 3 | 4 | mongoose 5 | .connect(`${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`, { 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true 8 | }) 9 | .catch(error => winston.error(error)) 10 | 11 | mongoose.connection.on('open', () => winston.info('MongoDB connected')) 12 | 13 | module.exports = mongoose 14 | -------------------------------------------------------------------------------- /workspaces/api/src/redis.js: -------------------------------------------------------------------------------- 1 | const Redis = require('ioredis') 2 | const winston = require('winston') 3 | 4 | const client = new Redis({ 5 | host: process.env.REDIS_HOST, 6 | port: process.env.REDIS_PORT 7 | }) 8 | 9 | client.on('error', error => { 10 | winston.error(error) 11 | client.quit() 12 | }) 13 | 14 | client.on('connect', () => winston.info('Redis client connected')) 15 | 16 | module.exports = client 17 | -------------------------------------------------------------------------------- /workspaces/api/src/service/logger.js: -------------------------------------------------------------------------------- 1 | const winston = require('winston') 2 | 3 | const { 4 | format: { combine, timestamp, json } 5 | } = winston 6 | 7 | winston.configure({ 8 | format: combine(timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), json()), 9 | transports: [new winston.transports.File({ filename: process.env.API_LOG_FILENAME })] 10 | }) 11 | -------------------------------------------------------------------------------- /workspaces/api/src/service/nodemailer.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodemailer = require('nodemailer') 3 | const Email = require('email-templates') 4 | const i18next = require('i18next') 5 | 6 | const transporter = nodemailer.createTransport({ 7 | host: process.env.MAIL_HOST, 8 | port: process.env.MAIL_PORT, 9 | auth: { 10 | user: process.env.MAIL_USER, 11 | pass: process.env.MAIL_PASSWORD 12 | } 13 | }) 14 | 15 | const mail = new Email({ 16 | views: { 17 | root: path.join(process.env.NODE_PATH, 'view', 'template'), 18 | locals: { 19 | i18n: i18next, 20 | clientUrl: process.env.CLIENT_URL 21 | }, 22 | options: { extension: 'ejs' } 23 | }, 24 | preview: false, 25 | send: true, 26 | transport: transporter 27 | }) 28 | 29 | module.exports = { transporter, mail } 30 | -------------------------------------------------------------------------------- /workspaces/api/src/validator/index.js: -------------------------------------------------------------------------------- 1 | const { userValidator } = require('@app/validator/userValidator') 2 | 3 | module.exports = { userValidator } 4 | -------------------------------------------------------------------------------- /workspaces/api/src/validator/userValidator.js: -------------------------------------------------------------------------------- 1 | const validator = require('validator') 2 | 3 | const userValidator = { 4 | changePassword: async (resolve, source, args, context, info) => { 5 | const { newPassword } = args 6 | 7 | if (!validator.isLength(newPassword, { min: 6 })) { 8 | return Promise.reject(new Error('Error: newPassword')) 9 | } 10 | 11 | return resolve(source, args, context, info) 12 | }, 13 | 14 | newPassword: async (resolve, source, args, context, info) => { 15 | const { newPassword } = args 16 | 17 | if (!validator.isLength(newPassword, { min: 6 })) { 18 | return Promise.reject(new Error('Error: newPassword')) 19 | } 20 | 21 | return resolve(source, args, context, info) 22 | }, 23 | 24 | resetPassword: async (resolve, source, args, context, info) => { 25 | let { email } = args 26 | 27 | email = validator.normalizeEmail(email) 28 | email = validator.trim(email) 29 | 30 | Object.assign(args, { email }) 31 | 32 | return resolve(source, args, context, info) 33 | }, 34 | 35 | signIn: async (resolve, source, args, context, info) => { 36 | let { email } = args 37 | 38 | email = validator.normalizeEmail(email) 39 | email = validator.trim(email) 40 | 41 | Object.assign(args, { email }) 42 | 43 | return resolve(source, args, context, info) 44 | }, 45 | 46 | signUp: async (resolve, source, args, context, info) => { 47 | let { email } = args 48 | 49 | email = validator.normalizeEmail(email) 50 | email = validator.trim(email) 51 | 52 | Object.assign(args, { email }) 53 | 54 | const { password } = args 55 | 56 | if (!validator.isEmail(email, { allow_utf8_local_part: false })) { 57 | return Promise.reject(new Error('Error: email')) 58 | } 59 | 60 | if (!validator.isLength(password, { min: 6 })) { 61 | return Promise.reject(new Error('Error: password')) 62 | } 63 | 64 | return resolve(source, args, context, info) 65 | }, 66 | 67 | updateUser: async (resolve, source, args, context, info) => { 68 | let { email } = args 69 | 70 | email = validator.normalizeEmail(email) 71 | email = validator.trim(email) 72 | 73 | Object.assign(args, { email }) 74 | 75 | const { firstName, lastName } = args 76 | 77 | if (!validator.isEmail(email, { allow_utf8_local_part: false })) { 78 | return Promise.reject(new Error('Error: email')) 79 | } 80 | if (!validator.isLength(firstName, { min: 2 })) { 81 | return Promise.reject(new Error('Error: firstName')) 82 | } 83 | if (!validator.isLength(lastName, { min: 2 })) { 84 | return Promise.reject(new Error('Error: lastName')) 85 | } 86 | 87 | return resolve(source, args, context, info) 88 | } 89 | } 90 | 91 | module.exports = { userValidator } 92 | -------------------------------------------------------------------------------- /workspaces/api/src/view/template/reset-password/html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%=i18n.getFixedT(locale)('resetPassword')%> 9 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 52 | 53 | 54 | 57 | 58 |
43 |

<%=i18n.getFixedT(locale)('resetPassword')%>

44 |
48 | 49 | <%=i18n.getFixedT(locale)('followUrl')%> 50 | 51 |
55 | Powered by watscho. 56 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /workspaces/api/src/view/template/verify-request/html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%=i18n.getFixedT(locale)('verification')%> 9 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 52 | 53 | 54 | 57 | 58 |
43 |

<%=i18n.getFixedT(locale)('verification')%>

44 |
48 | 49 | <%=i18n.getFixedT(locale)('followUrl')%> 50 | 51 |
55 | Powered by watscho. 56 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /workspaces/api/src/view/template/verify/html.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%=i18n.getFixedT(locale)('verification')%> 9 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 50 | 51 | 52 | 55 | 56 |
43 |

<%=i18n.getFixedT(locale)('verification')%>

44 |
48 |

<%=i18n.getFixedT(locale)('yourAccountHasBeenSuccessfullyVerified')%>

49 |
53 | Powered by watscho. 54 |
57 | 58 | 59 | --------------------------------------------------------------------------------