initial commit
This commit is contained in:
commit
9cfbf9ba17
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM node:8-alpine
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ARG NODE_ENV
|
||||
ENV NODE_ENV $NODE_ENV
|
||||
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
ADD package.json yarn.lock /app/
|
||||
RUN yarn --pure-lockfile
|
||||
ADD . /app
|
||||
|
||||
CMD ["yarn", "docker:start"]
|
153
README.md
Normal file
153
README.md
Normal file
@ -0,0 +1,153 @@
|
||||
# Express ES2017 REST API Boilerplate
|
||||
|
||||
|
||||
Boilerplate/Generator/Starter Project for building RESTful APIs and microservices using Node.js, Express and MongoDB
|
||||
|
||||
## Features
|
||||
|
||||
- No transpilers, just vanilla javascript
|
||||
- ES2017 latest features like Async/Await
|
||||
- CORS enabled
|
||||
- Uses [yarn](https://yarnpkg.com)
|
||||
- Express + MongoDB ([Mongoose](http://mongoosejs.com/))
|
||||
- Consistent coding styles with [editorconfig](http://editorconfig.org)
|
||||
- [Docker](https://www.docker.com/) support
|
||||
- Uses [helmet](https://github.com/helmetjs/helmet) to set some HTTP headers for security
|
||||
- Load environment variables from .env files with [dotenv](https://github.com/rolodato/dotenv-safe)
|
||||
- Request validation with [joi](https://github.com/hapijs/joi)
|
||||
- Gzip compression with [compression](https://github.com/expressjs/compression)
|
||||
- Linting with [eslint](http://eslint.org)
|
||||
- Tests with [mocha](https://mochajs.org), [chai](http://chaijs.com) and [sinon](http://sinonjs.org)
|
||||
- Code coverage with [istanbul](https://istanbul.js.org) and [coveralls](https://coveralls.io)
|
||||
- Git hooks with [husky](https://github.com/typicode/husky)
|
||||
- Logging with [morgan](https://github.com/expressjs/morgan)
|
||||
- Authentication and Authorization with [passport](http://passportjs.org)
|
||||
- API documentation generation with [apidoc](http://apidocjs.com)
|
||||
- Continuous integration support with [travisCI](https://travis-ci.org)
|
||||
- Monitoring with [pm2](https://github.com/Unitech/pm2)
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Node v7.6+](https://nodejs.org/en/download/current/) or [Docker](https://www.docker.com/)
|
||||
- [Yarn](https://yarnpkg.com/en/docs/install)
|
||||
|
||||
## Getting Started
|
||||
|
||||
#### Clone the repo and make it yours:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://github.com/krishfsousa/sg-node-express-rest-api
|
||||
cd sg-node-express-rest-api
|
||||
rm -rf .git
|
||||
```
|
||||
|
||||
#### Install dependencies:
|
||||
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
|
||||
#### Set environment variables:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Running Locally
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Running in Production
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Lint
|
||||
|
||||
```bash
|
||||
# lint code with ESLint
|
||||
yarn lint
|
||||
|
||||
# try to fix ESLint errors
|
||||
yarn lint:fix
|
||||
|
||||
# lint and watch for changes
|
||||
yarn lint:watch
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# run all tests with Mocha
|
||||
yarn test
|
||||
|
||||
# run unit tests
|
||||
yarn test:unit
|
||||
|
||||
# run integration tests
|
||||
yarn test:integration
|
||||
|
||||
# run all tests and watch for changes
|
||||
yarn test:watch
|
||||
|
||||
# open nyc test coverage reports
|
||||
yarn coverage
|
||||
```
|
||||
|
||||
## Validate
|
||||
|
||||
```bash
|
||||
# run lint and tests
|
||||
yarn validate
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
# show logs in production
|
||||
pm2 logs
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
```bash
|
||||
# generate and open api documentation
|
||||
yarn docs
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
# run container locally
|
||||
yarn docker:dev
|
||||
|
||||
# run container in production
|
||||
yarn docker:prod
|
||||
|
||||
# run tests
|
||||
yarn docker:test
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
Set your server ip:
|
||||
|
||||
```bash
|
||||
DEPLOY_SERVER=127.0.0.1
|
||||
```
|
||||
|
||||
Replace my Docker username with yours:
|
||||
|
||||
```bash
|
||||
nano deploy.sh
|
||||
```
|
||||
|
||||
Run deploy script:
|
||||
|
||||
```bash
|
||||
yarn deploy
|
||||
```
|
||||
|
12
deploy.sh
Normal file
12
deploy.sh
Normal file
@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
docker build -t krishfsousa/sg-node-express-rest-api .
|
||||
docker push krishfsousa/sg-node-express-rest-api
|
||||
|
||||
ssh deploy@$DEPLOY_SERVER << EOF
|
||||
docker pull krishfsousa/sg-node-express-rest-api
|
||||
docker stop api-boilerplate || true
|
||||
docker rm api-boilerplate || true
|
||||
docker rmi krishfsousa/sg-node-express-rest-api:current || true
|
||||
docker tag krishfsousa/sg-node-express-rest-api:latest krishfsousa/sg-node-express-rest-api:current
|
||||
docker run -d --restart always --name api-boilerplate -p 3000:3000 krishfsousa/sg-node-express-rest-api:current
|
||||
EOF
|
6
docker-compose.dev.yml
Normal file
6
docker-compose.dev.yml
Normal file
@ -0,0 +1,6 @@
|
||||
version: "2"
|
||||
services:
|
||||
boilerplate-api:
|
||||
command: yarn dev -- -L
|
||||
environment:
|
||||
- NODE_ENV=development
|
6
docker-compose.prod.yml
Normal file
6
docker-compose.prod.yml
Normal file
@ -0,0 +1,6 @@
|
||||
version: "2"
|
||||
services:
|
||||
boilerplate-api:
|
||||
command: yarn start
|
||||
environment:
|
||||
- NODE_ENV=production
|
6
docker-compose.test.yml
Normal file
6
docker-compose.test.yml
Normal file
@ -0,0 +1,6 @@
|
||||
version: "2"
|
||||
services:
|
||||
boilerplate-api:
|
||||
command: yarn test
|
||||
environment:
|
||||
- NODE_ENV=test
|
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@ -0,0 +1,17 @@
|
||||
version: "2"
|
||||
services:
|
||||
boilerplate-api:
|
||||
build: .
|
||||
environment:
|
||||
- MONGO_URI=mongodb://mongodb:27017/sg-node-express-rest-api
|
||||
volumes:
|
||||
- .:/app
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
mongodb:
|
||||
image: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
100
package.json
Normal file
100
package.json
Normal file
@ -0,0 +1,100 @@
|
||||
{
|
||||
"name": "sg-node-express-rest-api",
|
||||
"version": "0.0.2",
|
||||
"description": "Sentientgeeks Boilerplate Node Express RESTApi ES2017",
|
||||
"author": "sentientgeeks",
|
||||
"main": "src/index.js",
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"yarn": "*"
|
||||
},
|
||||
"scripts": {
|
||||
"precommit": "yarn lint",
|
||||
"prestart": "yarn docs",
|
||||
"start": "cross-env NODE_ENV=production pm2 start ./src/index.js",
|
||||
"dev": "nodemon ./src/index.js",
|
||||
"lint": "eslint ./src/ --ignore-path .gitignore --ignore-pattern internals/scripts",
|
||||
"lint:fix": "yarn lint --fix",
|
||||
"lint:watch": "yarn lint --watch",
|
||||
"test": "cross-env NODE_ENV=test nyc --reporter=html --reporter=text mocha --timeout 20000 --exit --recursive src/api/tests",
|
||||
"test:unit": "cross-env NODE_ENV=test mocha src/api/tests/unit",
|
||||
"test:integration": "cross-env NODE_ENV=test mocha --timeout 20000 --exit src/api/tests/integration",
|
||||
"test:watch": "cross-env NODE_ENV=test mocha --watch src/api/tests/unit",
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"postcoverage": "open-cli coverage/lcov-report/index.html",
|
||||
"validate": "yarn lint && yarn test",
|
||||
"postpublish": "git push --tags",
|
||||
"deploy": "sh ./deploy.sh",
|
||||
"docs": "apidoc -i src -o docs",
|
||||
"postdocs": "open-cli docs/index.html",
|
||||
"docker:start": "cross-env NODE_ENV=production pm2-docker start ./src/index.js",
|
||||
"docker:prod": "docker-compose -f docker-compose.yml -f docker-compose.prod.yml up",
|
||||
"docker:dev": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up",
|
||||
"docker:test": "docker-compose -f docker-compose.yml -f docker-compose.test.yml up --abort-on-container-exit"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://krishbhatt@bitbucket.org/krishbhatt/sg-node-express-rest-api.git"
|
||||
},
|
||||
"keywords": [
|
||||
"express",
|
||||
"node",
|
||||
"node.js",
|
||||
"mongodb",
|
||||
"mongoose",
|
||||
"passport",
|
||||
"es6",
|
||||
"es7",
|
||||
"es8",
|
||||
"es2017",
|
||||
"mocha",
|
||||
"istanbul",
|
||||
"nyc",
|
||||
"eslint",
|
||||
"Travis CI",
|
||||
"coveralls",
|
||||
"REST",
|
||||
"API",
|
||||
"boilerplate",
|
||||
"generator",
|
||||
"starter project"
|
||||
],
|
||||
"dependencies": {
|
||||
"axios": "^0.19.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bluebird": "^3.5.0",
|
||||
"body-parser": "^1.17.0",
|
||||
"compression": "^1.6.2",
|
||||
"cors": "^2.8.3",
|
||||
"cross-env": "^6.0.3",
|
||||
"dotenv-safe": "^6.0.0",
|
||||
"email-templates": "^6.0.3",
|
||||
"express": "^4.15.2",
|
||||
"express-validation": "^1.0.2",
|
||||
"nodemailer": "^6.3.1",
|
||||
"passport": "^0.4.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-jwt": "4.0.0",
|
||||
"pm2": "^4.2.0",
|
||||
"pug": "^2.0.4",
|
||||
"uuid": "^3.1.0",
|
||||
"winston": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "^0.17.5",
|
||||
"chai": "^4.1.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"coveralls": "^3.0.0",
|
||||
"eslint": "^6.4.0",
|
||||
"eslint-config-airbnb-base": "^12.0.1",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"husky": "^3.0.7",
|
||||
"mocha": "^6.2.2",
|
||||
"nodemon": "^2.0.1",
|
||||
"nyc": "^14.1.1",
|
||||
"opn-cli": "^5.0.0",
|
||||
"sinon": "^7.5.0",
|
||||
"sinon-chai": "^3.0.0",
|
||||
"supertest": "^4.0.2"
|
||||
}
|
||||
}
|
146
src/api/controllers/auth.controller.js
Normal file
146
src/api/controllers/auth.controller.js
Normal file
@ -0,0 +1,146 @@
|
||||
const httpStatus = require('http-status');
|
||||
const User = require('../models/user.model');
|
||||
const RefreshToken = require('../models/refreshToken.model');
|
||||
const PasswordResetToken = require('../models/passwordResetToken.model');
|
||||
const moment = require('moment-timezone');
|
||||
const { jwtExpirationInterval } = require('../../config/vars');
|
||||
const { omit } = require('lodash');
|
||||
const APIError = require('../utils/APIError');
|
||||
const emailProvider = require('../services/emails/emailProvider');
|
||||
|
||||
/**
|
||||
* Returns a formated object with tokens
|
||||
* @private
|
||||
*/
|
||||
function generateTokenResponse(user, accessToken) {
|
||||
const tokenType = 'Bearer';
|
||||
const refreshToken = RefreshToken.generate(user).token;
|
||||
const expiresIn = moment().add(jwtExpirationInterval, 'minutes');
|
||||
return {
|
||||
tokenType,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresIn,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns jwt token if registration was successful
|
||||
* @public
|
||||
*/
|
||||
exports.register = async (req, res, next) => {
|
||||
try {
|
||||
const userData = omit(req.body, 'role');
|
||||
const user = await new User(userData).save();
|
||||
const userTransformed = user.transform();
|
||||
const token = generateTokenResponse(user, user.token());
|
||||
res.status(httpStatus.CREATED);
|
||||
return res.json({ token, user: userTransformed });
|
||||
} catch (error) {
|
||||
return next(User.checkDuplicateEmail(error));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns jwt token if valid username and password is provided
|
||||
* @public
|
||||
*/
|
||||
exports.login = async (req, res, next) => {
|
||||
try {
|
||||
const { user, accessToken } = await User.findAndGenerateToken(req.body);
|
||||
const token = generateTokenResponse(user, accessToken);
|
||||
const userTransformed = user.transform();
|
||||
return res.json({ token, user: userTransformed });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* login with an existing user or creates a new one if valid accessToken token
|
||||
* Returns jwt token
|
||||
* @public
|
||||
*/
|
||||
exports.oAuth = async (req, res, next) => {
|
||||
try {
|
||||
const { user } = req;
|
||||
const accessToken = user.token();
|
||||
const token = generateTokenResponse(user, accessToken);
|
||||
const userTransformed = user.transform();
|
||||
return res.json({ token, user: userTransformed });
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a new jwt when given a valid refresh token
|
||||
* @public
|
||||
*/
|
||||
exports.refresh = async (req, res, next) => {
|
||||
try {
|
||||
const { email, refreshToken } = req.body;
|
||||
const refreshObject = await RefreshToken.findOneAndRemove({
|
||||
userEmail: email,
|
||||
token: refreshToken,
|
||||
});
|
||||
const { user, accessToken } = await User.findAndGenerateToken({ email, refreshObject });
|
||||
const response = generateTokenResponse(user, accessToken);
|
||||
return res.json(response);
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.sendPasswordReset = async (req, res, next) => {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
const user = await User.findOne({ email }).exec();
|
||||
|
||||
if (user) {
|
||||
const passwordResetObj = await PasswordResetToken.generate(user);
|
||||
emailProvider.sendPasswordReset(passwordResetObj);
|
||||
res.status(httpStatus.OK);
|
||||
return res.json('success');
|
||||
}
|
||||
throw new APIError({
|
||||
status: httpStatus.UNAUTHORIZED,
|
||||
message: 'No account found with that email',
|
||||
});
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
const { email, password, resetToken } = req.body;
|
||||
const resetTokenObject = await PasswordResetToken.findOneAndRemove({
|
||||
userEmail: email,
|
||||
resetToken,
|
||||
});
|
||||
|
||||
const err = {
|
||||
status: httpStatus.UNAUTHORIZED,
|
||||
isPublic: true,
|
||||
};
|
||||
if (!resetTokenObject) {
|
||||
err.message = 'Cannot find matching reset token';
|
||||
throw new APIError(err);
|
||||
}
|
||||
if (moment().isAfter(resetTokenObject.expires)) {
|
||||
err.message = 'Reset token is expired';
|
||||
throw new APIError(err);
|
||||
}
|
||||
|
||||
const user = await User.findOne({ email: resetTokenObject.userEmail }).exec();
|
||||
user.password = password;
|
||||
await user.save();
|
||||
emailProvider.sendPasswordChangeEmail(user);
|
||||
|
||||
res.status(httpStatus.OK);
|
||||
return res.json('Password Updated');
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
102
src/api/controllers/user.controller.js
Normal file
102
src/api/controllers/user.controller.js
Normal file
@ -0,0 +1,102 @@
|
||||
const httpStatus = require('http-status');
|
||||
const { omit } = require('lodash');
|
||||
const User = require('../models/user.model');
|
||||
|
||||
/**
|
||||
* Load user and append to req.
|
||||
* @public
|
||||
*/
|
||||
exports.load = async (req, res, next, id) => {
|
||||
try {
|
||||
const user = await User.get(id);
|
||||
req.locals = { user };
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user
|
||||
* @public
|
||||
*/
|
||||
exports.get = (req, res) => res.json(req.locals.user.transform());
|
||||
|
||||
/**
|
||||
* Get logged in user info
|
||||
* @public
|
||||
*/
|
||||
exports.loggedIn = (req, res) => res.json(req.user.transform());
|
||||
|
||||
/**
|
||||
* Create new user
|
||||
* @public
|
||||
*/
|
||||
exports.create = async (req, res, next) => {
|
||||
try {
|
||||
const user = new User(req.body);
|
||||
const savedUser = await user.save();
|
||||
res.status(httpStatus.CREATED);
|
||||
res.json(savedUser.transform());
|
||||
} catch (error) {
|
||||
next(User.checkDuplicateEmail(error));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace existing user
|
||||
* @public
|
||||
*/
|
||||
exports.replace = async (req, res, next) => {
|
||||
try {
|
||||
const { user } = req.locals;
|
||||
const newUser = new User(req.body);
|
||||
const ommitRole = user.role !== 'admin' ? 'role' : '';
|
||||
const newUserObject = omit(newUser.toObject(), '_id', ommitRole);
|
||||
|
||||
|
||||
res.json(savedUser.transform());
|
||||
} catch (error) {
|
||||
next(User.checkDuplicateEmail(error));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update existing user
|
||||
* @public
|
||||
*/
|
||||
exports.update = (req, res, next) => {
|
||||
const ommitRole = req.locals.user.role !== 'admin' ? 'role' : '';
|
||||
const updatedUser = omit(req.body, ommitRole);
|
||||
const user = Object.assign(req.locals.user, updatedUser);
|
||||
|
||||
user.save()
|
||||
.then(savedUser => res.json(savedUser.transform()))
|
||||
.catch(e => next(User.checkDuplicateEmail(e)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get user list
|
||||
* @public
|
||||
*/
|
||||
exports.list = async (req, res, next) => {
|
||||
try {
|
||||
const users = await User.list(req.query);
|
||||
const transformedUsers = users.map(user => user.transform());
|
||||
res.json(transformedUsers);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete user
|
||||
* @public
|
||||
*/
|
||||
exports.remove = (req, res, next) => {
|
||||
const { user } = req.locals;
|
||||
|
||||
user.remove()
|
||||
.then(() => res.status(httpStatus.NO_CONTENT).end())
|
||||
.catch(e => next(e));
|
||||
};
|
54
src/api/middlewares/auth.js
Normal file
54
src/api/middlewares/auth.js
Normal file
@ -0,0 +1,54 @@
|
||||
const httpStatus = require('http-status');
|
||||
const passport = require('passport');
|
||||
const User = require('../models/user.model');
|
||||
const APIError = require('../utils/APIError');
|
||||
|
||||
const ADMIN = 'admin';
|
||||
const LOGGED_USER = '_loggedUser';
|
||||
|
||||
const handleJWT = (req, res, next, roles) => async (err, user, info) => {
|
||||
const error = err || info;
|
||||
const logIn = Promise.promisify(req.logIn);
|
||||
const apiError = new APIError({
|
||||
message: error ? error.message : 'Unauthorized',
|
||||
status: httpStatus.UNAUTHORIZED,
|
||||
stack: error ? error.stack : undefined,
|
||||
});
|
||||
|
||||
try {
|
||||
if (error || !user) throw error;
|
||||
await logIn(user, { session: false });
|
||||
} catch (e) {
|
||||
return next(apiError);
|
||||
}
|
||||
|
||||
if (roles === LOGGED_USER) {
|
||||
if (user.role !== 'admin' && req.params.userId !== user._id.toString()) {
|
||||
apiError.status = httpStatus.FORBIDDEN;
|
||||
apiError.message = 'Forbidden';
|
||||
return next(apiError);
|
||||
}
|
||||
} else if (!roles.includes(user.role)) {
|
||||
apiError.status = httpStatus.FORBIDDEN;
|
||||
apiError.message = 'Forbidden';
|
||||
return next(apiError);
|
||||
} else if (err || !user) {
|
||||
return next(apiError);
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
exports.ADMIN = ADMIN;
|
||||
exports.LOGGED_USER = LOGGED_USER;
|
||||
|
||||
exports.authorize = (roles = User.roles) => (req, res, next) =>
|
||||
passport.authenticate(
|
||||
'jwt', { session: false },
|
||||
handleJWT(req, res, next, roles),
|
||||
)(req, res, next);
|
||||
|
||||
exports.oAuth = service =>
|
||||
passport.authenticate(service, { session: false });
|
62
src/api/middlewares/error.js
Normal file
62
src/api/middlewares/error.js
Normal file
@ -0,0 +1,62 @@
|
||||
const httpStatus = require('http-status');
|
||||
const expressValidation = require('express-validation');
|
||||
const APIError = require('../utils/APIError');
|
||||
const { env } = require('../../config/vars');
|
||||
|
||||
/**
|
||||
* Error handler. Send stacktrace only during development
|
||||
* @public
|
||||
*/
|
||||
const handler = (err, req, res, next) => {
|
||||
const response = {
|
||||
code: err.status,
|
||||
message: err.message || httpStatus[err.status],
|
||||
errors: err.errors,
|
||||
stack: err.stack,
|
||||
};
|
||||
|
||||
if (env !== 'development') {
|
||||
delete response.stack;
|
||||
}
|
||||
|
||||
res.status(err.status);
|
||||
res.json(response);
|
||||
};
|
||||
exports.handler = handler;
|
||||
|
||||
/**
|
||||
* If error is not an instanceOf APIError, convert it.
|
||||
* @public
|
||||
*/
|
||||
exports.converter = (err, req, res, next) => {
|
||||
let convertedError = err;
|
||||
|
||||
if (err instanceof expressValidation.ValidationError) {
|
||||
convertedError = new APIError({
|
||||
message: 'Validation Error',
|
||||
errors: err.errors,
|
||||
status: err.status,
|
||||
stack: err.stack,
|
||||
});
|
||||
} else if (!(err instanceof APIError)) {
|
||||
convertedError = new APIError({
|
||||
message: err.message,
|
||||
status: err.status,
|
||||
stack: err.stack,
|
||||
});
|
||||
}
|
||||
|
||||
return handler(convertedError, req, res);
|
||||
};
|
||||
|
||||
/**
|
||||
* Catch 404 and forward to error handler
|
||||
* @public
|
||||
*/
|
||||
exports.notFound = (req, res, next) => {
|
||||
const err = new APIError({
|
||||
message: 'Not found',
|
||||
status: httpStatus.NOT_FOUND,
|
||||
});
|
||||
return handler(err, req, res);
|
||||
};
|
57
src/api/models/passwordResetToken.model.js
Normal file
57
src/api/models/passwordResetToken.model.js
Normal file
@ -0,0 +1,57 @@
|
||||
const mongoose = require('mongoose');
|
||||
const crypto = require('crypto');
|
||||
const moment = require('moment-timezone');
|
||||
|
||||
/**
|
||||
* Refresh Token Schema
|
||||
* @private
|
||||
*/
|
||||
const passwordResetTokenSchema = new mongoose.Schema({
|
||||
resetToken: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
userEmail: {
|
||||
type: 'String',
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
expires: { type: Date },
|
||||
});
|
||||
|
||||
passwordResetTokenSchema.statics = {
|
||||
/**
|
||||
* Generate a reset token object and saves it into the database
|
||||
*
|
||||
* @param {User} user
|
||||
* @returns {ResetToken}
|
||||
*/
|
||||
async generate(user) {
|
||||
const userId = user._id;
|
||||
const userEmail = user.email;
|
||||
const resetToken = `${userId}.${crypto.randomBytes(40).toString('hex')}`;
|
||||
const expires = moment()
|
||||
.add(2, 'hours')
|
||||
.toDate();
|
||||
const ResetTokenObject = new PasswordResetToken({
|
||||
resetToken,
|
||||
userId,
|
||||
userEmail,
|
||||
expires,
|
||||
});
|
||||
await ResetTokenObject.save();
|
||||
return ResetTokenObject;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef RefreshToken
|
||||
*/
|
||||
const PasswordResetToken = mongoose.model('PasswordResetToken', passwordResetTokenSchema);
|
||||
module.exports = PasswordResetToken;
|
54
src/api/models/refreshToken.model.js
Normal file
54
src/api/models/refreshToken.model.js
Normal file
@ -0,0 +1,54 @@
|
||||
const mongoose = require('mongoose');
|
||||
const crypto = require('crypto');
|
||||
const moment = require('moment-timezone');
|
||||
|
||||
/**
|
||||
* Refresh Token Schema
|
||||
* @private
|
||||
*/
|
||||
const refreshTokenSchema = new mongoose.Schema({
|
||||
token: {
|
||||
type: String,
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
userEmail: {
|
||||
type: 'String',
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
expires: { type: Date },
|
||||
});
|
||||
|
||||
refreshTokenSchema.statics = {
|
||||
|
||||
/**
|
||||
* Generate a refresh token object and saves it into the database
|
||||
*
|
||||
* @param {User} user
|
||||
* @returns {RefreshToken}
|
||||
*/
|
||||
generate(user) {
|
||||
const userId = user._id;
|
||||
const userEmail = user.email;
|
||||
const token = `${userId}.${crypto.randomBytes(40).toString('hex')}`;
|
||||
const expires = moment().add(30, 'days').toDate();
|
||||
const tokenObject = new RefreshToken({
|
||||
token, userId, userEmail, expires,
|
||||
});
|
||||
tokenObject.save();
|
||||
return tokenObject;
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef RefreshToken
|
||||
*/
|
||||
const RefreshToken = mongoose.model('RefreshToken', refreshTokenSchema);
|
||||
module.exports = RefreshToken;
|
236
src/api/models/user.model.js
Normal file
236
src/api/models/user.model.js
Normal file
@ -0,0 +1,236 @@
|
||||
const mongoose = require('mongoose');
|
||||
const httpStatus = require('http-status');
|
||||
const { omitBy, isNil } = require('lodash');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const moment = require('moment-timezone');
|
||||
const jwt = require('jwt-simple');
|
||||
const uuidv4 = require('uuid/v4');
|
||||
const APIError = require('../utils/APIError');
|
||||
const { env, jwtSecret, jwtExpirationInterval } = require('../../config/vars');
|
||||
|
||||
/**
|
||||
* User Roles
|
||||
*/
|
||||
const roles = ['user', 'admin'];
|
||||
|
||||
/**
|
||||
* User Schema
|
||||
* @private
|
||||
*/
|
||||
const userSchema = new mongoose.Schema({
|
||||
email: {
|
||||
type: String,
|
||||
match: /^\S+@\S+\.\S+$/,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 6,
|
||||
maxlength: 128,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
maxlength: 128,
|
||||
index: true,
|
||||
trim: true,
|
||||
},
|
||||
services: {
|
||||
facebook: String,
|
||||
google: String,
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: roles,
|
||||
default: 'user',
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
trim: true,
|
||||
},
|
||||
}, {
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Add your
|
||||
* - pre-save hooks
|
||||
* - validations
|
||||
* - virtuals
|
||||
*/
|
||||
userSchema.pre('save', async function save(next) {
|
||||
try {
|
||||
if (!this.isModified('password')) return next();
|
||||
|
||||
const rounds = env === 'test' ? 1 : 10;
|
||||
|
||||
const hash = await bcrypt.hash(this.password, rounds);
|
||||
this.password = hash;
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
userSchema.method({
|
||||
transform() {
|
||||
const transformed = {};
|
||||
const fields = ['id', 'name', 'email', 'picture', 'role', 'createdAt'];
|
||||
|
||||
fields.forEach((field) => {
|
||||
transformed[field] = this[field];
|
||||
});
|
||||
|
||||
return transformed;
|
||||
},
|
||||
|
||||
token() {
|
||||
const payload = {
|
||||
exp: moment().add(jwtExpirationInterval, 'minutes').unix(),
|
||||
iat: moment().unix(),
|
||||
sub: this._id,
|
||||
};
|
||||
return jwt.encode(payload, jwtSecret);
|
||||
},
|
||||
|
||||
async passwordMatches(password) {
|
||||
return bcrypt.compare(password, this.password);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Statics
|
||||
*/
|
||||
userSchema.statics = {
|
||||
|
||||
roles,
|
||||
|
||||
/**
|
||||
* Get user
|
||||
*
|
||||
* @param {ObjectId} id - The objectId of user.
|
||||
* @returns {Promise<User, APIError>}
|
||||
*/
|
||||
async get(id) {
|
||||
try {
|
||||
let user;
|
||||
|
||||
if (mongoose.Types.ObjectId.isValid(id)) {
|
||||
user = await this.findById(id).exec();
|
||||
}
|
||||
if (user) {
|
||||
return user;
|
||||
}
|
||||
|
||||
throw new APIError({
|
||||
message: 'User does not exist',
|
||||
status: httpStatus.NOT_FOUND,
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find user by email and tries to generate a JWT token
|
||||
*
|
||||
* @param {ObjectId} id - The objectId of user.
|
||||
* @returns {Promise<User, APIError>}
|
||||
*/
|
||||
async findAndGenerateToken(options) {
|
||||
const { email, password, refreshObject } = options;
|
||||
if (!email) throw new APIError({ message: 'An email is required to generate a token' });
|
||||
|
||||
const user = await this.findOne({ email }).exec();
|
||||
const err = {
|
||||
status: httpStatus.UNAUTHORIZED,
|
||||
isPublic: true,
|
||||
};
|
||||
if (password) {
|
||||
if (user && await user.passwordMatches(password)) {
|
||||
return { user, accessToken: user.token() };
|
||||
}
|
||||
err.message = 'Incorrect email or password';
|
||||
} else if (refreshObject && refreshObject.userEmail === email) {
|
||||
if (moment(refreshObject.expires).isBefore()) {
|
||||
err.message = 'Invalid refresh token.';
|
||||
} else {
|
||||
return { user, accessToken: user.token() };
|
||||
}
|
||||
} else {
|
||||
err.message = 'Incorrect email or refreshToken';
|
||||
}
|
||||
throw new APIError(err);
|
||||
},
|
||||
|
||||
/**
|
||||
* List users in descending order of 'createdAt' timestamp.
|
||||
*
|
||||
* @param {number} skip - Number of users to be skipped.
|
||||
* @param {number} limit - Limit number of users to be returned.
|
||||
* @returns {Promise<User[]>}
|
||||
*/
|
||||
list({
|
||||
page = 1, perPage = 30, name, email, role,
|
||||
}) {
|
||||
const options = omitBy({ name, email, role }, isNil);
|
||||
|
||||
return this.find(options)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(perPage * (page - 1))
|
||||
.limit(perPage)
|
||||
.exec();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return new validation error
|
||||
* if error is a mongoose duplicate key error
|
||||
*
|
||||
* @param {Error} error
|
||||
* @returns {Error|APIError}
|
||||
*/
|
||||
checkDuplicateEmail(error) {
|
||||
if (error.name === 'MongoError' && error.code === 11000) {
|
||||
return new APIError({
|
||||
message: 'Validation Error',
|
||||
errors: [{
|
||||
field: 'email',
|
||||
location: 'body',
|
||||
messages: ['"email" already exists'],
|
||||
}],
|
||||
status: httpStatus.CONFLICT,
|
||||
isPublic: true,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
return error;
|
||||
},
|
||||
|
||||
async oAuthLogin({
|
||||
service, id, email, name, picture,
|
||||
}) {
|
||||
const user = await this.findOne({ $or: [{ [`services.${service}`]: id }, { email }] });
|
||||
if (user) {
|
||||
user.services[service] = id;
|
||||
if (!user.name) user.name = name;
|
||||
if (!user.picture) user.picture = picture;
|
||||
return user.save();
|
||||
}
|
||||
const password = uuidv4();
|
||||
return this.create({
|
||||
services: { [service]: id }, email, password, name, picture,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef User
|
||||
*/
|
||||
module.exports = mongoose.model('User', userSchema);
|
150
src/api/routes/v1/auth.route.js
Normal file
150
src/api/routes/v1/auth.route.js
Normal file
@ -0,0 +1,150 @@
|
||||
const express = require('express');
|
||||
const validate = require('express-validation');
|
||||
const controller = require('../../controllers/auth.controller');
|
||||
const oAuthLogin = require('../../middlewares/auth').oAuth;
|
||||
const {
|
||||
login,
|
||||
register,
|
||||
oAuth,
|
||||
refresh,
|
||||
sendPasswordReset,
|
||||
passwordReset,
|
||||
} = require('../../validations/auth.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* @api {post} v1/auth/register Register
|
||||
* @apiDescription Register a new user
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Register
|
||||
* @apiGroup Auth
|
||||
* @apiPermission public
|
||||
*
|
||||
* @apiParam {String} email User's email
|
||||
* @apiParam {String{6..128}} password User's password
|
||||
*
|
||||
* @apiSuccess (Created 201) {String} token.tokenType Access Token's type
|
||||
* @apiSuccess (Created 201) {String} token.accessToken Authorization Token
|
||||
* @apiSuccess (Created 201) {String} token.refreshToken Token to get a new accessToken
|
||||
* after expiration time
|
||||
* @apiSuccess (Created 201) {Number} token.expiresIn Access Token's expiration time
|
||||
* in miliseconds
|
||||
* @apiSuccess (Created 201) {String} token.timezone The server's Timezone
|
||||
*
|
||||
* @apiSuccess (Created 201) {String} user.id User's id
|
||||
* @apiSuccess (Created 201) {String} user.name User's name
|
||||
* @apiSuccess (Created 201) {String} user.email User's email
|
||||
* @apiSuccess (Created 201) {String} user.role User's role
|
||||
* @apiSuccess (Created 201) {Date} user.createdAt Timestamp
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
*/
|
||||
router.route('/register')
|
||||
.post(validate(register), controller.register);
|
||||
|
||||
|
||||
/**
|
||||
* @api {post} v1/auth/login Login
|
||||
* @apiDescription Get an accessToken
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName Login
|
||||
* @apiGroup Auth
|
||||
* @apiPermission public
|
||||
*
|
||||
* @apiParam {String} email User's email
|
||||
* @apiParam {String{..128}} password User's password
|
||||
*
|
||||
* @apiSuccess {String} token.tokenType Access Token's type
|
||||
* @apiSuccess {String} token.accessToken Authorization Token
|
||||
* @apiSuccess {String} token.refreshToken Token to get a new accessToken
|
||||
* after expiration time
|
||||
* @apiSuccess {Number} token.expiresIn Access Token's expiration time
|
||||
* in miliseconds
|
||||
*
|
||||
* @apiSuccess {String} user.id User's id
|
||||
* @apiSuccess {String} user.name User's name
|
||||
* @apiSuccess {String} user.email User's email
|
||||
* @apiSuccess {String} user.role User's role
|
||||
* @apiSuccess {Date} user.createdAt Timestamp
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
* @apiError (Unauthorized 401) Unauthorized Incorrect email or password
|
||||
*/
|
||||
router.route('/login')
|
||||
.post(validate(login), controller.login);
|
||||
|
||||
|
||||
/**
|
||||
* @api {post} v1/auth/refresh-token Refresh Token
|
||||
* @apiDescription Refresh expired accessToken
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName RefreshToken
|
||||
* @apiGroup Auth
|
||||
* @apiPermission public
|
||||
*
|
||||
* @apiParam {String} email User's email
|
||||
* @apiParam {String} refreshToken Refresh token aquired when user logged in
|
||||
*
|
||||
* @apiSuccess {String} tokenType Access Token's type
|
||||
* @apiSuccess {String} accessToken Authorization Token
|
||||
* @apiSuccess {String} refreshToken Token to get a new accessToken after expiration time
|
||||
* @apiSuccess {Number} expiresIn Access Token's expiration time in miliseconds
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
* @apiError (Unauthorized 401) Unauthorized Incorrect email or refreshToken
|
||||
*/
|
||||
router.route('/refresh-token')
|
||||
.post(validate(refresh), controller.refresh);
|
||||
|
||||
|
||||
router.route('/send-password-reset')
|
||||
.post(validate(sendPasswordReset), controller.sendPasswordReset);
|
||||
|
||||
router.route('/reset-password')
|
||||
.post(validate(passwordReset), controller.resetPassword);
|
||||
|
||||
/**
|
||||
* @api {post} v1/auth/facebook Facebook Login
|
||||
* @apiDescription Login with facebook. Creates a new user if it does not exist
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName FacebookLogin
|
||||
* @apiGroup Auth
|
||||
* @apiPermission public
|
||||
*
|
||||
* @apiParam {String} access_token Facebook's access_token
|
||||
*
|
||||
* @apiSuccess {String} tokenType Access Token's type
|
||||
* @apiSuccess {String} accessToken Authorization Token
|
||||
* @apiSuccess {String} refreshToken Token to get a new accessToken after expiration time
|
||||
* @apiSuccess {Number} expiresIn Access Token's expiration time in miliseconds
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
* @apiError (Unauthorized 401) Unauthorized Incorrect access_token
|
||||
*/
|
||||
router.route('/facebook')
|
||||
.post(validate(oAuth), oAuthLogin('facebook'), controller.oAuth);
|
||||
|
||||
/**
|
||||
* @api {post} v1/auth/google Google Login
|
||||
* @apiDescription Login with google. Creates a new user if it does not exist
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName GoogleLogin
|
||||
* @apiGroup Auth
|
||||
* @apiPermission public
|
||||
*
|
||||
* @apiParam {String} access_token Google's access_token
|
||||
*
|
||||
* @apiSuccess {String} tokenType Access Token's type
|
||||
* @apiSuccess {String} accessToken Authorization Token
|
||||
* @apiSuccess {String} refreshToken Token to get a new accpessToken after expiration time
|
||||
* @apiSuccess {Number} expiresIn Access Token's expiration time in miliseconds
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
* @apiError (Unauthorized 401) Unauthorized Incorrect access_token
|
||||
*/
|
||||
router.route('/google')
|
||||
.post(validate(oAuth), oAuthLogin('google'), controller.oAuth);
|
||||
|
||||
|
||||
module.exports = router;
|
20
src/api/routes/v1/index.js
Normal file
20
src/api/routes/v1/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
const userRoutes = require('./user.route');
|
||||
const authRoutes = require('./auth.route');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET v1/status
|
||||
*/
|
||||
router.get('/status', (req, res) => res.send('OK'));
|
||||
|
||||
/**
|
||||
* GET v1/docs
|
||||
*/
|
||||
router.use('/docs', express.static('docs'));
|
||||
|
||||
router.use('/users', userRoutes);
|
||||
router.use('/auth', authRoutes);
|
||||
|
||||
module.exports = router;
|
193
src/api/routes/v1/user.route.js
Normal file
193
src/api/routes/v1/user.route.js
Normal file
@ -0,0 +1,193 @@
|
||||
const express = require('express');
|
||||
const validate = require('express-validation');
|
||||
const controller = require('../../controllers/user.controller');
|
||||
const { authorize, ADMIN, LOGGED_USER } = require('../../middlewares/auth');
|
||||
const {
|
||||
listUsers,
|
||||
createUser,
|
||||
replaceUser,
|
||||
updateUser,
|
||||
} = require('../../validations/user.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* Load user when API with userId route parameter is hit
|
||||
*/
|
||||
router.param('userId', controller.load);
|
||||
|
||||
|
||||
router
|
||||
.route('/')
|
||||
/**
|
||||
* @api {get} v1/users List Users
|
||||
* @apiDescription Get a list of users
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName ListUsers
|
||||
* @apiGroup User
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiHeader {String} Authorization User's access token
|
||||
*
|
||||
* @apiParam {Number{1-}} [page=1] List page
|
||||
* @apiParam {Number{1-100}} [perPage=1] Users per page
|
||||
* @apiParam {String} [name] User's name
|
||||
* @apiParam {String} [email] User's email
|
||||
* @apiParam {String=user,admin} [role] User's role
|
||||
*
|
||||
* @apiSuccess {Object[]} users List of users.
|
||||
*
|
||||
* @apiError (Unauthorized 401) Unauthorized Only authenticated users can access the data
|
||||
* @apiError (Forbidden 403) Forbidden Only admins can access the data
|
||||
*/
|
||||
.get(authorize(ADMIN), validate(listUsers), controller.list)
|
||||
/**
|
||||
* @api {post} v1/users Create User
|
||||
* @apiDescription Create a new user
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName CreateUser
|
||||
* @apiGroup User
|
||||
* @apiPermission admin
|
||||
*
|
||||
* @apiHeader {String} Authorization User's access token
|
||||
*
|
||||
* @apiParam {String} email User's email
|
||||
* @apiParam {String{6..128}} password User's password
|
||||
* @apiParam {String{..128}} [name] User's name
|
||||
* @apiParam {String=user,admin} [role] User's role
|
||||
*
|
||||
* @apiSuccess (Created 201) {String} id User's id
|
||||
* @apiSuccess (Created 201) {String} name User's name
|
||||
* @apiSuccess (Created 201) {String} email User's email
|
||||
* @apiSuccess (Created 201) {String} role User's role
|
||||
* @apiSuccess (Created 201) {Date} createdAt Timestamp
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
* @apiError (Unauthorized 401) Unauthorized Only authenticated users can create the data
|
||||
* @apiError (Forbidden 403) Forbidden Only admins can create the data
|
||||
*/
|
||||
.post(authorize(ADMIN), validate(createUser), controller.create);
|
||||
|
||||
|
||||
router
|
||||
.route('/profile')
|
||||
/**
|
||||
* @api {get} v1/users/profile User Profile
|
||||
* @apiDescription Get logged in user profile information
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName UserProfile
|
||||
* @apiGroup User
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiHeader {String} Authorization User's access token
|
||||
*
|
||||
* @apiSuccess {String} id User's id
|
||||
* @apiSuccess {String} name User's name
|
||||
* @apiSuccess {String} email User's email
|
||||
* @apiSuccess {String} role User's role
|
||||
* @apiSuccess {Date} createdAt Timestamp
|
||||
*
|
||||
* @apiError (Unauthorized 401) Unauthorized Only authenticated Users can access the data
|
||||
*/
|
||||
.get(authorize(), controller.loggedIn);
|
||||
|
||||
|
||||
router
|
||||
.route('/:userId')
|
||||
/**
|
||||
* @api {get} v1/users/:id Get User
|
||||
* @apiDescription Get user information
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName GetUser
|
||||
* @apiGroup User
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiHeader {String} Authorization User's access token
|
||||
*
|
||||
* @apiSuccess {String} id User's id
|
||||
* @apiSuccess {String} name User's name
|
||||
* @apiSuccess {String} email User's email
|
||||
* @apiSuccess {String} role User's role
|
||||
* @apiSuccess {Date} createdAt Timestamp
|
||||
*
|
||||
* @apiError (Unauthorized 401) Unauthorized Only authenticated users can access the data
|
||||
* @apiError (Forbidden 403) Forbidden Only user with same id or admins can access the data
|
||||
* @apiError (Not Found 404) NotFound User does not exist
|
||||
*/
|
||||
.get(authorize(LOGGED_USER), controller.get)
|
||||
/**
|
||||
* @api {put} v1/users/:id Replace User
|
||||
* @apiDescription Replace the whole user document with a new one
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName ReplaceUser
|
||||
* @apiGroup User
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiHeader {String} Authorization User's access token
|
||||
*
|
||||
* @apiParam {String} email User's email
|
||||
* @apiParam {String{6..128}} password User's password
|
||||
* @apiParam {String{..128}} [name] User's name
|
||||
* @apiParam {String=user,admin} [role] User's role
|
||||
* (You must be an admin to change the user's role)
|
||||
*
|
||||
* @apiSuccess {String} id User's id
|
||||
* @apiSuccess {String} name User's name
|
||||
* @apiSuccess {String} email User's email
|
||||
* @apiSuccess {String} role User's role
|
||||
* @apiSuccess {Date} createdAt Timestamp
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
* @apiError (Unauthorized 401) Unauthorized Only authenticated users can modify the data
|
||||
* @apiError (Forbidden 403) Forbidden Only user with same id or admins can modify the data
|
||||
* @apiError (Not Found 404) NotFound User does not exist
|
||||
*/
|
||||
.put(authorize(LOGGED_USER), validate(replaceUser), controller.replace)
|
||||
/**
|
||||
* @api {patch} v1/users/:id Update User
|
||||
* @apiDescription Update some fields of a user document
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName UpdateUser
|
||||
* @apiGroup User
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiHeader {String} Authorization User's access token
|
||||
*
|
||||
* @apiParam {String} email User's email
|
||||
* @apiParam {String{6..128}} password User's password
|
||||
* @apiParam {String{..128}} [name] User's name
|
||||
* @apiParam {String=user,admin} [role] User's role
|
||||
* (You must be an admin to change the user's role)
|
||||
*
|
||||
* @apiSuccess {String} id User's id
|
||||
* @apiSuccess {String} name User's name
|
||||
* @apiSuccess {String} email User's email
|
||||
* @apiSuccess {String} role User's role
|
||||
* @apiSuccess {Date} createdAt Timestamp
|
||||
*
|
||||
* @apiError (Bad Request 400) ValidationError Some parameters may contain invalid values
|
||||
* @apiError (Unauthorized 401) Unauthorized Only authenticated users can modify the data
|
||||
* @apiError (Forbidden 403) Forbidden Only user with same id or admins can modify the data
|
||||
* @apiError (Not Found 404) NotFound User does not exist
|
||||
*/
|
||||
.patch(authorize(LOGGED_USER), validate(updateUser), controller.update)
|
||||
/**
|
||||
* @api {patch} v1/users/:id Delete User
|
||||
* @apiDescription Delete a user
|
||||
* @apiVersion 1.0.0
|
||||
* @apiName DeleteUser
|
||||
* @apiGroup User
|
||||
* @apiPermission user
|
||||
*
|
||||
* @apiHeader {String} Authorization User's access token
|
||||
*
|
||||
* @apiSuccess (No Content 204) Successfully deleted
|
||||
*
|
||||
* @apiError (Unauthorized 401) Unauthorized Only authenticated users can delete the data
|
||||
* @apiError (Forbidden 403) Forbidden Only user with same id or admins can delete the data
|
||||
* @apiError (Not Found 404) NotFound User does not exist
|
||||
*/
|
||||
.delete(authorize(LOGGED_USER), controller.remove);
|
||||
|
||||
|
||||
module.exports = router;
|
35
src/api/services/authProviders.js
Normal file
35
src/api/services/authProviders.js
Normal file
@ -0,0 +1,35 @@
|
||||
/* eslint-disable camelcase */
|
||||
const axios = require('axios');
|
||||
|
||||
exports.facebook = async (access_token) => {
|
||||
const fields = 'id, name, email, picture';
|
||||
const url = 'https://graph.facebook.com/me';
|
||||
const params = { access_token, fields };
|
||||
const response = await axios.get(url, { params });
|
||||
const {
|
||||
id, name, email, picture,
|
||||
} = response.data;
|
||||
return {
|
||||
service: 'facebook',
|
||||
picture: picture.data.url,
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
};
|
||||
};
|
||||
|
||||
exports.google = async (access_token) => {
|
||||
const url = 'https://www.googleapis.com/oauth2/v3/userinfo';
|
||||
const params = { access_token };
|
||||
const response = await axios.get(url, { params });
|
||||
const {
|
||||
sub, name, email, picture,
|
||||
} = response.data;
|
||||
return {
|
||||
service: 'google',
|
||||
picture,
|
||||
id: sub,
|
||||
name,
|
||||
email,
|
||||
};
|
||||
};
|
77
src/api/services/emails/emailProvider.js
Normal file
77
src/api/services/emails/emailProvider.js
Normal file
@ -0,0 +1,77 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const { emailConfig } = require('../../../config/vars');
|
||||
const Email = require('email-templates');
|
||||
|
||||
// SMTP is the main transport in Nodemailer for delivering messages.
|
||||
// SMTP is also the protocol used between almost all email hosts, so its truly universal.
|
||||
// if you dont want to use SMTP you can create your own transport here
|
||||
// such as an email service API or nodemailer-sendgrid-transport
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
port: emailConfig.port,
|
||||
host: emailConfig.host,
|
||||
auth: {
|
||||
user: emailConfig.username,
|
||||
pass: emailConfig.password,
|
||||
},
|
||||
secure: false, // upgrades later with STARTTLS -- change this based on the PORT
|
||||
});
|
||||
|
||||
// verify connection configuration
|
||||
transporter.verify((error) => {
|
||||
if (error) {
|
||||
console.log('error with email connection');
|
||||
}
|
||||
});
|
||||
|
||||
exports.sendPasswordReset = async (passwordResetObject) => {
|
||||
const email = new Email({
|
||||
views: { root: __dirname },
|
||||
message: {
|
||||
from: 'support@your-app.com',
|
||||
},
|
||||
// uncomment below to send emails in development/test env:
|
||||
send: true,
|
||||
transport: transporter,
|
||||
});
|
||||
|
||||
email
|
||||
.send({
|
||||
template: 'passwordReset',
|
||||
message: {
|
||||
to: passwordResetObject.userEmail,
|
||||
},
|
||||
locals: {
|
||||
productName: 'Test App',
|
||||
// passwordResetUrl should be a URL to your app that displays a view where they
|
||||
// can enter a new password along with passing the resetToken in the params
|
||||
passwordResetUrl: `https://your-app/new-password/view?resetToken=${passwordResetObject.resetToken}`,
|
||||
},
|
||||
})
|
||||
.catch(() => console.log('error sending password reset email'));
|
||||
};
|
||||
|
||||
exports.sendPasswordChangeEmail = async (user) => {
|
||||
const email = new Email({
|
||||
views: { root: __dirname },
|
||||
message: {
|
||||
from: 'support@your-app.com',
|
||||
},
|
||||
// uncomment below to send emails in development/test env:
|
||||
send: true,
|
||||
transport: transporter,
|
||||
});
|
||||
|
||||
email
|
||||
.send({
|
||||
template: 'passwordChange',
|
||||
message: {
|
||||
to: user.email,
|
||||
},
|
||||
locals: {
|
||||
productName: 'Test App',
|
||||
name: user.name,
|
||||
},
|
||||
})
|
||||
.catch(() => console.log('error sending change password email'));
|
||||
};
|
407
src/api/services/emails/passwordChange/html.pug
Normal file
407
src/api/services/emails/passwordChange/html.pug
Normal file
@ -0,0 +1,407 @@
|
||||
doctype transitional
|
||||
head
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
||||
meta(name='x-apple-disable-message-reformatting')
|
||||
meta(http-equiv='Content-Type' content='text/html; charset=UTF-8')
|
||||
title
|
||||
style(type='text/css' rel='stylesheet' media='all').
|
||||
/* Base ------------------------------ */
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
border-right: 18px solid #FF6136;
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 35px 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
body {
|
||||
background-color: #FFF;
|
||||
color: #333;
|
||||
}
|
||||
p {
|
||||
color: #333;
|
||||
}
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
.content-cell {
|
||||
padding: 35px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.attributes_content,
|
||||
.discount {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
//if mso
|
||||
style(type='text/css').
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
style(type='text/css' rel='stylesheet' media='all').
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
body {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
body {
|
||||
background-color: #FFF;
|
||||
color: #333;
|
||||
}
|
||||
span.preheader(style='display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;')
|
||||
| Hello #{name},
|
||||
table.email-wrapper(width='100%' cellpadding='0' cellspacing='0' role='presentation' style='width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;')
|
||||
tr
|
||||
td(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
table.email-content(width='100%' cellpadding='0' cellspacing='0' role='presentation' style='width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;')
|
||||
tr
|
||||
td.email-masthead(style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; text-align: center; padding: 25px 0;' align='center')
|
||||
a.f-fallback.email-masthead_name(href='https://example.com' style='color: #A8AAAF; font-size: 16px; font-weight: bold; text-decoration: none; text-shadow: 0 1px 0 white;')
|
||||
| #{productName}
|
||||
// Email Body
|
||||
tr
|
||||
td.email-body(width='570' cellpadding='0' cellspacing='0' style='word-break: break-word; margin: 0; padding: 0; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0;')
|
||||
table.email-body_inner(align='center' width='570' cellpadding='0' cellspacing='0' role='presentation' style='width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0 auto; padding: 0;')
|
||||
// Body content
|
||||
tr
|
||||
td.content-cell(style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; padding: 35px;')
|
||||
.f-fallback
|
||||
h1(style='margin-top: 0; color: #333333; font-size: 22px; font-weight: bold; text-align: left;' align='left') Hello,
|
||||
p(style='font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;')
|
||||
| Your password for #{productName} has been changed successfully. You can now login with your new password
|
||||
// Action
|
||||
table.body-action(align='center' width='100%' cellpadding='0' cellspacing='0' role='presentation' style='width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 30px auto; padding: 0;')
|
||||
tr
|
||||
td(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
//
|
||||
Border based button
|
||||
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design
|
||||
table(width='100%' border='0' cellspacing='0' cellpadding='0' role='presentation')
|
||||
tr
|
||||
td(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
|
||||
tr
|
||||
td(style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
table.email-footer(align='center' width='570' cellpadding='0' cellspacing='0' role='presentation' style='width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 0 auto; padding: 0;')
|
||||
tr
|
||||
td.content-cell(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; padding: 35px;')
|
||||
p.f-fallback.sub.align-center(style='font-size: 13px; line-height: 1.625; text-align: center; color: #A8AAAF; margin: .4em 0 1.1875em;' align='center') © 2019 #{productName}. All rights reserved.
|
1
src/api/services/emails/passwordChange/subject.pug
Normal file
1
src/api/services/emails/passwordChange/subject.pug
Normal file
@ -0,0 +1 @@
|
||||
= `${productName} Password Changed`
|
436
src/api/services/emails/passwordReset/html.pug
Normal file
436
src/api/services/emails/passwordReset/html.pug
Normal file
@ -0,0 +1,436 @@
|
||||
doctype transitional
|
||||
head
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1.0')
|
||||
meta(name='x-apple-disable-message-reformatting')
|
||||
meta(http-equiv='Content-Type' content='text/html; charset=UTF-8')
|
||||
title
|
||||
style(type='text/css' rel='stylesheet' media='all').
|
||||
/* Base ------------------------------ */
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
border-right: 18px solid #FF6136;
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 35px 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
body {
|
||||
background-color: #FFF;
|
||||
color: #333;
|
||||
}
|
||||
p {
|
||||
color: #333;
|
||||
}
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
.content-cell {
|
||||
padding: 35px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: #FFF !important;
|
||||
}
|
||||
.attributes_content,
|
||||
.discount {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
//if mso
|
||||
style(type='text/css').
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
style(type='text/css' rel='stylesheet' media='all').
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
body {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
body {
|
||||
background-color: #FFF;
|
||||
color: #333;
|
||||
}
|
||||
span.preheader(style='display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;')
|
||||
| Use
|
||||
| this link to reset your password. The link is only valid for 2 hours.
|
||||
table.email-wrapper(width='100%' cellpadding='0' cellspacing='0' role='presentation' style='width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;')
|
||||
tr
|
||||
td(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
table.email-content(width='100%' cellpadding='0' cellspacing='0' role='presentation' style='width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;')
|
||||
tr
|
||||
td.email-masthead(style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; text-align: center; padding: 25px 0;' align='center')
|
||||
a.f-fallback.email-masthead_name(href='https://example.com' style='color: #A8AAAF; font-size: 16px; font-weight: bold; text-decoration: none; text-shadow: 0 1px 0 white;')
|
||||
#{productName}
|
||||
// Email Body
|
||||
tr
|
||||
td.email-body(width='570' cellpadding='0' cellspacing='0' style='word-break: break-word; margin: 0; padding: 0; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0;')
|
||||
table.email-body_inner(align='center' width='570' cellpadding='0' cellspacing='0' role='presentation' style='width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0 auto; padding: 0;')
|
||||
// Body content
|
||||
tr
|
||||
td.content-cell(style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; padding: 35px;')
|
||||
.f-fallback
|
||||
h1(style='margin-top: 0; color: #333333; font-size: 22px; font-weight: bold; text-align: left;' align='left') Hi,
|
||||
p(style='font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;')
|
||||
| You recently requested to reset your password for your #{productName}
|
||||
| account. Use the button below to reset it.
|
||||
strong
|
||||
| This password reset
|
||||
| is only valid for the next 2 hours.
|
||||
// Action
|
||||
table.body-action(align='center' width='100%' cellpadding='0' cellspacing='0' role='presentation' style='width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 30px auto; padding: 0;')
|
||||
tr
|
||||
td(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
//
|
||||
Border based button
|
||||
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design
|
||||
table(width='100%' border='0' cellspacing='0' cellpadding='0' role='presentation')
|
||||
tr
|
||||
td(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
a.f-fallback.button.button--green(href='#{passwordResetUrl}' target='_blank' style='color: #FFF; border-color: #22bc66; border-style: solid; border-width: 10px 18px; background-color: #22BC66; display: inline-block; text-decoration: none; border-radius: 3px; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); -webkit-text-size-adjust: none; box-sizing: border-box;')
|
||||
| Reset
|
||||
| your password
|
||||
p(style='font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;')
|
||||
| If you did not request a password reset,
|
||||
| please ignore this email.
|
||||
p(style='font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;')
|
||||
| Thanks,
|
||||
br
|
||||
| The #{productName} Team
|
||||
// Sub copy
|
||||
table.body-sub(role='presentation' style='margin-top: 25px; padding-top: 25px; border-top-width: 1px; border-top-color: #EAEAEC; border-top-style: solid;')
|
||||
tr
|
||||
td(style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
p.f-fallback.sub(style='font-size: 13px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;')
|
||||
| If you’re having trouble with the button above, copy and
|
||||
| paste the URL below into your web browser.
|
||||
p.f-fallback.sub(style='font-size: 13px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;')
|
||||
| #{passwordResetUrl}
|
||||
tr
|
||||
td(style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;')
|
||||
table.email-footer(align='center' width='570' cellpadding='0' cellspacing='0' role='presentation' style='width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 0 auto; padding: 0;')
|
||||
tr
|
||||
td.content-cell(align='center' style='word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; padding: 35px;')
|
||||
p.f-fallback.sub.align-center(style='font-size: 13px; line-height: 1.625; text-align: center; color: #A8AAAF; margin: .4em 0 1.1875em;' align='center') © 2019 [Product Name]. All rights reserved.
|
||||
p.f-fallback.sub.align-center(style='font-size: 13px; line-height: 1.625; text-align: center; color: #A8AAAF; margin: .4em 0 1.1875em;' align='center')
|
||||
| [Company Name, LLC]
|
||||
br
|
||||
| 1234 Street Rd.
|
||||
br
|
||||
| Suite 1234
|
1
src/api/services/emails/passwordReset/subject.pug
Normal file
1
src/api/services/emails/passwordReset/subject.pug
Normal file
@ -0,0 +1 @@
|
||||
= `${productName} Password Reset`
|
528
src/api/tests/integration/auth.test.js
Normal file
528
src/api/tests/integration/auth.test.js
Normal file
@ -0,0 +1,528 @@
|
||||
/* eslint-disable arrow-body-style */
|
||||
const request = require('supertest');
|
||||
const httpStatus = require('http-status');
|
||||
const { expect } = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const moment = require('moment-timezone');
|
||||
const app = require('../../../index');
|
||||
const User = require('../../models/user.model');
|
||||
const RefreshToken = require('../../models/refreshToken.model');
|
||||
const PasswordResetToken = require('../../models/passwordResetToken.model');
|
||||
const authProviders = require('../../services/authProviders');
|
||||
const emailProvider = require('../../services/emails/emailProvider');
|
||||
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
const fakeOAuthRequest = () =>
|
||||
Promise.resolve({
|
||||
service: 'facebook',
|
||||
id: '123',
|
||||
name: 'user',
|
||||
email: 'test@test.com',
|
||||
picture: 'test.jpg',
|
||||
});
|
||||
|
||||
describe('Authentication API', () => {
|
||||
let dbUser;
|
||||
let user;
|
||||
let refreshToken;
|
||||
let resetToken;
|
||||
let expiredRefreshToken;
|
||||
let expiredResetToken;
|
||||
|
||||
beforeEach(async () => {
|
||||
dbUser = {
|
||||
email: 'branstark@gmail.com',
|
||||
password: 'mypassword',
|
||||
name: 'Bran Stark',
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
user = {
|
||||
email: 'krish@gmail.com',
|
||||
password: '123456',
|
||||
name: 'krish Sousa',
|
||||
};
|
||||
|
||||
refreshToken = {
|
||||
token:
|
||||
'5947397b323ae82d8c3a333b.c69d0435e62c9f4953af912442a3d064e20291f0d228c0552ed4be473e7d191ba40b18c2c47e8b9d',
|
||||
userId: '5947397b323ae82d8c3a333b',
|
||||
userEmail: dbUser.email,
|
||||
expires: moment()
|
||||
.add(1, 'day')
|
||||
.toDate(),
|
||||
};
|
||||
|
||||
resetToken = {
|
||||
resetToken:
|
||||
'5947397b323ae82d8c3a333b.c69d0435e62c9f4953af912442a3d064e20291f0d228c0552ed4be473e7d191ba40b18c2c47e8b9d',
|
||||
userId: '5947397b323ae82d8c3a333b',
|
||||
userEmail: dbUser.email,
|
||||
expires: moment()
|
||||
.add(2, 'hours')
|
||||
.toDate(),
|
||||
};
|
||||
|
||||
expiredRefreshToken = {
|
||||
token:
|
||||
'5947397b323ae82d8c3a333b.c69d0435e62c9f4953af912442a3d064e20291f0d228c0552ed4be473e7d191ba40b18c2c47e8b9d',
|
||||
userId: '5947397b323ae82d8c3a333b',
|
||||
userEmail: dbUser.email,
|
||||
expires: moment()
|
||||
.subtract(1, 'day')
|
||||
.toDate(),
|
||||
};
|
||||
|
||||
expiredResetToken = {
|
||||
resetToken:
|
||||
'5947397b323ae82d8c3a333b.c69d0435e62c9f4953af912442a3d064e20291f0d228c0552ed4be473e7d191ba40b18c2c47e8b9d',
|
||||
userId: '5947397b323ae82d8c3a333b',
|
||||
userEmail: dbUser.email,
|
||||
expires: moment()
|
||||
.subtract(2, 'hours')
|
||||
.toDate(),
|
||||
};
|
||||
|
||||
await User.deleteMany({});
|
||||
await User.create(dbUser);
|
||||
await RefreshToken.deleteMany({});
|
||||
await PasswordResetToken.deleteMany({});
|
||||
});
|
||||
|
||||
afterEach(() => sandbox.restore());
|
||||
|
||||
describe('POST /v1/auth/register', () => {
|
||||
it('should register a new user when request is ok', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/register')
|
||||
.send(user)
|
||||
.expect(httpStatus.CREATED)
|
||||
.then((res) => {
|
||||
delete user.password;
|
||||
expect(res.body.token).to.have.a.property('accessToken');
|
||||
expect(res.body.token).to.have.a.property('refreshToken');
|
||||
expect(res.body.token).to.have.a.property('expiresIn');
|
||||
expect(res.body.user).to.include(user);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email already exists', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/register')
|
||||
.send(dbUser)
|
||||
.expect(httpStatus.CONFLICT)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" already exists');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when the email provided is not valid', () => {
|
||||
user.email = 'this_is_not_an_email';
|
||||
return request(app)
|
||||
.post('/v1/auth/register')
|
||||
.send(user)
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" must be a valid email');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email and password are not provided', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/register')
|
||||
.send({})
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" is required');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /v1/auth/login', () => {
|
||||
it('should return an accessToken and a refreshToken when email and password matches', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/login')
|
||||
.send(dbUser)
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
delete dbUser.password;
|
||||
expect(res.body.token).to.have.a.property('accessToken');
|
||||
expect(res.body.token).to.have.a.property('refreshToken');
|
||||
expect(res.body.token).to.have.a.property('expiresIn');
|
||||
expect(res.body.user).to.include(dbUser);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email and password are not provided', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/login')
|
||||
.send({})
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" is required');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when the email provided is not valid', () => {
|
||||
user.email = 'this_is_not_an_email';
|
||||
return request(app)
|
||||
.post('/v1/auth/login')
|
||||
.send(user)
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" must be a valid email');
|
||||
});
|
||||
});
|
||||
|
||||
it("should report error when email and password don't match", () => {
|
||||
dbUser.password = 'xxx';
|
||||
return request(app)
|
||||
.post('/v1/auth/login')
|
||||
.send(dbUser)
|
||||
.expect(httpStatus.UNAUTHORIZED)
|
||||
.then((res) => {
|
||||
const { code } = res.body;
|
||||
const { message } = res.body;
|
||||
expect(code).to.be.equal(401);
|
||||
expect(message).to.be.equal('Incorrect email or password');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /v1/auth/facebook', () => {
|
||||
it('should create a new user and return an accessToken when user does not exist', () => {
|
||||
sandbox.stub(authProviders, 'facebook').callsFake(fakeOAuthRequest);
|
||||
return request(app)
|
||||
.post('/v1/auth/facebook')
|
||||
.send({ access_token: '123' })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body.token).to.have.a.property('accessToken');
|
||||
expect(res.body.token).to.have.a.property('refreshToken');
|
||||
expect(res.body.token).to.have.a.property('expiresIn');
|
||||
expect(res.body.user).to.be.an('object');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an accessToken when user already exists', async () => {
|
||||
dbUser.email = 'test@test.com';
|
||||
await User.create(dbUser);
|
||||
sandbox.stub(authProviders, 'facebook').callsFake(fakeOAuthRequest);
|
||||
return request(app)
|
||||
.post('/v1/auth/facebook')
|
||||
.send({ access_token: '123' })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body.token).to.have.a.property('accessToken');
|
||||
expect(res.body.token).to.have.a.property('refreshToken');
|
||||
expect(res.body.token).to.have.a.property('expiresIn');
|
||||
expect(res.body.user).to.be.an('object');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when access_token is not provided', async () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/facebook')
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('access_token');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"access_token" is required');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /v1/auth/google', () => {
|
||||
it('should create a new user and return an accessToken when user does not exist', () => {
|
||||
sandbox.stub(authProviders, 'google').callsFake(fakeOAuthRequest);
|
||||
return request(app)
|
||||
.post('/v1/auth/google')
|
||||
.send({ access_token: '123' })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body.token).to.have.a.property('accessToken');
|
||||
expect(res.body.token).to.have.a.property('refreshToken');
|
||||
expect(res.body.token).to.have.a.property('expiresIn');
|
||||
expect(res.body.user).to.be.an('object');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an accessToken when user already exists', async () => {
|
||||
dbUser.email = 'test@test.com';
|
||||
await User.create(dbUser);
|
||||
sandbox.stub(authProviders, 'google').callsFake(fakeOAuthRequest);
|
||||
return request(app)
|
||||
.post('/v1/auth/google')
|
||||
.send({ access_token: '123' })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body.token).to.have.a.property('accessToken');
|
||||
expect(res.body.token).to.have.a.property('refreshToken');
|
||||
expect(res.body.token).to.have.a.property('expiresIn');
|
||||
expect(res.body.user).to.be.an('object');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error when access_token is not provided', async () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/google')
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('access_token');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"access_token" is required');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /v1/auth/refresh-token', () => {
|
||||
it('should return a new accessToken when refreshToken and email match', async () => {
|
||||
await RefreshToken.create(refreshToken);
|
||||
return request(app)
|
||||
.post('/v1/auth/refresh-token')
|
||||
.send({ email: dbUser.email, refreshToken: refreshToken.token })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body).to.have.a.property('accessToken');
|
||||
expect(res.body).to.have.a.property('refreshToken');
|
||||
expect(res.body).to.have.a.property('expiresIn');
|
||||
});
|
||||
});
|
||||
|
||||
it("should report error when email and refreshToken don't match", async () => {
|
||||
await RefreshToken.create(refreshToken);
|
||||
return request(app)
|
||||
.post('/v1/auth/refresh-token')
|
||||
.send({ email: user.email, refreshToken: refreshToken.token })
|
||||
.expect(httpStatus.UNAUTHORIZED)
|
||||
.then((res) => {
|
||||
const { code } = res.body;
|
||||
const { message } = res.body;
|
||||
expect(code).to.be.equal(401);
|
||||
expect(message).to.be.equal('Incorrect email or refreshToken');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email and refreshToken are not provided', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/refresh-token')
|
||||
.send({})
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const field1 = res.body.errors[0].field;
|
||||
const location1 = res.body.errors[0].location;
|
||||
const messages1 = res.body.errors[0].messages;
|
||||
const field2 = res.body.errors[1].field;
|
||||
const location2 = res.body.errors[1].location;
|
||||
const messages2 = res.body.errors[1].messages;
|
||||
expect(field1).to.be.equal('email');
|
||||
expect(location1).to.be.equal('body');
|
||||
expect(messages1).to.include('"email" is required');
|
||||
expect(field2).to.be.equal('refreshToken');
|
||||
expect(location2).to.be.equal('body');
|
||||
expect(messages2).to.include('"refreshToken" is required');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when the refreshToken is expired', async () => {
|
||||
await RefreshToken.create(expiredRefreshToken);
|
||||
|
||||
return request(app)
|
||||
.post('/v1/auth/refresh-token')
|
||||
.send({ email: dbUser.email, refreshToken: expiredRefreshToken.token })
|
||||
.expect(httpStatus.UNAUTHORIZED)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(401);
|
||||
expect(res.body.message).to.be.equal('Invalid refresh token.');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('POST /v1/auth/send-password-reset', () => {
|
||||
it('should send an email with password reset link when email matches a user', async () => {
|
||||
const PasswordResetTokenObj = await PasswordResetToken.create(resetToken);
|
||||
|
||||
expect(PasswordResetTokenObj.resetToken).to.be.equal('5947397b323ae82d8c3a333b.c69d0435e62c9f4953af912442a3d064e20291f0d228c0552ed4be473e7d191ba40b18c2c47e8b9d');
|
||||
expect(PasswordResetTokenObj.userId.toString()).to.be.equal('5947397b323ae82d8c3a333b');
|
||||
expect(PasswordResetTokenObj.userEmail).to.be.equal(dbUser.email);
|
||||
expect(PasswordResetTokenObj.expires).to.be.above(moment()
|
||||
.add(1, 'hour')
|
||||
.toDate());
|
||||
|
||||
sandbox
|
||||
.stub(emailProvider, 'sendPasswordReset')
|
||||
.callsFake(() => Promise.resolve('email sent'));
|
||||
|
||||
return request(app)
|
||||
.post('/v1/auth/send-password-reset')
|
||||
.send({ email: dbUser.email })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body).to.be.equal('success');
|
||||
});
|
||||
});
|
||||
|
||||
it("should report error when email doesn't match a user", async () => {
|
||||
await PasswordResetToken.create(resetToken);
|
||||
return request(app)
|
||||
.post('/v1/auth/send-password-reset')
|
||||
.send({ email: user.email })
|
||||
.expect(httpStatus.UNAUTHORIZED)
|
||||
.then((res) => {
|
||||
const { code } = res.body;
|
||||
const { message } = res.body;
|
||||
expect(code).to.be.equal(httpStatus.UNAUTHORIZED);
|
||||
expect(message).to.be.equal('No account found with that email');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email is not provided', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/send-password-reset')
|
||||
.send({})
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const field1 = res.body.errors[0].field;
|
||||
const location1 = res.body.errors[0].location;
|
||||
const messages1 = res.body.errors[0].messages;
|
||||
expect(field1).to.be.equal('email');
|
||||
expect(location1).to.be.equal('body');
|
||||
expect(messages1).to.include('"email" is required');
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('POST /v1/auth/reset-password', () => {
|
||||
it('should update password and send confirmation email when email and reset token are valid', async () => {
|
||||
await PasswordResetToken.create(resetToken);
|
||||
|
||||
sandbox
|
||||
.stub(emailProvider, 'sendPasswordChangeEmail')
|
||||
.callsFake(() => Promise.resolve('email sent'));
|
||||
|
||||
return request(app)
|
||||
.post('/v1/auth/reset-password')
|
||||
.send({
|
||||
email: dbUser.email,
|
||||
password: 'updatedPassword2',
|
||||
resetToken: resetToken.resetToken,
|
||||
})
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body).to.be.equal('Password Updated');
|
||||
});
|
||||
});
|
||||
it("should report error when email and reset token doesn't match a user", async () => {
|
||||
await PasswordResetToken.create(resetToken);
|
||||
return request(app)
|
||||
.post('/v1/auth/reset-password')
|
||||
.send({
|
||||
email: user.email,
|
||||
password: 'updatedPassword',
|
||||
resetToken: resetToken.resetToken,
|
||||
})
|
||||
.expect(httpStatus.UNAUTHORIZED)
|
||||
.then((res) => {
|
||||
const { code } = res.body;
|
||||
const { message } = res.body;
|
||||
expect(code).to.be.equal(401);
|
||||
expect(message).to.be.equal('Cannot find matching reset token');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email is not provided', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/reset-password')
|
||||
.send({ password: 'updatedPassword', resetToken: resetToken.resetToken })
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const field1 = res.body.errors[0].field;
|
||||
const location1 = res.body.errors[0].location;
|
||||
const messages1 = res.body.errors[0].messages;
|
||||
expect(field1).to.be.equal('email');
|
||||
expect(location1).to.be.equal('body');
|
||||
expect(messages1).to.include('"email" is required');
|
||||
});
|
||||
});
|
||||
it('should report error when reset token is not provided', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/reset-password')
|
||||
.send({ email: dbUser.email, password: 'updatedPassword' })
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const field1 = res.body.errors[0].field;
|
||||
const location1 = res.body.errors[0].location;
|
||||
const messages1 = res.body.errors[0].messages;
|
||||
expect(field1).to.be.equal('resetToken');
|
||||
expect(location1).to.be.equal('body');
|
||||
expect(messages1).to.include('"resetToken" is required');
|
||||
});
|
||||
});
|
||||
it('should report error when password is not provided', () => {
|
||||
return request(app)
|
||||
.post('/v1/auth/reset-password')
|
||||
.send({ email: dbUser.email, resetToken: resetToken.resetToken })
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const field1 = res.body.errors[0].field;
|
||||
const location1 = res.body.errors[0].location;
|
||||
const messages1 = res.body.errors[0].messages;
|
||||
expect(field1).to.be.equal('password');
|
||||
expect(location1).to.be.equal('body');
|
||||
expect(messages1).to.include('"password" is required');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when the resetToken is expired', async () => {
|
||||
const expiredPasswordResetTokenObj = await PasswordResetToken.create(expiredResetToken);
|
||||
|
||||
expect(expiredPasswordResetTokenObj.resetToken).to.be.equal('5947397b323ae82d8c3a333b.c69d0435e62c9f4953af912442a3d064e20291f0d228c0552ed4be473e7d191ba40b18c2c47e8b9d');
|
||||
expect(expiredPasswordResetTokenObj.userId.toString()).to.be.equal('5947397b323ae82d8c3a333b');
|
||||
expect(expiredPasswordResetTokenObj.userEmail).to.be.equal(dbUser.email);
|
||||
expect(expiredPasswordResetTokenObj.expires).to.be.below(moment().toDate());
|
||||
|
||||
return request(app)
|
||||
.post('/v1/auth/reset-password')
|
||||
.send({
|
||||
email: dbUser.email,
|
||||
password: 'updated password',
|
||||
resetToken: expiredResetToken.resetToken,
|
||||
})
|
||||
.expect(httpStatus.UNAUTHORIZED)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(401);
|
||||
expect(res.body.message).to.include('Reset token is expired');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
550
src/api/tests/integration/user.test.js
Normal file
550
src/api/tests/integration/user.test.js
Normal file
@ -0,0 +1,550 @@
|
||||
/* eslint-disable arrow-body-style */
|
||||
/* eslint-disable no-unused-expressions */
|
||||
const request = require('supertest');
|
||||
const httpStatus = require('http-status');
|
||||
const { expect } = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { some, omitBy, isNil } = require('lodash');
|
||||
const app = require('../../../index');
|
||||
const User = require('../../models/user.model');
|
||||
const JWT_EXPIRATION = require('../../../config/vars').jwtExpirationInterval;
|
||||
|
||||
/**
|
||||
* root level hooks
|
||||
*/
|
||||
|
||||
async function format(user) {
|
||||
const formated = user;
|
||||
|
||||
// delete password
|
||||
delete formated.password;
|
||||
|
||||
// get users from database
|
||||
const dbUser = (await User.findOne({ email: user.email })).transform();
|
||||
|
||||
// remove null and undefined properties
|
||||
return omitBy(dbUser, isNil);
|
||||
}
|
||||
|
||||
describe('Users API', async () => {
|
||||
let adminAccessToken;
|
||||
let userAccessToken;
|
||||
let dbUsers;
|
||||
let user;
|
||||
let admin;
|
||||
|
||||
const password = '123456';
|
||||
const passwordHashed = await bcrypt.hash(password, 1);
|
||||
|
||||
beforeEach(async () => {
|
||||
dbUsers = {
|
||||
branStark: {
|
||||
email: 'branstark@gmail.com',
|
||||
password: passwordHashed,
|
||||
name: 'Bran Stark',
|
||||
role: 'admin',
|
||||
},
|
||||
jonSnow: {
|
||||
email: 'jonsnow@gmail.com',
|
||||
password: passwordHashed,
|
||||
name: 'Jon Snow',
|
||||
},
|
||||
};
|
||||
|
||||
user = {
|
||||
email: 'sousa.dfs@gmail.com',
|
||||
password,
|
||||
name: 'krish Sousa',
|
||||
};
|
||||
|
||||
admin = {
|
||||
email: 'sousa.dfs@gmail.com',
|
||||
password,
|
||||
name: 'krish Sousa',
|
||||
role: 'admin',
|
||||
};
|
||||
|
||||
await User.deleteMany({});
|
||||
await User.insertMany([dbUsers.branStark, dbUsers.jonSnow]);
|
||||
dbUsers.branStark.password = password;
|
||||
dbUsers.jonSnow.password = password;
|
||||
adminAccessToken = (await User.findAndGenerateToken(dbUsers.branStark)).accessToken;
|
||||
userAccessToken = (await User.findAndGenerateToken(dbUsers.jonSnow)).accessToken;
|
||||
});
|
||||
|
||||
describe('POST /v1/users', () => {
|
||||
it('should create a new user when request is ok', () => {
|
||||
return request(app)
|
||||
.post('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(admin)
|
||||
.expect(httpStatus.CREATED)
|
||||
.then((res) => {
|
||||
delete admin.password;
|
||||
expect(res.body).to.include(admin);
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a new user and set default role to "user"', () => {
|
||||
return request(app)
|
||||
.post('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.CREATED)
|
||||
.then((res) => {
|
||||
expect(res.body.role).to.be.equal('user');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email already exists', () => {
|
||||
user.email = dbUsers.branStark.email;
|
||||
|
||||
return request(app)
|
||||
.post('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.CONFLICT)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" already exists');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email is not provided', () => {
|
||||
delete user.email;
|
||||
|
||||
return request(app)
|
||||
.post('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" is required');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when password length is less than 6', () => {
|
||||
user.password = '12345';
|
||||
|
||||
return request(app)
|
||||
.post('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('password');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"password" length must be at least 6 characters long');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when logged user is not an admin', () => {
|
||||
return request(app)
|
||||
.post('/v1/users')
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.FORBIDDEN)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(httpStatus.FORBIDDEN);
|
||||
expect(res.body.message).to.be.equal('Forbidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v1/users', () => {
|
||||
it('should get all users', () => {
|
||||
return request(app)
|
||||
.get('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.OK)
|
||||
.then(async (res) => {
|
||||
const bran = await format(dbUsers.branStark);
|
||||
const john = await format(dbUsers.jonSnow);
|
||||
// before comparing it is necessary to convert String to Date
|
||||
res.body[0].createdAt = new Date(res.body[0].createdAt);
|
||||
res.body[1].createdAt = new Date(res.body[1].createdAt);
|
||||
|
||||
const includesBranStark = some(res.body, bran);
|
||||
const includesjonSnow = some(res.body, john);
|
||||
|
||||
expect(res.body).to.be.an('array');
|
||||
expect(res.body).to.have.lengthOf(2);
|
||||
expect(includesBranStark).to.be.true;
|
||||
expect(includesjonSnow).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should get all users with pagination', () => {
|
||||
return request(app)
|
||||
.get('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.query({ page: 2, perPage: 1 })
|
||||
.expect(httpStatus.OK)
|
||||
.then(async (res) => {
|
||||
delete dbUsers.jonSnow.password;
|
||||
|
||||
expect(res.body).to.be.an('array');
|
||||
expect(res.body[0]).to.be.an('object');
|
||||
expect(res.body).to.have.lengthOf(1);
|
||||
expect(res.body[0].name).to.be.equal('Jon Snow');
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter users', () => {
|
||||
return request(app)
|
||||
.get('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.query({ email: dbUsers.jonSnow.email })
|
||||
.expect(httpStatus.OK)
|
||||
.then(async (res) => {
|
||||
delete dbUsers.jonSnow.password;
|
||||
const john = await format(dbUsers.jonSnow);
|
||||
|
||||
// before comparing it is necessary to convert String to Date
|
||||
res.body[0].createdAt = new Date(res.body[0].createdAt);
|
||||
|
||||
const includesjonSnow = some(res.body, john);
|
||||
|
||||
expect(res.body).to.be.an('array');
|
||||
expect(res.body).to.have.lengthOf(1);
|
||||
expect(includesjonSnow).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when pagination\'s parameters are not a number', () => {
|
||||
return request(app)
|
||||
.get('/v1/users')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.query({ page: '?', perPage: 'whaat' })
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('page');
|
||||
expect(location).to.be.equal('query');
|
||||
expect(messages).to.include('"page" must be a number');
|
||||
return Promise.resolve(res);
|
||||
})
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[1];
|
||||
const { location } = res.body.errors[1];
|
||||
const { messages } = res.body.errors[1];
|
||||
expect(field).to.be.equal('perPage');
|
||||
expect(location).to.be.equal('query');
|
||||
expect(messages).to.include('"perPage" must be a number');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error if logged user is not an admin', () => {
|
||||
return request(app)
|
||||
.get('/v1/users')
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.expect(httpStatus.FORBIDDEN)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(httpStatus.FORBIDDEN);
|
||||
expect(res.body.message).to.be.equal('Forbidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v1/users/:userId', () => {
|
||||
it('should get user', async () => {
|
||||
const id = (await User.findOne({}))._id;
|
||||
delete dbUsers.branStark.password;
|
||||
|
||||
return request(app)
|
||||
.get(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body).to.include(dbUsers.branStark);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error "User does not exist" when user does not exists', () => {
|
||||
return request(app)
|
||||
.get('/v1/users/56c787ccc67fc16ccc1a5e92')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.NOT_FOUND)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(404);
|
||||
expect(res.body.message).to.be.equal('User does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error "User does not exist" when id is not a valid ObjectID', () => {
|
||||
return request(app)
|
||||
.get('/v1/users/palmeiras1914')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.NOT_FOUND)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(404);
|
||||
expect(res.body.message).to.equal('User does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when logged user is not the same as the requested one', async () => {
|
||||
const id = (await User.findOne({ email: dbUsers.branStark.email }))._id;
|
||||
|
||||
return request(app)
|
||||
.get(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.expect(httpStatus.FORBIDDEN)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(httpStatus.FORBIDDEN);
|
||||
expect(res.body.message).to.be.equal('Forbidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /v1/users/:userId', () => {
|
||||
it('should replace user', async () => {
|
||||
delete dbUsers.branStark.password;
|
||||
const id = (await User.findOne(dbUsers.branStark))._id;
|
||||
|
||||
return request(app)
|
||||
.put(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
delete user.password;
|
||||
expect(res.body).to.include(user);
|
||||
expect(res.body.role).to.be.equal('user');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when email is not provided', async () => {
|
||||
const id = (await User.findOne({}))._id;
|
||||
delete user.email;
|
||||
|
||||
return request(app)
|
||||
.put(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('email');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"email" is required');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error user when password length is less than 6', async () => {
|
||||
const id = (await User.findOne({}))._id;
|
||||
user.password = '12345';
|
||||
|
||||
return request(app)
|
||||
.put(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send(user)
|
||||
.expect(httpStatus.BAD_REQUEST)
|
||||
.then((res) => {
|
||||
const { field } = res.body.errors[0];
|
||||
const { location } = res.body.errors[0];
|
||||
const { messages } = res.body.errors[0];
|
||||
expect(field).to.be.equal('password');
|
||||
expect(location).to.be.equal('body');
|
||||
expect(messages).to.include('"password" length must be at least 6 characters long');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error "User does not exist" when user does not exists', () => {
|
||||
return request(app)
|
||||
.put('/v1/users/palmeiras1914')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.NOT_FOUND)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(404);
|
||||
expect(res.body.message).to.be.equal('User does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when logged user is not the same as the requested one', async () => {
|
||||
const id = (await User.findOne({ email: dbUsers.branStark.email }))._id;
|
||||
|
||||
return request(app)
|
||||
.put(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.expect(httpStatus.FORBIDDEN)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(httpStatus.FORBIDDEN);
|
||||
expect(res.body.message).to.be.equal('Forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not replace the role of the user (not admin)', async () => {
|
||||
const id = (await User.findOne({ email: dbUsers.jonSnow.email }))._id;
|
||||
const role = 'admin';
|
||||
|
||||
return request(app)
|
||||
.put(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.send(admin)
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body.role).to.not.be.equal(role);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /v1/users/:userId', () => {
|
||||
it('should update user', async () => {
|
||||
delete dbUsers.branStark.password;
|
||||
const id = (await User.findOne(dbUsers.branStark))._id;
|
||||
const { name } = user;
|
||||
|
||||
return request(app)
|
||||
.patch(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send({ name })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body.name).to.be.equal(name);
|
||||
expect(res.body.email).to.be.equal(dbUsers.branStark.email);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update user when no parameters were given', async () => {
|
||||
delete dbUsers.branStark.password;
|
||||
const id = (await User.findOne(dbUsers.branStark))._id;
|
||||
|
||||
return request(app)
|
||||
.patch(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.send()
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body).to.include(dbUsers.branStark);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error "User does not exist" when user does not exists', () => {
|
||||
return request(app)
|
||||
.patch('/v1/users/palmeiras1914')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.NOT_FOUND)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(404);
|
||||
expect(res.body.message).to.be.equal('User does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when logged user is not the same as the requested one', async () => {
|
||||
const id = (await User.findOne({ email: dbUsers.branStark.email }))._id;
|
||||
|
||||
return request(app)
|
||||
.patch(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.expect(httpStatus.FORBIDDEN)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(httpStatus.FORBIDDEN);
|
||||
expect(res.body.message).to.be.equal('Forbidden');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update the role of the user (not admin)', async () => {
|
||||
const id = (await User.findOne({ email: dbUsers.jonSnow.email }))._id;
|
||||
const role = 'admin';
|
||||
|
||||
return request(app)
|
||||
.patch(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.send({ role })
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body.role).to.not.be.equal(role);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /v1/users', () => {
|
||||
it('should delete user', async () => {
|
||||
const id = (await User.findOne({}))._id;
|
||||
|
||||
return request(app)
|
||||
.delete(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.NO_CONTENT)
|
||||
.then(() => request(app).get('/v1/users'))
|
||||
.then(async () => {
|
||||
const users = await User.find({});
|
||||
expect(users).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error "User does not exist" when user does not exists', () => {
|
||||
return request(app)
|
||||
.delete('/v1/users/palmeiras1914')
|
||||
.set('Authorization', `Bearer ${adminAccessToken}`)
|
||||
.expect(httpStatus.NOT_FOUND)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(404);
|
||||
expect(res.body.message).to.be.equal('User does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error when logged user is not the same as the requested one', async () => {
|
||||
const id = (await User.findOne({ email: dbUsers.branStark.email }))._id;
|
||||
|
||||
return request(app)
|
||||
.delete(`/v1/users/${id}`)
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.expect(httpStatus.FORBIDDEN)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(httpStatus.FORBIDDEN);
|
||||
expect(res.body.message).to.be.equal('Forbidden');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /v1/users/profile', () => {
|
||||
it('should get the logged user\'s info', () => {
|
||||
delete dbUsers.jonSnow.password;
|
||||
|
||||
return request(app)
|
||||
.get('/v1/users/profile')
|
||||
.set('Authorization', `Bearer ${userAccessToken}`)
|
||||
.expect(httpStatus.OK)
|
||||
.then((res) => {
|
||||
expect(res.body).to.include(dbUsers.jonSnow);
|
||||
});
|
||||
});
|
||||
|
||||
it('should report error without stacktrace when accessToken is expired', async () => {
|
||||
// fake time
|
||||
const clock = sinon.useFakeTimers();
|
||||
const expiredAccessToken = (await User.findAndGenerateToken(dbUsers.branStark)).accessToken;
|
||||
|
||||
// move clock forward by minutes set in config + 1 minute
|
||||
clock.tick((JWT_EXPIRATION * 60000) + 60000);
|
||||
|
||||
return request(app)
|
||||
.get('/v1/users/profile')
|
||||
.set('Authorization', `Bearer ${expiredAccessToken}`)
|
||||
.expect(httpStatus.UNAUTHORIZED)
|
||||
.then((res) => {
|
||||
expect(res.body.code).to.be.equal(httpStatus.UNAUTHORIZED);
|
||||
expect(res.body.message).to.be.equal('jwt expired');
|
||||
expect(res.body).to.not.have.a.property('stack');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
46
src/api/utils/APIError.js
Normal file
46
src/api/utils/APIError.js
Normal file
@ -0,0 +1,46 @@
|
||||
const httpStatus = require('http-status');
|
||||
|
||||
/**
|
||||
* @extends Error
|
||||
*/
|
||||
class ExtendableError extends Error {
|
||||
constructor({
|
||||
message, errors, status, isPublic, stack,
|
||||
}) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.message = message;
|
||||
this.errors = errors;
|
||||
this.status = status;
|
||||
this.isPublic = isPublic;
|
||||
this.isOperational = true; // This is required since bluebird 4 doesn't append it anymore.
|
||||
this.stack = stack;
|
||||
// Error.captureStackTrace(this, this.constructor.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class representing an API error.
|
||||
* @extends ExtendableError
|
||||
*/
|
||||
class APIError extends ExtendableError {
|
||||
/**
|
||||
* Creates an API error.
|
||||
* @param {string} message - Error message.
|
||||
* @param {number} status - HTTP status code of error.
|
||||
* @param {boolean} isPublic - Whether the message should be visible to user or not.
|
||||
*/
|
||||
constructor({
|
||||
message,
|
||||
errors,
|
||||
stack,
|
||||
status = httpStatus.INTERNAL_SERVER_ERROR,
|
||||
isPublic = false,
|
||||
}) {
|
||||
super({
|
||||
message, errors, status, isPublic, stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = APIError;
|
584
src/api/utils/passwordResetEmailTemplate.html
Normal file
584
src/api/utils/passwordResetEmailTemplate.html
Normal file
@ -0,0 +1,584 @@
|
||||
<!DOCTYPE html
|
||||
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="x-apple-disable-message-reformatting" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title></title>
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
mso-hide: all;
|
||||
font-size: 1px;
|
||||
line-height: 1px;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Type ------------------------------ */
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin: .4em 0 1.1875em;
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
border-right: 18px solid #3869D4;
|
||||
border-bottom: 10px solid #3869D4;
|
||||
border-left: 18px solid #3869D4;
|
||||
display: inline-block;
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
border-right: 18px solid #22BC66;
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
border-right: 18px solid #FF6136;
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 35px 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 25px 0 0 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #FFF;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #A8AAAF;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
|
||||
/* Body ------------------------------ */
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
-premailer-width: 570px;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
padding: 0;
|
||||
-premailer-width: 100%;
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
.content-cell {
|
||||
padding: 35px;
|
||||
}
|
||||
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #333333 !important;
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
blockquote,
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
color: #FFF !important;
|
||||
}
|
||||
|
||||
.attributes_content,
|
||||
.discount {
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.email-masthead_name {
|
||||
text-shadow: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
.f-fallback {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
body {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #FFF;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body
|
||||
style="width: 100% !important; height: 100%; -webkit-text-size-adjust: none; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; background-color: #FFF; color: #333; margin: 0;"
|
||||
bgcolor="#FFF">
|
||||
<span class="preheader"
|
||||
style="display: none !important; visibility: hidden; mso-hide: all; font-size: 1px; line-height: 1px; max-height: 0; max-width: 0; opacity: 0; overflow: hidden;">Use
|
||||
this link to reset your password. The link is only valid for 24 hours.</span>
|
||||
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;">
|
||||
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation"
|
||||
style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0; padding: 0;">
|
||||
<tr>
|
||||
<td class="email-masthead"
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; text-align: center; padding: 25px 0;"
|
||||
align="center">
|
||||
<a href="https://example.com" class="f-fallback email-masthead_name"
|
||||
style="color: #A8AAAF; font-size: 16px; font-weight: bold; text-decoration: none; text-shadow: 0 1px 0 white;">
|
||||
[Product Name]
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Email Body -->
|
||||
<tr>
|
||||
<td class="email-body" width="570" cellpadding="0" cellspacing="0"
|
||||
style="word-break: break-word; margin: 0; padding: 0; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0;">
|
||||
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; margin: 0 auto; padding: 0;">
|
||||
<!-- Body content -->
|
||||
<tr>
|
||||
<td class="content-cell"
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; padding: 35px;">
|
||||
<div class="f-fallback">
|
||||
<h1 style="margin-top: 0; color: #333333; font-size: 22px; font-weight: bold; text-align: left;"
|
||||
align="left">Hi {{name}},</h1>
|
||||
<p
|
||||
style="font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;">
|
||||
You recently requested to reset your password for your [Product Name]
|
||||
account. Use the button below to reset it. <strong>This password reset
|
||||
is only valid for the next 2 hours.</strong></p>
|
||||
<!-- Action -->
|
||||
<table class="body-action" align="center" width="100%" cellpadding="0"
|
||||
cellspacing="0" role="presentation"
|
||||
style="width: 100%; -premailer-width: 100%; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 30px auto; padding: 0;">
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;">
|
||||
<!-- Border based button
|
||||
https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0"
|
||||
role="presentation">
|
||||
<tr>
|
||||
<td align="center"
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;">
|
||||
<a href="{{action_url}}"
|
||||
class="f-fallback button button--green"
|
||||
target="_blank"
|
||||
style="color: #FFF; border-color: #22bc66; border-style: solid; border-width: 10px 18px; background-color: #22BC66; display: inline-block; text-decoration: none; border-radius: 3px; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16); -webkit-text-size-adjust: none; box-sizing: border-box;">Reset
|
||||
your password</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p
|
||||
style="font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;">
|
||||
For security, this request was received from a {{operating_system}}
|
||||
device using {{browser_name}}. If you did not request a password reset,
|
||||
please ignore this email or <a href="{{support_url}}"
|
||||
style="color: #3869D4;">contact support</a> if you have questions.
|
||||
</p>
|
||||
<p
|
||||
style="font-size: 16px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;">
|
||||
Thanks,
|
||||
<br />The [Product Name] Team</p>
|
||||
<!-- Sub copy -->
|
||||
<table class="body-sub" role="presentation"
|
||||
style="margin-top: 25px; padding-top: 25px; border-top-width: 1px; border-top-color: #EAEAEC; border-top-style: solid;">
|
||||
<tr>
|
||||
<td
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;">
|
||||
<p class="f-fallback sub"
|
||||
style="font-size: 13px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;">
|
||||
If you’re having trouble with the button above, copy and
|
||||
paste the URL below into your web browser.</p>
|
||||
<p class="f-fallback sub"
|
||||
style="font-size: 13px; line-height: 1.625; color: #333; margin: .4em 0 1.1875em;">
|
||||
{{action_url}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px;">
|
||||
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0"
|
||||
role="presentation"
|
||||
style="width: 570px; -premailer-width: 570px; -premailer-cellpadding: 0; -premailer-cellspacing: 0; text-align: center; margin: 0 auto; padding: 0;">
|
||||
<tr>
|
||||
<td class="content-cell" align="center"
|
||||
style="word-break: break-word; font-family: "Nunito Sans", Helvetica, Arial, sans-serif; font-size: 16px; padding: 35px;">
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px; line-height: 1.625; text-align: center; color: #A8AAAF; margin: .4em 0 1.1875em;"
|
||||
align="center">© 2019 [Product Name]. All rights reserved.</p>
|
||||
<p class="f-fallback sub align-center"
|
||||
style="font-size: 13px; line-height: 1.625; text-align: center; color: #A8AAAF; margin: .4em 0 1.1875em;"
|
||||
align="center">
|
||||
[Company Name, LLC]
|
||||
<br />1234 Street Rd.
|
||||
<br />Suite 1234
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
69
src/api/validations/auth.validation.js
Normal file
69
src/api/validations/auth.validation.js
Normal file
@ -0,0 +1,69 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
module.exports = {
|
||||
// POST /v1/auth/register
|
||||
register: {
|
||||
body: {
|
||||
email: Joi.string()
|
||||
.email()
|
||||
.required(),
|
||||
password: Joi.string()
|
||||
.required()
|
||||
.min(6)
|
||||
.max(128),
|
||||
},
|
||||
},
|
||||
|
||||
// POST /v1/auth/login
|
||||
login: {
|
||||
body: {
|
||||
email: Joi.string()
|
||||
.email()
|
||||
.required(),
|
||||
password: Joi.string()
|
||||
.required()
|
||||
.max(128),
|
||||
},
|
||||
},
|
||||
|
||||
// POST /v1/auth/facebook
|
||||
// POST /v1/auth/google
|
||||
oAuth: {
|
||||
body: {
|
||||
access_token: Joi.string().required(),
|
||||
},
|
||||
},
|
||||
|
||||
// POST /v1/auth/refresh
|
||||
refresh: {
|
||||
body: {
|
||||
email: Joi.string()
|
||||
.email()
|
||||
.required(),
|
||||
refreshToken: Joi.string().required(),
|
||||
},
|
||||
},
|
||||
|
||||
// POST /v1/auth/refresh
|
||||
sendPasswordReset: {
|
||||
body: {
|
||||
email: Joi.string()
|
||||
.email()
|
||||
.required(),
|
||||
},
|
||||
},
|
||||
|
||||
// POST /v1/auth/password-reset
|
||||
passwordReset: {
|
||||
body: {
|
||||
email: Joi.string()
|
||||
.email()
|
||||
.required(),
|
||||
password: Joi.string()
|
||||
.required()
|
||||
.min(6)
|
||||
.max(128),
|
||||
resetToken: Joi.string().required(),
|
||||
},
|
||||
},
|
||||
};
|
52
src/api/validations/user.validation.js
Normal file
52
src/api/validations/user.validation.js
Normal file
@ -0,0 +1,52 @@
|
||||
const Joi = require('joi');
|
||||
const User = require('../models/user.model');
|
||||
|
||||
module.exports = {
|
||||
|
||||
// GET /v1/users
|
||||
listUsers: {
|
||||
query: {
|
||||
page: Joi.number().min(1),
|
||||
perPage: Joi.number().min(1).max(100),
|
||||
name: Joi.string(),
|
||||
email: Joi.string(),
|
||||
role: Joi.string().valid(User.roles),
|
||||
},
|
||||
},
|
||||
|
||||
// POST /v1/users
|
||||
createUser: {
|
||||
body: {
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).max(128).required(),
|
||||
name: Joi.string().max(128),
|
||||
role: Joi.string().valid(User.roles),
|
||||
},
|
||||
},
|
||||
|
||||
// PUT /v1/users/:userId
|
||||
replaceUser: {
|
||||
body: {
|
||||
email: Joi.string().email().required(),
|
||||
password: Joi.string().min(6).max(128).required(),
|
||||
name: Joi.string().max(128),
|
||||
role: Joi.string().valid(User.roles),
|
||||
},
|
||||
params: {
|
||||
userId: Joi.string().regex(/^[a-fA-F0-9]{24}$/).required(),
|
||||
},
|
||||
},
|
||||
|
||||
// PATCH /v1/users/:userId
|
||||
updateUser: {
|
||||
body: {
|
||||
email: Joi.string().email(),
|
||||
password: Joi.string().min(6).max(128),
|
||||
name: Joi.string().max(128),
|
||||
role: Joi.string().valid(User.roles),
|
||||
},
|
||||
params: {
|
||||
userId: Joi.string().regex(/^[a-fA-F0-9]{24}$/).required(),
|
||||
},
|
||||
},
|
||||
};
|
58
src/config/express.js
Normal file
58
src/config/express.js
Normal file
@ -0,0 +1,58 @@
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const bodyParser = require('body-parser');
|
||||
const compress = require('compression');
|
||||
const methodOverride = require('method-override');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const passport = require('passport');
|
||||
const routes = require('../api/routes/v1');
|
||||
const { logs } = require('./vars');
|
||||
const strategies = require('./passport');
|
||||
const error = require('../api/middlewares/error');
|
||||
|
||||
/**
|
||||
* Express instance
|
||||
* @public
|
||||
*/
|
||||
const app = express();
|
||||
|
||||
// request logging. dev: console | production: file
|
||||
app.use(morgan(logs));
|
||||
|
||||
// parse body params and attache them to req.body
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
// gzip compression
|
||||
app.use(compress());
|
||||
|
||||
// lets you use HTTP verbs such as PUT or DELETE
|
||||
// in places where the client doesn't support it
|
||||
app.use(methodOverride());
|
||||
|
||||
// secure apps by setting various HTTP headers
|
||||
app.use(helmet());
|
||||
|
||||
// enable CORS - Cross Origin Resource Sharing
|
||||
app.use(cors());
|
||||
|
||||
// enable authentication
|
||||
app.use(passport.initialize());
|
||||
passport.use('jwt', strategies.jwt);
|
||||
passport.use('facebook', strategies.facebook);
|
||||
passport.use('google', strategies.google);
|
||||
|
||||
// mount api v1 routes
|
||||
app.use('/v1', routes);
|
||||
|
||||
// if error is not an instanceOf APIError, convert it.
|
||||
app.use(error.converter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(error.notFound);
|
||||
|
||||
// error handler, send stacktrace only during development
|
||||
app.use(error.handler);
|
||||
|
||||
module.exports = app;
|
32
src/config/logger.js
Normal file
32
src/config/logger.js
Normal file
@ -0,0 +1,32 @@
|
||||
const winston = require('winston');
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `combined.log`
|
||||
// - Write all logs error (and below) to `error.log`.
|
||||
//
|
||||
new winston.transports.File({ filename: 'error.log', level: 'error' }),
|
||||
new winston.transports.File({ filename: 'combined.log' }),
|
||||
],
|
||||
});
|
||||
|
||||
//
|
||||
// If we're not in production then log to the `console` with the format:
|
||||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
||||
//
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
}));
|
||||
}
|
||||
|
||||
logger.stream = {
|
||||
write: (message) => {
|
||||
logger.info(message.trim());
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = logger;
|
36
src/config/mongoose.js
Normal file
36
src/config/mongoose.js
Normal file
@ -0,0 +1,36 @@
|
||||
const mongoose = require('mongoose');
|
||||
const logger = require('./../config/logger');
|
||||
const { mongo, env } = require('./vars');
|
||||
|
||||
// set mongoose Promise to Bluebird
|
||||
mongoose.Promise = Promise;
|
||||
|
||||
// Exit application on error
|
||||
mongoose.connection.on('error', (err) => {
|
||||
logger.error(`MongoDB connection error: ${err}`);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
// print mongoose logs in dev env
|
||||
if (env === 'development') {
|
||||
mongoose.set('debug', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to mongo db
|
||||
*
|
||||
* @returns {object} Mongoose connection
|
||||
* @public
|
||||
*/
|
||||
exports.connect = () => {
|
||||
mongoose
|
||||
.connect(mongo.uri, {
|
||||
useCreateIndex: true,
|
||||
keepAlive: 1,
|
||||
useNewUrlParser: true,
|
||||
useUnifiedTopology: true,
|
||||
useFindAndModify: false,
|
||||
})
|
||||
.then(() => console.log('mongoDB connected...'));
|
||||
return mongoose.connection;
|
||||
};
|
35
src/config/passport.js
Normal file
35
src/config/passport.js
Normal file
@ -0,0 +1,35 @@
|
||||
const JwtStrategy = require('passport-jwt').Strategy;
|
||||
const BearerStrategy = require('passport-http-bearer');
|
||||
const { ExtractJwt } = require('passport-jwt');
|
||||
const { jwtSecret } = require('./vars');
|
||||
const authProviders = require('../api/services/authProviders');
|
||||
const User = require('../api/models/user.model');
|
||||
|
||||
const jwtOptions = {
|
||||
secretOrKey: jwtSecret,
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('Bearer'),
|
||||
};
|
||||
|
||||
const jwt = async (payload, done) => {
|
||||
try {
|
||||
const user = await User.findById(payload.sub);
|
||||
if (user) return done(null, user);
|
||||
return done(null, false);
|
||||
} catch (error) {
|
||||
return done(error, false);
|
||||
}
|
||||
};
|
||||
|
||||
const oAuth = service => async (token, done) => {
|
||||
try {
|
||||
const userData = await authProviders[service](token);
|
||||
const user = await User.oAuthLogin(userData);
|
||||
return done(null, user);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.jwt = new JwtStrategy(jwtOptions, jwt);
|
||||
exports.facebook = new BearerStrategy(oAuth('facebook'));
|
||||
exports.google = new BearerStrategy(oAuth('google'));
|
24
src/config/vars.js
Normal file
24
src/config/vars.js
Normal file
@ -0,0 +1,24 @@
|
||||
const path = require('path');
|
||||
|
||||
// import .env variables
|
||||
require('dotenv-safe').load({
|
||||
path: path.join(__dirname, '../../.env'),
|
||||
sample: path.join(__dirname, '../../.env.example'),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
env: process.env.NODE_ENV,
|
||||
port: process.env.PORT,
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
jwtExpirationInterval: process.env.JWT_EXPIRATION_MINUTES,
|
||||
mongo: {
|
||||
uri: process.env.NODE_ENV === 'test' ? process.env.MONGO_URI_TESTS : process.env.MONGO_URI,
|
||||
},
|
||||
logs: process.env.NODE_ENV === 'production' ? 'combined' : 'dev',
|
||||
emailConfig: {
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT,
|
||||
username: process.env.EMAIL_USERNAME,
|
||||
password: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
};
|
17
src/index.js
Normal file
17
src/index.js
Normal file
@ -0,0 +1,17 @@
|
||||
// make bluebird default Promise
|
||||
Promise = require('bluebird'); // eslint-disable-line no-global-assign
|
||||
const { port, env } = require('./config/vars');
|
||||
const logger = require('./config/logger');
|
||||
const app = require('./config/express');
|
||||
const mongoose = require('./config/mongoose');
|
||||
|
||||
// open mongoose connection
|
||||
|
||||
// listen to requests
|
||||
app.listen(port, () => logger.info(`server started on port ${port} (${env})`));
|
||||
|
||||
/**
|
||||
* Exports express
|
||||
* @public
|
||||
*/
|
||||
module.exports = app;
|
Loading…
x
Reference in New Issue
Block a user