├── .editorconfig
├── .gitignore
├── README.md
├── apis
└── userEndpoint.js
├── app.js
├── helpers
├── cryptHelpers.js
├── errorHandlers.js
├── jwtHelpers.js
├── loggerHelpers.js
├── mailHelpers.js
├── responseHelpers.js
└── stringHelpers.js
├── middlewares
└── authMiddleware.js
├── models
└── User.js
├── package-lock.json
├── package.json
├── routes
└── index.js
├── seeds
├── seeder.js
└── users.json
├── services
├── index.js
└── users
│ ├── authenticate.js
│ ├── confirmResetPassword.js
│ ├── confirmSignUp.js
│ ├── findUserByEmail.js
│ ├── findUserCurrent.js
│ ├── findUsers.js
│ ├── forgotPassword.js
│ ├── signUp.js
│ └── testAxios.js
├── start.js
├── tests
└── .gitkeep
├── variables.env.example
└── views
└── email
├── confirm-sign-up.pug
├── email-layout.pug
├── password-reset.pug
└── styles.css
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | [*]
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [vcbuild.bat]
13 | end_of_line = crlf
14 |
15 | [{lib,src,test}/**.js]
16 | indent_style = space
17 | indent_size = 2
18 |
19 | [*.js]
20 | indent_style = space
21 | indent_size = 2
22 |
23 | [src/**.{h,cc}]
24 | indent_style = space
25 | indent_size = 2
26 |
27 | [test/*.py]
28 | indent_style = space
29 | indent_size = 2
30 |
31 | [configure]
32 | indent_style = space
33 | indent_size = 2
34 |
35 | [Makefile]
36 | indent_style = tab
37 | indent_size = 8
38 |
39 | [{deps,tools}/**]
40 | indent_style = ignore
41 | indent_size = ignore
42 | end_of_line = ignore
43 | trim_trailing_whitespace = ignore
44 | charset = ignore
45 |
46 | [{test/fixtures,deps,tools/node_modules,tools/gyp,tools/icu,tools/msvs}/**]
47 | insert_final_newline = false
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env.local
15 | .env.development.local
16 | .env.test.local
17 | .env.production.local
18 |
19 | npm-debug.log*
20 | yarn-debug.log*
21 | yarn-error.log*
22 |
23 | # My custom
24 | *.log
25 | .idea
26 | variables.env
27 | /.vscode
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/standard/standard)
2 |
3 | [](https://standardjs.com)
4 |
5 | # STACK
6 |
7 | - Node.js / Express.js
8 | - MongoDB / Mongoose
9 | - JWT
10 | - [JavaScript Standard Style Guide](https://standardjs.com/)
11 |
12 | ---
13 | # HOW TO RUN
14 |
15 | - Copy `variables.env.sample` to `variables.env` & custom it
16 |
17 | ```bash
18 | npm start
19 |
20 | // or
21 |
22 | npm run watch
23 | ```
24 |
25 | - Data sample:
26 | ```bash
27 | npm run seed
28 |
29 | npm run seed:delete
30 |
31 | npm run seed:refresh
32 | ```
33 |
34 | - Lint:
35 | ```bash
36 | npm run lint
37 | ```
38 |
39 | # ROUTES / API
40 | - Root url:
41 | ```bash
42 | curl -X GET \
43 | http://localhost:3000/
44 | ```
45 |
46 | - API Authenticate:
47 | ```bash
48 | curl -X POST \
49 | http://localhost:3000/api/authenticate \
50 | -H 'content-type: application/json' \
51 | -d '{
52 | "email": "xinh@mail.com",
53 | "password": "123456"
54 | }'
55 | ```
56 |
57 | - API Get users:
58 | ```bash
59 | curl -X GET \
60 | http://localhost:3000/api/users \
61 | -H 'authorization: Bearer {{YOUR_TOKEN}}'
62 | ```
63 |
64 | - API Sign up:
65 | ```bash
66 | curl -X POST \
67 | http://localhost:3000/api/sign-up \
68 | -H 'content-type: application/json' \
69 | -d '{
70 | "name": "Mail1",
71 | "email": "mail1@mail.com",
72 | "password": "123456"
73 | }'
74 | ```
75 |
76 | - API Confirm sign up:
77 | ```bash
78 | curl -X GET \
79 | 'http://localhost:3000/api/confirm-sign-up?token={{YOUR_TOKEN}}'
80 | ```
81 |
82 | - API Test axios:
83 | ```bash
84 | curl -X GET \
85 | http://localhost:3000/api/test-axios \
86 | -H 'authorization: Bearer {{YOUR_TOKEN}}'
87 | ```
88 |
89 | - API Forgot password:
90 | ```bash
91 | curl -X POST \
92 | http://localhost:3000/api/forgot-password \
93 | -H 'content-type: application/json' \
94 | -d '{
95 | "email": "xinh@mail.com"
96 | }'
97 | ```
98 |
99 | - API Confirm reset password:
100 | ```bash
101 | curl -X GET \
102 | 'http://localhost:3000/api/confirm-resest-password?token={{YOUR_TOKEN}}'
103 | ```
104 |
105 | - API Get user current:
106 | ```bash
107 | curl -X GET \
108 | http://localhost:3000/api/get-user-current \
109 | -H 'authorization: Bearer {{YOUR_TOKEN}}'
110 | ```
111 |
112 | ---
113 | # REFERENCES
114 |
--------------------------------------------------------------------------------
/apis/userEndpoint.js:
--------------------------------------------------------------------------------
1 | const services = require('../services')
2 | const { SuccessResponse } = require('../helpers/responseHelpers')
3 |
4 | exports.getUsers = async (req, res) => {
5 | const users = await services.users.findUsers()
6 |
7 | return res.json(
8 | new SuccessResponse.Builder()
9 | .withContent(users)
10 | .build()
11 | )
12 | }
13 |
14 | exports.postAuthenticate = async (req, res) => {
15 | // Get input data
16 | let email = req.body.email
17 | let password = req.body.password
18 |
19 | let token = await services.users.authenticate(email, password)
20 |
21 | return res.json(
22 | new SuccessResponse.Builder()
23 | .withContent(token)
24 | )
25 | }
26 |
27 | exports.postSignUp = async (req, res) => {
28 | // Get input data
29 | let name = req.body.name
30 | let email = req.body.email
31 | let password = req.body.password
32 |
33 | const user = await services.users.signUp(name, email, password, req.headers.host)
34 |
35 | return res.json(
36 | new SuccessResponse.Builder()
37 | .withContent(user)
38 | .build()
39 | )
40 | }
41 |
42 | exports.getConfirmSignUp = async (req, res) => {
43 | // Get input data
44 | let token = req.query.token
45 |
46 | const user = await services.users.confirmSignUp(token)
47 |
48 | return res.json(
49 | new SuccessResponse.Builder()
50 | .withContent(user)
51 | .build()
52 | )
53 | }
54 |
55 | exports.getTestAxios = async (req, res) => {
56 | const data = await services.users.testAxios()
57 |
58 | return res.json(
59 | new SuccessResponse.Builder()
60 | .withContent(data)
61 | .build()
62 | )
63 | }
64 |
65 | exports.postForgotPassword = async (req, res) => {
66 | // Get input data
67 | let email = req.body.email
68 |
69 | const user = await services.users.forgotPassword(email, req.headers.host)
70 |
71 | return res.json(
72 | new SuccessResponse.Builder()
73 | .withContent(user)
74 | .withMessage('You have been emailed a password reset link.')
75 | .build()
76 | )
77 | }
78 |
79 | exports.getConfirmResetPassword = async (req, res) => {
80 | // Get input data
81 | let token = req.query.token
82 |
83 | const user = await services.users.confirmResetPassword(token)
84 |
85 | return res.json(
86 | new SuccessResponse.Builder()
87 | .withContent(user)
88 | .withMessage(`Your password is: `)
89 | .build()
90 | )
91 | }
92 |
93 | exports.getUserCurrent = async (req, res) => {
94 | // Get token from request
95 | // const token = req.decoded.payload.email
96 |
97 | // const user = await services.users.findUserCurrent(token)
98 |
99 | // return res.json(
100 | // new SuccessResponse.Builder()
101 | // .withContent(user)
102 | // .build()
103 | // )
104 |
105 | return res.json(
106 | new SuccessResponse.Builder()
107 | .withContent(req.userCurrent)
108 | .build()
109 | )
110 | }
111 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | const morgan = require('morgan')
4 | const cors = require('cors')
5 |
6 | const routes = require('./routes/index')
7 |
8 | // Create our Express app
9 | const app = express()
10 |
11 | // Enable All CORS Requests
12 | app.use(cors())
13 |
14 | // Takes the raw requests and turns them into usable properties on req.body
15 | app.use(bodyParser.json())
16 | app.use(bodyParser.urlencoded({ extended: true }))
17 |
18 | // Use morgan to log requests to the console
19 | app.use(morgan('dev'))
20 |
21 | // After allllll that above middleware, we finally handle our own routes!
22 | app.use('/', routes)
23 |
24 | // Done! we export it so we can start the site in start.js
25 | module.exports = app
26 |
--------------------------------------------------------------------------------
/helpers/cryptHelpers.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto')
2 |
3 | exports.md5 = function (str) {
4 | var md5sum = crypto.createHash('md5')
5 | md5sum.update(str)
6 | str = md5sum.digest('hex')
7 | return str
8 | }
9 |
10 | exports.encryptAes = function (str, secret) {
11 | var cipher = crypto.createCipher('aes192', secret)
12 | var enc = cipher.update(str, 'utf8', 'hex')
13 | enc += cipher.final('hex')
14 | return enc
15 | }
16 |
17 | exports.decryptAes = function (str, secret) {
18 | var decipher = crypto.createDecipher('aes192', secret)
19 | var dec = decipher.update(str, 'hex', 'utf8')
20 | dec += decipher.final('utf8')
21 | return dec
22 | }
23 |
--------------------------------------------------------------------------------
/helpers/errorHandlers.js:
--------------------------------------------------------------------------------
1 | /*
2 | Catch Errors Handler
3 |
4 | With async/await, you need some way to catch errors
5 | Instead of using try{} catch(e) {} in each controller, we wrap the function in
6 | catchErrors(), catch and errors they throw, and pass it along to our express middleware with next()
7 | */
8 |
9 | exports.catchErrors = (fn) => {
10 | return (req, res, next) => {
11 | return fn(req, res, next).catch(next)
12 | }
13 | }
14 |
15 | /*
16 | Not Found Error Handler
17 | If we hit a route that is not found, we mark it as 404 and pass it along to the next error handler to display
18 | */
19 | exports.notFound = (req, res, next) => {
20 | const err = new Error('Not Found')
21 | err.status = 404
22 | next(err)
23 | }
24 |
25 | /*
26 | MongoDB Validation Error Handler
27 | Detect if there are mongodb validation errors that we can nicely show via flash messages
28 | */
29 |
30 | exports.flashValidationErrors = (err, req, res, next) => {
31 | if (!err.errors) return next(err)
32 | // validation errors look like
33 | const errorKeys = Object.keys(err.errors)
34 | errorKeys.forEach(key => req.flash('error', err.errors[key].message))
35 | res.redirect('back')
36 | }
37 |
38 | /*
39 | Development Error Hanlder
40 | In development we show good error messages so if we hit a syntax error or any other previously un-handled error, we can show good info on what happened
41 | */
42 | exports.developmentErrors = (err, req, res, next) => {
43 | err.stack = err.stack || ''
44 | const errorDetails = {
45 | message: err.message,
46 | status: err.status,
47 | stackHighlighted: err.stack.replace(/[a-z_-\d]+.js:\d+:\d+/gi, '$&')
48 | }
49 | res.status(err.status || 500)
50 | res.format({
51 | // Based on the `Accept` http header
52 | 'text/html': () => {
53 | res.render('error', errorDetails)
54 | }, // Form Submit, Reload the page
55 | 'application/json': () => res.json(errorDetails) // Ajax call, send JSON back
56 | })
57 | }
58 |
59 | /*
60 | Production Error Hanlder
61 | No stacktraces are leaked to user
62 | */
63 | exports.productionErrors = (err, req, res, next) => {
64 | res.status(err.status || 500)
65 | res.render('error', {
66 | message: err.message,
67 | error: {}
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/helpers/jwtHelpers.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 |
3 | exports.encode = (payload) => {
4 | let token = jwt.sign({ payload }, process.env.JWT_SECRET, { expiresIn: '1h' })
5 | return token
6 | }
7 |
8 | exports.decode = async (token) => {
9 | let decoded = await jwt.verify(token, process.env.JWT_SECRET)
10 | return decoded
11 | }
12 |
--------------------------------------------------------------------------------
/helpers/loggerHelpers.js:
--------------------------------------------------------------------------------
1 | const { createLogger, format, transports } = require('winston')
2 | const { combine, timestamp, label, printf, colorize } = format
3 |
4 | const myFormat = printf(info => {
5 | return `${info.timestamp} [${info.label}] ${info.level}: ${info.message}`
6 | })
7 |
8 | const logger = createLogger({
9 | level: process.env.LOG_LEVEL,
10 | format: combine(
11 | label({ label: process.env.APP_NAME }),
12 | timestamp(),
13 | myFormat
14 | ),
15 | transports: [
16 | //
17 | // - Write to all logs with level `info` and below to `combined.log`
18 | // - Write all logs error (and below) to `error.log`.
19 | //
20 | new transports.File({ filename: 'error.log', level: 'error' }),
21 | new transports.File({ filename: 'combined.log' })
22 | ]
23 | })
24 |
25 | //
26 | // If we're not in production then log to the `console` with the format:
27 | // `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
28 | //
29 | if (process.env.NODE_ENV !== 'production') {
30 | logger.add(new transports.Console({
31 | format: combine(
32 | label({ label: process.env.APP_NAME }),
33 | colorize({ property: 'label' }), // colorize({ all: true }),
34 | timestamp(),
35 | myFormat
36 | ),
37 | colorize: true
38 | }))
39 | }
40 |
41 | module.exports = logger
42 |
--------------------------------------------------------------------------------
/helpers/mailHelpers.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer')
2 | const pug = require('pug')
3 | const juice = require('juice')
4 | const htmlToText = require('html-to-text')
5 | const promisify = require('es6-promisify')
6 |
7 | const transport = nodemailer.createTransport({
8 | host: process.env.MAIL_HOST,
9 | port: process.env.MAIL_PORT,
10 | auth: {
11 | user: process.env.MAIL_USER,
12 | pass: process.env.MAIL_PASS
13 | }
14 | })
15 |
16 | const generateHTML = (filename, options = {}) => {
17 | const html = pug.renderFile(`${__dirname}/../views/email/${filename}.pug`, options)
18 | const inlined = juice(html)
19 | return inlined
20 | }
21 |
22 | exports.send = async (options) => {
23 | const html = generateHTML(options.filename, options)
24 | const text = htmlToText.fromString(html)
25 |
26 | const mailOptions = {
27 | from: `Xinh Nguyen `,
28 | to: options.user.email,
29 | subject: options.subject,
30 | html,
31 | text
32 | }
33 | const sendMail = promisify(transport.sendMail, transport)
34 | return sendMail(mailOptions)
35 | }
36 |
--------------------------------------------------------------------------------
/helpers/responseHelpers.js:
--------------------------------------------------------------------------------
1 | exports.SuccessResponse = class SuccessResponse {
2 | constructor (build) {
3 | this.success = true
4 | this.content = build.content
5 | this.message = build.message || ''
6 | }
7 | static get Builder () {
8 | class Builder {
9 | withContent (content) {
10 | this.content = content
11 | return this
12 | }
13 | withMessage (message) {
14 | this.message = message
15 | return this
16 | }
17 | build () {
18 | return new SuccessResponse(this)
19 | }
20 | }
21 | return Builder
22 | }
23 | }
24 |
25 | exports.FailResponse = class FailResponse {
26 | constructor (build) {
27 | this.success = false
28 | this.content = build.content
29 | this.message = build.message || ''
30 | }
31 | static get Builder () {
32 | class Builder {
33 | withContent (content) {
34 | this.content = content
35 | return this
36 | }
37 | withMessage (message) {
38 | this.message = message
39 | return this
40 | }
41 | build () {
42 | return new FailResponse(this)
43 | }
44 | }
45 | return Builder
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/helpers/stringHelpers.js:
--------------------------------------------------------------------------------
1 | exports.randomPassword = () => {
2 | return Math.random() // Generate random number, eg: 0.123456
3 | .toString(36) // Convert to base-36 : "0.4fzyo82mvyr"
4 | .slice(-8) // Cut off last 8 characters : "yo82mvyr"
5 | }
6 |
--------------------------------------------------------------------------------
/middlewares/authMiddleware.js:
--------------------------------------------------------------------------------
1 | const { FailResponse } = require('../helpers/responseHelpers')
2 | const logger = require('../helpers/loggerHelpers')
3 | const jwtHelpers = require('../helpers/jwtHelpers')
4 |
5 | const services = require('../services')
6 |
7 | exports.getAuthorize = async (req, res, next) => {
8 | // Check header or url parameters or post parameters for token
9 | const headerAuthorize = req.body.token || req.query.token || req.headers['x-access-token'] || req.headers.authorization
10 |
11 | // Check exist token
12 | if (!headerAuthorize) {
13 | logger.warn('Header Authorize Not Found')
14 | return res.json(
15 | new FailResponse.Builder()
16 | .withMessage('Header Authorize Not Found')
17 | .build()
18 | )
19 | }
20 |
21 | // Get token
22 | const token = headerAuthorize.replace(process.env.JWT_TOKEN_TYPE, '').trim()
23 |
24 | // Decode token
25 | // Verifies secret and checks exp
26 | try {
27 | const decoded = await jwtHelpers.decode(token, process.env.JWT_SECRET)
28 | // Save decoded to request
29 | req.decoded = decoded
30 | // Save user current to request
31 | const email = decoded.payload.email
32 | req.userCurrent = await services.users.findUserByEmail(email)
33 | return next()
34 | } catch (err) {
35 | logger.error(`Token Decode Error ${err}`)
36 | return res.json(
37 | new FailResponse.Builder()
38 | .withContent(err)
39 | .build()
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/models/User.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const Schema = mongoose.Schema
3 | mongoose.Promise = global.Promise
4 | const md5 = require('md5')
5 | const validator = require('validator')
6 | const bcrypt = require('bcrypt')
7 |
8 | const userSchema = new Schema({
9 | email: {
10 | type: String,
11 | unique: true,
12 | lowercase: true,
13 | trim: true,
14 | validate: {
15 | isAsync: true,
16 | validator: validator.isEmail,
17 | message: 'Invalid Email Address'
18 | },
19 | required: 'Please Supply an email address'
20 | },
21 | password: {
22 | type: String,
23 | required: 'Please supply a password',
24 | trim: true
25 | },
26 | name: {
27 | type: String,
28 | required: 'Please supply a name',
29 | trim: true
30 | },
31 | resetPasswordToken: String,
32 | resetPasswordExpires: Date,
33 | enable: { type: Boolean, default: false }
34 | }, { timestamps: true, toJSON: { virtuals: true } })
35 |
36 | userSchema.virtual('gravatar').get(() => {
37 | const hash = md5(this.email)
38 | return `https://gravatar.com/avatar/${hash}?s=200`
39 | })
40 |
41 | userSchema.pre('save', async function (next) {
42 | try {
43 | if (!this.isModified('password')) {
44 | // Skip it & stop this function from running
45 | return next()
46 | }
47 |
48 | // Generate a salt
49 | const salt = await bcrypt.genSalt(Number(process.env.SALT_ROUNDS))
50 |
51 | // Hash the password along with our new salt
52 | const hash = await bcrypt.hash(this.password, salt)
53 |
54 | // Override the cleartext password with the hashed one
55 | this.password = hash
56 |
57 | return next()
58 | } catch (e) {
59 | return next(e)
60 | }
61 | })
62 |
63 | module.exports = mongoose.model('User', userSchema)
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "expressjs-starter-kit",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "start.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node ./start.js",
9 | "watch": "nodemon ./start.js --ignore public/",
10 | "seed": "node ./seeds/seeder.js",
11 | "seed:delete": "node ./seeds/seeder.js --delete",
12 | "seed:refresh": "npm run seed:delete && npm run seed",
13 | "lint": "standard"
14 | },
15 | "author": "",
16 | "license": "ISC",
17 | "dependencies": {
18 | "axios": "^0.17.1",
19 | "bcrypt": "^1.0.3",
20 | "body-parser": "^1.18.2",
21 | "cors": "^2.8.4",
22 | "dotenv": "^4.0.0",
23 | "es6-promisify": "^5.0.0",
24 | "express": "^4.16.2",
25 | "html-to-text": "^3.3.0",
26 | "jsonwebtoken": "^8.1.0",
27 | "juice": "^4.2.2",
28 | "md5": "^2.2.1",
29 | "mongoose": "^4.13.9",
30 | "morgan": "^1.9.0",
31 | "nodemailer": "^4.4.1",
32 | "pug": "^2.0.0-rc.4",
33 | "validator": "^9.2.0",
34 | "winston": "^3.0.0-rc1"
35 | },
36 | "devDependencies": {
37 | "nodemon": "^1.14.10",
38 | "standard": "^10.0.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 |
4 | const { catchErrors } = require('../helpers/errorHandlers')
5 | const userEndpoint = require('../apis/userEndpoint')
6 | const { getAuthorize } = require('../middlewares/authMiddleware')
7 |
8 | // Unprotected routes
9 | router.get('/', (req, res) => res.json({ msg: process.env.APP_NAME }))
10 | router.post('/api/authenticate', catchErrors(userEndpoint.postAuthenticate))
11 | router.post('/api/sign-up', catchErrors(userEndpoint.postSignUp))
12 | router.get('/api/confirm-sign-up', catchErrors(userEndpoint.getConfirmSignUp))
13 | router.post('/api/forgot-password', catchErrors(userEndpoint.postForgotPassword))
14 | router.get('/api/confirm-reset-password', catchErrors(userEndpoint.getConfirmResetPassword))
15 |
16 | // Middlewares
17 | router.use(getAuthorize)
18 |
19 | // Protected routes
20 | router.get('/api/users', catchErrors(userEndpoint.getUsers))
21 | router.get('/api/get-user-current', catchErrors(userEndpoint.getUserCurrent))
22 | router.get('/api/test-axios', catchErrors(userEndpoint.getTestAxios))
23 |
24 | module.exports = router
25 |
--------------------------------------------------------------------------------
/seeds/seeder.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | require('dotenv').config({ path: path.join(__dirname, '../variables.env') })
4 | const fs = require('fs')
5 |
6 | const mongoose = require('mongoose')
7 | mongoose.connect(process.env.DATABASE, { useMongoClient: true })
8 | mongoose.Promise = global.Promise // Tell Mongoose to use ES6 promises
9 |
10 | // Import all of our models - they need to be imported only once
11 | const User = require('../models/User')
12 |
13 | // Password: 123456
14 | const users = JSON.parse(fs.readFileSync(path.join(__dirname, '/users.json'), 'utf-8'))
15 |
16 | const deleteData = async () => {
17 | console.log('😢😢 Goodbye Data...')
18 | await User.remove()
19 | console.log('Data Deleted. To load sample data, run\n\n\t npm run seed\n\n')
20 | process.exit()
21 | }
22 |
23 | const loadData = async () => {
24 | try {
25 | await User.insertMany(users)
26 | console.log('👍👍👍👍👍👍👍👍 Done!')
27 | process.exit()
28 | } catch (e) {
29 | console.log('\n👎👎👎👎👎👎👎👎 Error! The Error info is below but if you are importing sample data make sure to drop the existing database first with.\n\n\t npm run seed:delete\n\n\n')
30 | console.log(e)
31 | process.exit()
32 | }
33 | }
34 |
35 | if (process.argv.includes('--delete')) {
36 | deleteData()
37 | } else {
38 | loadData()
39 | }
40 |
--------------------------------------------------------------------------------
/seeds/users.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "_id": "58c039018060197ca0b52d4c",
4 | "salt": "1997f002c3db624f17acdcc8a1a21be1708916f0b21360e7b460f20dae2a179a",
5 | "hash": "c26a834cacea8486c5e25b046c219ea11df633320ecf78834ee72d6be12cfb06a2ce224442cee16f0d299931dc8c1694a7416c825fef4e95fcb5a7aa28cf4a98bc0111eb00fd7623e7ab12139327b66558dd97606df3f2848de21e878bc3226016b6faaef7e04cbdcd15ea23835dfc8f0b603ae0a74af7daecffcb6ff30d734c57cd35579a8535d3bc65cc0c86befcde4a3b8bb5f1fce31b709f58202507ee1809c61b2600122ca077988c229414baee51d8993a0b0389d60d154d6b64285cd56ff4ace497727ea1c48917feef981ea2115520c9fe3952e03e7c779eebd0a58ad94c62d321ebb22b11f6e7d414e3c61e5a4aebb6d40c304e6f2875181483716f5ad5b8e525dfbd7bda537b429dfd43881ea9ff60c5e405268180d4f2b442cca51ac1ee303f8a94d2622ec13e4a11e04a4565187569bc27e32dc8868677f2f5a8cecbef7a49e6708ba264d1c1335a4b8fe45fdb2912b59441f438ef762d7cf7f0f03ea76290fc701858817413cc60eae20ec6c62d787eaa6d2a9917213f3d832793f3651450969ed32ccf888cb6dcffa50d3d210eeb592319392c1a0523d942dfcd06f1be714cecf48b01fc7f3c2ac6f3f7a87d8db22c418a8b4e18fdfb129c7ee0e4912b7d343d6d30d3a59c868c047f6ddb88de5317c161c88217cd1788895482834596e753bed0d452969866ca83a891103f216797e001edff058e9d4ece97",
6 | "email": "xinh@mail.com",
7 | "password": "$2a$10$eyWiOKpRT6WTZiMtQL1HZuknBf1Jo4nploL/cR/TgwOhKpAFpZW8O",
8 | "name": "Xinh Nguyen",
9 | "enable": true
10 | },
11 | {
12 | "_id": "58c03ada8060197ca0b52d52",
13 | "salt": "3a8479b05be4c4f1f62f484e02081be7662ee46b5de17cb8721d043fca364ca5",
14 | "hash": "3355b0acc7b0cebd66675843ae7d56048ea093432c6ca096858850d18a443bcb3fc87d5b4df911f3b73e22bc672e760284013e581ecd98cbf4223441c6d708a1266cc188753f167caafe9b06f2fa236ece8b5320f3f002167d781698f12da373a48e465a44b008b84cf92142bc4f35818d6e86a45a0a17c43ff6f51d0eea4cd1cb59a3cc9d8ac44f5feec3a054993dacc6f83d85d324ee50eb7cfa1e01f878394e350a11ee9a3620c5280c192ca6b50a288695e5676c6dba828162b13b1a3d6a8b91bc82799043a3127c8fc34300987ca73dbec36721d1b78fa35f36c67fabdaaecc437e34cedeffdf41ae61e1b5b12d326b9ecb9bbc09b24ac9aa89e5ca0297ad2ee21f448c38324bc7795563a41d9661c63975ecfd4734496e5bf93376691e6683e8a58a272800957ab6606372bdfee5e31ce551b83ac7db6d422a72394747ef836d7a02ecb9502001d82254663fe5f42d040d653730d366f168429f70907b107fab12beca4d0b273130a2aeae1b652001e86b5bed85355721cb5d355bd4200369677b8b62761ac2063323270b33e45827d51f7b78e8f2ac7d9c701e5528087452692dba57d4d81d65cc8ad1d898fdbd108fdb3be148869ff737f91b5ebc1b68595504c9fd8c2b38a5809f76e470689dd1e5b27803503192c61e6cb135b9b93651bb06b369ebbf7585356255d73c74cabe9ae4f84f98c388b729399340106a",
15 | "email": "debbie@example.com",
16 | "password": "$2a$10$eyWiOKpRT6WTZiMtQL1HZuknBf1Jo4nploL/cR/TgwOhKpAFpZW8O",
17 | "name": "Debbie Downer",
18 | "enable": true
19 | },
20 | {
21 | "_id": "58c08bbed1d97c276fd56ce3",
22 | "salt": "e2e70759d59ad47beb07170abf44a7f24a03c41b28c579b79e0a9e75d3648aef",
23 | "hash": "40d93d6c3ef8fb2e4775867de18532f9d6a8509a5bb85a1e20c418dbea4083a94e258b4340572a5e6fc18fac32f98362137fb081492db9e0f12c7c3b8c87090c77a12894046fecccb83714e78fe4a7b3e993b90e4d8816fbf05306d5e7ca2b0209833831e16451a928fcb3ad23b1a0f8ef029d8813b6dd00068f3e20df58e5a32c85f2cc85b4a9ac0b1bcd5e2bbab489d7d5649c26d75e70d3fcb01347de7f0e2f41dc53d28dec7a0a9fd4e05b2e499a6f646dcad99c80adfb7c4d35ce607ab4450c24aa08faf4f43b5e303410bc7cf6cf968bb15d474cf7f6edf9bd66daac6340c839d5675a0b8137d1b35c2888e0a82c0d5643c12b69b01e2cbb19f82daffffc80ceb28799711f181e37b232d29795772bde87b9eb099e22f5afcd3ec6c76fef4f55d1ba17f5a15fea2b52b58fe465e5733b1136a3ee7b45bbae79957b7c0c2f81922dac35b227fe934ae92aac413c29d79cc1551dd95c9144d058449d1ed3d24fdcc83f06ba8e25f21b5057216a1291459991c9914378b38da30064d228411ae7886199101ff1bbfd348e691f963f266892252c3a14714650b3c92cb9f8970186950fd0f7a5a71c2f679cc577c81e4b668aa8a76a282e2cdf55a38bf9aa225c330236fb279c8a6f094179876ddaa574e15f08abbfeb2138cb3fae1753d3ab0206bb198f5315700a61cb83ef5be559884aa6132fe1161af8b3873546b5acfa",
24 | "email": "beau@example.com",
25 | "password": "$2a$10$eyWiOKpRT6WTZiMtQL1HZuknBf1Jo4nploL/cR/TgwOhKpAFpZW8O",
26 | "name": "Beau",
27 | "enable": true
28 | }
29 | ]
30 |
--------------------------------------------------------------------------------
/services/index.js:
--------------------------------------------------------------------------------
1 | const { authenticate } = require('./users/authenticate')
2 | const { confirmResetPassword } = require('./users/confirmResetPassword')
3 | const { confirmSignUp } = require('./users/confirmSignUp')
4 | const { findUserCurrent } = require('./users/findUserCurrent')
5 | const { findUsers } = require('./users/findUsers')
6 | const { forgotPassword } = require('./users/forgotPassword')
7 | const { signUp } = require('./users/signUp')
8 | const { testAxios } = require('./users/testAxios')
9 | const { findUserByEmail } = require('./users/findUserByEmail')
10 |
11 | const services = {
12 | users: {
13 | authenticate,
14 | confirmResetPassword,
15 | confirmSignUp,
16 | findUserCurrent,
17 | findUsers,
18 | forgotPassword,
19 | signUp,
20 | testAxios,
21 | findUserByEmail
22 | }
23 | }
24 |
25 | module.exports = services
26 |
--------------------------------------------------------------------------------
/services/users/authenticate.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const bcrypt = require('bcrypt')
3 |
4 | const User = mongoose.model('User')
5 | const logger = require('../../helpers/loggerHelpers')
6 | const jwtHelpers = require('../../helpers/jwtHelpers')
7 |
8 | exports.authenticate = async (email, password) => {
9 | // Find the user
10 | const user = await User.findOne({ email, enable: true })
11 |
12 | // Check if user exist
13 | if (!user) {
14 | logger.warn('Authentication failed. User not found.')
15 | throw new Error('Authentication failed. User not found.')
16 | }
17 |
18 | // Check if password matches
19 | if (!await bcrypt.compare(password, user.password)) {
20 | logger.warn('Authentication failed. Wrong password.')
21 | throw new Error('Authentication failed. Wrong password.')
22 | }
23 |
24 | // Create a token with only our given payload
25 | let token = jwtHelpers.encode({ email }, process.env.JWT_SECRET, { expiresIn: '1h' })
26 | logger.info(`Auth token created: ${token}`)
27 |
28 | // Return the information including token as JSON
29 | return { token: `${process.env.JWT_TOKEN_TYPE} ${token}` }
30 | }
31 |
--------------------------------------------------------------------------------
/services/users/confirmResetPassword.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const User = mongoose.model('User')
4 | const { randomPassword } = require('../../helpers/stringHelpers')
5 | const logger = require('../../helpers/loggerHelpers')
6 |
7 | exports.confirmResetPassword = async (token) => {
8 | // See if a user with that token exists
9 | const user = await User.findOne({
10 | resetPasswordToken: token,
11 | resetPasswordExpires: { $gt: Date.now() }
12 | })
13 | if (!user) {
14 | throw new Error('Password reset is invalid or has expired')
15 | }
16 |
17 | // Generate new password
18 | let password = randomPassword()
19 | logger.info(`Password was generated: ${password}`)
20 | user.password = password
21 | user.resetPasswordToken = undefined
22 | user.resetPasswordExpires = undefined
23 | await user.save()
24 |
25 | return user
26 | }
27 |
--------------------------------------------------------------------------------
/services/users/confirmSignUp.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const User = mongoose.model('User')
4 | const logger = require('../../helpers/loggerHelpers')
5 | const jwtHelpers = require('../../helpers/jwtHelpers')
6 |
7 | exports.confirmSignUp = async (token) => {
8 | // Check exist token
9 | if (!token) {
10 | throw new Error('Token Not Found')
11 | }
12 |
13 | // Decode token
14 | // Verifies secret and checks exp
15 | try {
16 | let decoded = await jwtHelpers.decode(token, process.env.JWT_SECRET)
17 | let email = decoded.payload.email
18 |
19 | // Enable user
20 | let user = await User.findOne({ email })
21 | user.enable = true
22 | await user.save()
23 |
24 | // Create response
25 | return user
26 | } catch (err) {
27 | logger.error(`Token Decode Error ${err}`)
28 | throw new Error(`Token Decode Error ${err}`)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/services/users/findUserByEmail.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const User = mongoose.model('User')
4 |
5 | exports.findUserByEmail = async (email) => {
6 | const user = await User.findOne({ email })
7 | return user
8 | }
9 |
--------------------------------------------------------------------------------
/services/users/findUserCurrent.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const jwtHelpers = require('../../helpers/jwtHelpers')
3 |
4 | const User = mongoose.model('User')
5 |
6 | exports.findUserCurrent = async (token) => {
7 | // Decode token
8 | try {
9 | let decoded = await jwtHelpers.decode(token, process.env.JWT_SECRET)
10 | let email = decoded.payload.email
11 | // Find user
12 | let user = await User.findOne({ email })
13 | return user
14 | } catch (err) {
15 | throw new Error(err)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/services/users/findUsers.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const User = mongoose.model('User')
4 |
5 | exports.findUsers = async () => {
6 | const users = await User.find()
7 | return users
8 | }
9 |
--------------------------------------------------------------------------------
/services/users/forgotPassword.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const User = mongoose.model('User')
4 | const mailHelpers = require('../../helpers/mailHelpers')
5 | const jwtHelpers = require('../../helpers/jwtHelpers')
6 |
7 | exports.forgotPassword = async (email, host) => {
8 | // 1. See if a user with that email exists
9 | const user = await User.findOne({ email })
10 | if (!user) {
11 | throw new Error('No account with that email exists.')
12 | }
13 | // 2. Set reset tokens and expiry on their account
14 | user.resetPasswordToken = jwtHelpers.encode({ email }, process.env.JWT_SECRET, { expiresIn: '1h' })
15 | user.resetPasswordExpires = Date.now() + 3600000 // 1 hour from now
16 | await user.save()
17 | // 3. Send them an email with the token
18 | const resetURL = `http://${host}/api/confirm-reset-password?token=${user.resetPasswordToken}`
19 | await mailHelpers.send({
20 | user,
21 | filename: 'password-reset',
22 | subject: 'Password Reset',
23 | resetURL
24 | })
25 |
26 | return user
27 | }
28 |
--------------------------------------------------------------------------------
/services/users/signUp.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const User = mongoose.model('User')
4 | const mailHelpers = require('../../helpers/mailHelpers')
5 | const jwtHelpers = require('../../helpers/jwtHelpers')
6 |
7 | exports.signUp = async (name, email, password, host) => {
8 | // Save the user
9 | const user = new User({ name, email, password })
10 | await user.save()
11 |
12 | // Send them an email with the token
13 | const tokenConfirm = jwtHelpers.encode({ email }, process.env.JWT_SECRET, { expiresIn: '15d' })
14 | const resetURL = `http://${host}/api/confirm-sign-up?token=${tokenConfirm}`
15 | await mailHelpers.send({
16 | user,
17 | filename: 'confirm-sign-up',
18 | subject: 'Confirm Sign Up',
19 | resetURL
20 | })
21 |
22 | return user
23 | }
24 |
--------------------------------------------------------------------------------
/services/users/testAxios.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 |
3 | exports.testAxios = async () => {
4 | // Grab some data over an Ajax request
5 | const xinh = await axios('https://api.github.com/users/nguyentrucxinh')
6 |
7 | // Many requests should be concurrent - don't slow things down!
8 | // Fire off two requests and save their promises
9 | const userPromise = axios('https://randomuser.me/api/')
10 | const namePromise = axios('https://uinames.com/api/')
11 | // Await all three promises to come back and destructure the result into their own variables
12 | const [user, name] = await Promise.all([userPromise, namePromise])
13 |
14 | return { xinh: xinh.data, user: user.data, name: name.data }
15 | }
16 |
--------------------------------------------------------------------------------
/start.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const fs = require('fs')
3 | const path = require('path')
4 |
5 | // Import environmental variables from our variables.env file
6 | require('dotenv').config({ path: 'variables.env' })
7 |
8 | // Connect to our Database and handle an bad connections
9 | mongoose.connect(process.env.DATABASE, { useMongoClient: true })
10 | mongoose.Promise = global.Promise // Tell Mongoose to use ES6 promises
11 | mongoose.connection.on('error', (err) => {
12 | console.error(`-> ${err.message}`)
13 | })
14 |
15 | // READY?! Let's go!
16 |
17 | // Import all of our models
18 | const modelsPath = path.join(__dirname, 'models')
19 | fs.readdirSync(modelsPath).forEach(function (file) {
20 | require(path.join(modelsPath, file))
21 | })
22 |
23 | // Start our app!
24 | const app = require('./app')
25 | app.set('port', process.env.PORT || 7777)
26 | const server = app.listen(app.get('port'), () => {
27 | console.log(`Express running → PORT ${server.address().port}`)
28 | })
29 |
--------------------------------------------------------------------------------
/tests/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ntxinh/expressjs-starter-kit/3be41ee32bdc7e639037f2be56853aac01721c75/tests/.gitkeep
--------------------------------------------------------------------------------
/variables.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | DATABASE=
3 | MAIL_USER=
4 | MAIL_PASS=
5 | MAIL_HOST=mailtrap.io
6 | MAIL_PORT=2525
7 | PORT=3000
8 | JWT_SECRET=secret
9 | JWT_TOKEN_TYPE=Bearer
10 | SALT_ROUNDS=10
11 | LOG_LEVEL=info
12 | APP_NAME=Express.js Starter Kit
13 |
--------------------------------------------------------------------------------
/views/email/confirm-sign-up.pug:
--------------------------------------------------------------------------------
1 | extends email-layout
2 |
3 | block content
4 | h2 Confirm Sign Up
5 | p Hello!
6 | p Thank you for registering at my website. Your account is created and must be activated before you can use it.
7 | p To activate the account click on following link:
8 | +button(resetURL, 'Confirm Sign Up →')
9 | p If you can't click the above button please visit #{resetURL}
10 | p After activation you may login to my website using your username and password.
11 | br
12 | p If you didn't request this email, please ignore it.
13 |
--------------------------------------------------------------------------------
/views/email/email-layout.pug:
--------------------------------------------------------------------------------
1 | //- Handy mixin for making buttons
2 | mixin button(url, text)
3 | table.btn.btn-primary(border='0', cellpadding='0', cellspacing='0')
4 | tbody
5 | tr
6 | td(align='left')
7 | table(border='0', cellpadding='0', cellspacing='0')
8 | tbody: tr: td: a(href=url, target='_blank')= text
9 |
10 | //- The Email
11 | doctype html
12 | html
13 | head
14 | meta(name='viewport', content='width=device-width')
15 | meta(http-equiv='Content-Type', content='text/html; charset=UTF-8')
16 | style
17 | include styles.css
18 | body
19 | table.body(border='0', cellpadding='0', cellspacing='0')
20 | tr
21 | td
22 | td.container
23 | .content
24 | table.main
25 | tr
26 | td.wrapper
27 | table(border='0', cellpadding='0', cellspacing='0')
28 | tr
29 | td
30 | block content
31 |
--------------------------------------------------------------------------------
/views/email/password-reset.pug:
--------------------------------------------------------------------------------
1 | extends email-layout
2 |
3 | block content
4 | h2 Password Reset
5 | p Hello. You have requested a password reset. Please click the following button to continue on with resetting your password. Please note this link is only valid for the next hour.
6 | +button(resetURL, 'Reset my Password →')
7 | p If you can't click the above button please visit #{resetURL}
8 | br
9 | p If you didn't request this email, please ignore it.
10 |
--------------------------------------------------------------------------------
/views/email/styles.css:
--------------------------------------------------------------------------------
1 | /* -------------------------------------
2 | GLOBAL RESETS
3 | ------------------------------------- */
4 | img {
5 | border: none;
6 | -ms-interpolation-mode: bicubic;
7 | max-width: 100%; }
8 |
9 | body {
10 | background-color: #303030;
11 | font-family: sans-serif;
12 | -webkit-font-smoothing: antialiased;
13 | font-size: 14px;
14 | line-height: 1.4;
15 | margin: 0;
16 | padding: 0;
17 | -ms-text-size-adjust: 100%;
18 | -webkit-text-size-adjust: 100%; }
19 |
20 | table {
21 | border-collapse: separate;
22 | mso-table-lspace: 0pt;
23 | mso-table-rspace: 0pt;
24 | width: 100%; }
25 | table td {
26 | font-family: sans-serif;
27 | font-size: 14px;
28 | vertical-align: top; }
29 |
30 | /* -------------------------------------
31 | BODY & CONTAINER
32 | ------------------------------------- */
33 |
34 | .body {
35 | width: 100%;
36 | }
37 |
38 | /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
39 | .container {
40 | display: block;
41 | Margin: 0 auto !important;
42 | /* makes it centered */
43 | max-width: 580px;
44 | padding: 10px;
45 | width: 580px; }
46 |
47 | /* This should also be a block element, so that it will fill 100% of the .container */
48 | .content {
49 | box-sizing: border-box;
50 | display: block;
51 | Margin: 0 auto;
52 | max-width: 580px;
53 | padding: 10px; }
54 |
55 | /* -------------------------------------
56 | HEADER, FOOTER, MAIN
57 | ------------------------------------- */
58 | .main {
59 | background: #fff;
60 | border-top:20px solid #fff200;
61 | width: 100%; }
62 |
63 | .wrapper {
64 | box-sizing: border-box;
65 | padding: 20px; }
66 |
67 | .footer {
68 | clear: both;
69 | padding-top: 10px;
70 | text-align: center;
71 | width: 100%; }
72 | .footer td,
73 | .footer p,
74 | .footer span,
75 | .footer a {
76 | color: #999999;
77 | font-size: 12px;
78 | text-align: center; }
79 |
80 | /* -------------------------------------
81 | TYPOGRAPHY
82 | ------------------------------------- */
83 | h1,
84 | h2,
85 | h3,
86 | h4 {
87 | color: #000000;
88 | font-family: sans-serif;
89 | font-weight: 400;
90 | line-height: 1.4;
91 | margin: 0;
92 | Margin-bottom: 30px; }
93 |
94 | h1 {
95 | font-size: 35px;
96 | font-weight: 300;
97 | text-align: center;
98 | text-transform: capitalize; }
99 |
100 | p,
101 | ul,
102 | ol {
103 | font-family: sans-serif;
104 | font-size: 14px;
105 | font-weight: normal;
106 | margin: 0;
107 | Margin-bottom: 15px; }
108 | p li,
109 | ul li,
110 | ol li {
111 | list-style-position: inside;
112 | margin-left: 5px; }
113 |
114 | a {
115 | color: #fff200;
116 | text-decoration: underline; }
117 |
118 | /* -------------------------------------
119 | BUTTONS
120 | ------------------------------------- */
121 | .btn {
122 | box-sizing: border-box;
123 | width: 100%; }
124 | .btn > tbody > tr > td {
125 | padding-bottom: 15px; }
126 | .btn table {
127 | width: auto; }
128 | .btn table td {
129 | background-color: #ffffff;
130 | border-radius: 5px;
131 | text-align: center; }
132 | .btn a {
133 | background-color: #ffffff;
134 | border: solid 1px #fff200;
135 | border-radius: 5px;
136 | box-sizing: border-box;
137 | color: #fff200;
138 | cursor: pointer;
139 | display: inline-block;
140 | font-size: 14px;
141 | font-weight: bold;
142 | margin: 0;
143 | padding: 12px 25px;
144 | text-decoration: none;
145 | text-transform: capitalize; }
146 |
147 | .btn-primary table td {
148 | background-color: #fff200; }
149 |
150 | .btn-primary a {
151 | background-color: #fff200;
152 | border-color: #fff200;
153 | color: #3a3a3a; }
154 |
155 | /* -------------------------------------
156 | OTHER STYLES THAT MIGHT BE USEFUL
157 | ------------------------------------- */
158 | .last {
159 | margin-bottom: 0; }
160 |
161 | .first {
162 | margin-top: 0; }
163 |
164 | .align-center {
165 | text-align: center; }
166 |
167 | .align-right {
168 | text-align: right; }
169 |
170 | .align-left {
171 | text-align: left; }
172 |
173 | .clear {
174 | clear: both; }
175 |
176 | .mt0 {
177 | margin-top: 0; }
178 |
179 | .mb0 {
180 | margin-bottom: 0; }
181 |
182 | .preheader {
183 | color: transparent;
184 | display: none;
185 | height: 0;
186 | max-height: 0;
187 | max-width: 0;
188 | opacity: 0;
189 | overflow: hidden;
190 | mso-hide: all;
191 | visibility: hidden;
192 | width: 0; }
193 |
194 | .powered-by a {
195 | text-decoration: none; }
196 |
197 | hr {
198 | border: 0;
199 | border-bottom: 1px solid #303030;
200 | Margin: 20px 0; }
201 |
202 | /* -------------------------------------
203 | RESPONSIVE AND MOBILE FRIENDLY STYLES
204 | ------------------------------------- */
205 | @media only screen and (max-width: 620px) {
206 | table[class=body] h1 {
207 | font-size: 28px !important;
208 | margin-bottom: 10px !important; }
209 | table[class=body] p,
210 | table[class=body] ul,
211 | table[class=body] ol,
212 | table[class=body] td,
213 | table[class=body] span,
214 | table[class=body] a {
215 | font-size: 16px !important; }
216 | table[class=body] .wrapper,
217 | table[class=body] .article {
218 | padding: 10px !important; }
219 | table[class=body] .content {
220 | padding: 0 !important; }
221 | table[class=body] .container {
222 | padding: 0 !important;
223 | width: 100% !important; }
224 | table[class=body] .main {
225 | border-left-width: 0 !important;
226 | border-radius: 0 !important;
227 | border-right-width: 0 !important; }
228 | table[class=body] .btn table {
229 | width: 100% !important; }
230 | table[class=body] .btn a {
231 | width: 100% !important; }
232 | table[class=body] .img-responsive {
233 | height: auto !important;
234 | max-width: 100% !important;
235 | width: auto !important; }}
236 |
237 | /* -------------------------------------
238 | PRESERVE THESE STYLES IN THE HEAD
239 | ------------------------------------- */
240 | @media all {
241 | .ExternalClass {
242 | width: 100%; }
243 | .ExternalClass,
244 | .ExternalClass p,
245 | .ExternalClass span,
246 | .ExternalClass font,
247 | .ExternalClass td,
248 | .ExternalClass div {
249 | line-height: 100%; }
250 | .apple-link a {
251 | color: inherit !important;
252 | font-family: inherit !important;
253 | font-size: inherit !important;
254 | font-weight: inherit !important;
255 | line-height: inherit !important;
256 | text-decoration: none !important; }
257 | .btn-primary table td:hover {
258 | background-color: #00ffde !important; }
259 | .btn-primary a:hover {
260 | background-color: #00ffde !important;
261 | border-color: #00ffde !important; } }
262 |
263 |
264 |
--------------------------------------------------------------------------------