Friday, 28 December 2018

Textnow unlimited US & Canada Calling and Texting $9.99

Technology uses a hybrid of Wi-Fi + cellular for texting and calling. This means low-cost plans with affordable, name-brand smartphones.







Sunday, 23 December 2018

Deploy React + Express to Heroku

You’ve got a React app, and an API server written in Express or something else. Now – how do you deploy them both to a server?

This article will cover how to keep them together. We’ll build the Express server to serve React’s static files in addition to providing an API, and then deploy it to Heroku. Heroku is easy to deploy to and free to get started with.


heroku logs

remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Node.js app detected
remote:
remote: -----> Creating runtime environment
remote:
remote:        NPM_CONFIG_LOGLEVEL=error
remote:        NODE_ENV=production
remote:        NODE_MODULES_CACHE=true
remote:        NODE_VERBOSE=false
remote:
remote: -----> Installing binaries
remote:        engines.node (package.json):  unspecified
remote:        engines.npm (package.json):   unspecified (use default)
remote:
remote:        Resolving node version 10.x...
remote:        Downloading and installing node 10.14.2...
remote:        Using default npm version: 6.4.1
remote:
remote: -----> Building dependencies
remote:        Installing node modules (package.json + package-lock)
remote:        added 51 packages from 39 contributors and audited 124 packages in 2.565s
remote:        found 0 vulnerabilities
remote:
remote:        Running heroku-postbuild
remote:
remote:        > react-express-heroku@1.0.0 heroku-postbuild /tmp/build_8618b4496c28b4d8453989494ad86267
remote:        > cd client && npm install && npm run build
remote:
remote:        added 1725 packages from 667 contributors and audited 35703 packages in 36.53s
remote:        found 0 vulnerabilities
remote:
remote:
remote:        > client@0.1.0 build /tmp/build_8618b4496c28b4d8453989494ad86267/client
remote:        > react-scripts build
remote:
remote:        Creating an optimized production build...
remote:        Compiled with warnings.
remote:
remote:        ./src/index.js
remote:          Line 5:  'serviceWorker' is defined but never used  no-unused-vars
remote:
remote:        Search for the keywords to learn more about each warning.
remote:        To ignore, add // eslint-disable-next-line to the line before.
remote:
remote:        File sizes after gzip:
remote:
remote:          34.7 KB  build/static/js/1.4a34e45d.chunk.js
remote:          763 B    build/static/js/runtime~main.229c360f.js
remote:          749 B    build/static/js/main.12ccf6aa.chunk.js
remote:          613 B    build/static/css/main.9a92f39c.chunk.css
remote:
remote:        The project was built assuming it is hosted at the server root.
remote:        You can control this with the homepage field in your package.json.
remote:        For example, add this to build it for GitHub Pages:
remote:
remote:          "homepage" : "http://myname.github.io/myapp",
remote:
remote:        The build folder is ready to be deployed.
remote:        You may serve it with a static server:
remote:
remote:          npm install -g serve
remote:          serve -s build
remote:
remote:        Find out more about deployment here:
remote:
remote:          http://bit.ly/CRA-deploy
remote:
remote:
remote: -----> Caching build
remote:        - node_modules
remote:
remote: -----> Pruning devDependencies
remote:        audited 124 packages in 0.853s
remote:        found 0 vulnerabilities
remote:
remote:
remote: -----> Build succeeded!
remote: -----> Discovering process types
remote:        Procfile declares types     -> (none)
remote:        Default types for buildpack -> web
remote:
remote: -----> Compressing...
remote:        Done: 69.7M
remote: -----> Launching...
remote:        Released v3
remote:        https://chuanshuoge1-react-express.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.

-----------------------------------------------

package.json

{
  "name": "react-express-heroku",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "heroku-postbuild": "cd client && npm install && npm run build"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.16.4",
    "password-generator": "^2.2.0"
  }
}

-----------------------------------------------------

client/package.json

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-scripts": "2.1.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ],
//for testing, ignored by heroku
  "proxy": "http://localhost:5000"
}

-------------------------------------------

index.js

const express = require('express');
const path = require('path');
const generatePassword = require('password-generator');

const app = express();

// Serve static files from the React app
app.use(express.static(path.join(__dirname, 'client/build')));

// Put all API endpoints under '/api'
app.get('/api/passwords', (req, res) => {
  const count = 5;

  // Generate some passwords
  const passwords = Array.from(Array(count).keys()).map(i =>
    generatePassword(12, false)
  )

  // Return them as json
  res.json(passwords);

  console.log(`Sent ${count} passwords`);
});

// The "catchall" handler: for any request that doesn't
// match one above, send back React's index.html file.
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname+'/client/build/index.html'));
});

const port = process.env.PORT || 5000;
app.listen(port);

console.log(`Password generator listening on ${port}`);

----------------------------------------------

client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
//serviceWorker.unregister();

---------------------------------------------

client/app.js

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  // Initialize state
  state = { passwords: [] }

  // Fetch passwords after first mount
  componentDidMount() {
    this.getPasswords();
  }

  getPasswords = () => {
    // Get the passwords and store them in state
    fetch('/api/passwords')
      .then(res => res.json())
      .then(passwords => this.setState({ passwords }));
  }

  render() {
    const { passwords } = this.state;

    return (
      <div className="App">
        {/* Render the passwords if we have them */}
        {passwords.length ? (
          <div>
            <h1>5 Passwords.</h1>
            <ul className="passwords">
              {/*
                Generally it's bad to use "index" as a key.
                It's ok for this example because there will always
                be the same number of passwords, and they never
                change positions in the array.
              */}
              {passwords.map((password, index) =>
                <li key={index}>
                  {password}
                </li>
              )}
            </ul>
            <button
              className="more"
              onClick={this.getPasswords}>
              Get More
            </button>
          </div>
        ) : (
          // Render a helpful message otherwise
          <div>
            <h1>No passwords :(</h1>
            <button
              className="more"
              onClick={this.getPasswords}>
              Try Again?
            </button>
          </div>
        )}
      </div>
    );
  }
}

export default App;

------------------------------------------

reference:
https://daveceddia.com/deploy-react-express-app-heroku/

Thursday, 20 December 2018

next.js


server.js

const express = require('express')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare()
.then(() => {
  const server = express()

  server.get('/p/:id', (req, res) => {
    const actualPage = '/post'
    const queryParams = { id: req.params.id } 
    app.render(req,res,actualPage, queryParams)
  })

  server.get('*', (req, res) => {
    return handle(req, res)
  })

  server.listen(3000, (err) => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})
.catch((ex) => {
  console.error(ex.stack)
  process.exit(1)
})

-------------------------------------------

pages/index.js

import Layout from '../components/layout'
import Link from 'next/link'
import fetch from 'isomorphic-unfetch'


const Index = (props) => (
  <Layout>
    <h1>Batman TV Shows</h1>
    <ul>
      {props.shows.map(({show}) => (
        <li key={show.id}>
          <Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
            <a>{show.name}</a>
          </Link>
        </li>
      ))}
    </ul>
  </Layout>
)

Index.getInitialProps = async function() {
    const res = await fetch('https://api.tvmaze.com/search/shows?q=batman')
    const data = await res.json()
  
    console.log(`Show data fetched. Count: ${data.length}`)
  
    return {
      shows: data
    }
  }
  
  export default Index

----------------------------------------

pages/post.js

import {withRouter} from 'next/router'
import Layout from '../components/layout'

const Page = withRouter((props) => (
    <Layout>

       <h1>{props.show.name}</h1>
       <p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
       <img src={props.show.image.medium}/>
    </Layout>
))

Page.getInitialProps = async function (context) {
    console.log(context.query);
    const { id } = context.query
    const res = await fetch(`https://api.tvmaze.com/shows/${id}`)
    const show = await res.json()
  
    console.log(`Fetched show: ${show.name}`)
  
    return { show }
  }

export default Page

---------------------------------------

components/layout.js

import Header from './header'

const layoutStyle = {
  margin: 20,
  padding: 20,
  border: '1px solid #DDD'
}

const Layout = (props) => (
  <div style={layoutStyle}>
    <Header />
    {props.children}
  </div>
)

export default Layout

-----------------------------------------

components/header

import Link from 'next/link'

const linkStyle = {
  marginRight: 15
}

const Header = () => (
    <div>
        <Link href="/">
          <a style={linkStyle}>Home</a>
        </Link>
        <Link href="/about">
          <a style={linkStyle}>About</a>
        </Link>
    </div>
)

export default Header

------------------------------------------

reference:

https://nextjs.org/learn

deploy node express
https://zeit.co/examples/nodejs-express/

deploy next
https://zeit.co/examples/nextjs/

Tuesday, 18 December 2018

Bank express + ejs + mongodb, authentication jwt + bcrypt

project urlhttps://chuanshuoge1-bank.herokuapp.com/
welcome page

signup page register bob (password abc) sam (password 123)

check customers, bob, sam are registered, password in database is hashed

log in to bob's account

obtain a token from server after authentication, valid token is required for all transactions. 
token is valid for 2min, sign in again to obtain another after expiration

within token valid time, deposit $100

with valid token, withdraw $5

wire transfer $50 to sam

login to sam's account, sam receives $50 from bob

check accounts on mongodb cloud

accout schema

package.json

{
  "name": "express-mango-signin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.18.0",
    "bcryptjs": "^2.4.3",
    "ejs": "^2.6.1",
    "express": "^4.16.4",
    "express-jwt": "^5.3.1",
    "jsonwebtoken": "^8.4.0",
    "mongoose": "^5.3.15",
    "mongoose-timestamp": "^0.6.0"
  },
  "devDependencies": {
    "nodemon": "^1.18.8"
  }
}

--------------------------------------------------------------

index.js

const express = require('express');
const mongoose = require('mongoose');
const config = require('./config');
const bodyParser = require('body-parser');
const account = require('./models/account_model');

const app = express();

app.set('view engine', 'ejs');

//Body parser Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

//welcome page
app.get('/', (req, res) => res.render('pages/index'));

//get all users' login
app.get('/users', async (req, res, next)=>{
    try{
        const users = await account.find({});
        let user_login =[];
        users.forEach(user => {
            user_login.push({name: user.name, password: user.password});
        });
        res.render('pages/users', {users: user_login})
    }catch(err){
        return next(err);
    }
})

//signup page
app.get('/signup', (req, res) => res.render('pages/signup'));

//login page
app.get('/auth', (req, res) => res.render('pages/login'));

//delete a user
/%
app.delete('/users/:id', async(req, res, next)=>{

    try{
        const new_user = await 
        account.findOneAndRemove({_id: req.params.id});
        res.sendStatus(204)
    }catch(err){
        return next(new Error('user not found'));
    }
});%/

app.listen(config.PORT, ()=>{
    mongoose.connect(config.MONGODB_URI, {useNewUrlParser: true});
});

const db = mongoose.connection;

db.on('error', (err)=>console.log(err));

db.once('open', ()=>{
    require('./routes/signup')(app);
    require('./routes/login')(app);
    require('./routes/userAccount')(app);
    console.log('server started on port ',config.PORT);
});

------------------------------------------------------

models/account_model.js

const mongoose = require('mongoose');
const timestamp= require('mongoose-timestamp');

const account_schema = new mongoose.Schema({
    name:{
        type: String,
        required: true,
        trim: true
    },
    balance:{
        type: Number,
        trim: true
    },
    password:{
        type: String,
        required: true
    },
    ledger:{
        type: [String],
    }
})

account_schema.plugin(timestamp);

const account = mongoose.model('account', account_schema);

module.exports = account;

----------------------------------------------------------

routes/signup.js

const bcrypt = require('bcryptjs');
const account = require('../models/account_model');

module.exports = app => {

    //add a user
    app.post('/signup', async(req, res, next)=>{
    
        const {name, password} = req.body;

        //check if user exist
        const existing_user = await account.findOne({name: name});
        //user exist return
        if(existing_user){
            res.render('pages/signup',  {message: 'user already exists'});
            return;
        }
    
        const user = new account({
            name,
            password,
            balance: 0,
            ledger: [],
        });
    
        bcrypt.genSalt(5, (err, salt) => {
            bcrypt.hash(user.password, salt, async(err, hash) => {
                //hash password
                user.password = hash;
                //save user
                try{
                    const new_user = await user.save();
                    res.render('pages/signup',  {message: 'user created'});
                }catch(err){
                    //return next(err);
                    res.render('pages/signup',  {message: err.toString()});
                }
            });
        });
        
    });
}

-----------------------------------------------------

routes/login.js

const bcrypt = require('bcryptjs');
const mongoose = require('mongoose');
const account = mongoose.model('account');
const jwt = require('jsonwebtoken');
const config = require('../config');

const authenticate = (name, password) => {
    return new Promise(async (resolve, reject)=>{
        try{
            //get user by name
            const user = await account.findOne({name: name});

            //check password
            bcrypt.compare(password, user.password, (err,isMatch)=>{
                if(err){throw err;}
                if(isMatch){
                    resolve(user);
                }else{
                    //password didn't match
                    reject('user/password not match');
                }
            });
        }catch(err){
            //user name not found
            reject('user not exist');
        }
    })
}

module.exports = app => {
    app.post('/auth',async (req,res,next)=>{
        const {name, password} = req.body;

        try{
            //authenticate user
            const user = await authenticate(name, password);
            
            //create jwt
            const token = jwt.sign(user.toJSON(), config.JWT_SECRET, {
                expiresIn: '2m'
            });

            const {iat, exp} = jwt.decode(token);

            //token duration
            const expire = exp - iat;

            //respond with token
            //res.send({iat, exp, token});
            res.render('pages/login', 
            {
             message: 'login successful',
             user: {name: user.name, id: user._id},   //only pass id, name to front end
             token,
             expire,
             projectURL: config.URL,
        });

        }catch(err){
            //return next(err);
            res.render('pages/login',  {message: err.toString()});
        }
    });
}

------------------------------------------------------

routes/userAccount.js


const mongoose = require('mongoose');
const account = mongoose.model('account');
const config = require('../config');
const rjwt = require('express-jwt');

module.exports = app => {
    //protected route, retrieve user account with token and id 
    app.post('/balance/:id', rjwt({secret: config.JWT_SECRET}), async (req,res,next)=>{

        try{
            //get user by id
            const user = await account.findById(req.params.id);
            
            res.send(user);

        }catch(err){
            return next(err);
        }
    });

    //protected route, update user balance with token and id
    app.put('/deposite/:id', rjwt({secret: config.JWT_SECRET}), async (req,res,next)=>{
        
        const { amount} = req.body;
        const {id} = req.params;

        try{
            //get user by id
            const user_old = await account.findById(id);

            //calculate balance after transaction
            const new_balance = user_old.balance + parseFloat(amount); 

            //record transaction
            const d = new Date();
            const transaction = d.toISOString() + ' deposite $' + amount;

            //only save last 10 transactions in ledger
            const new_ledger = [...user_old.ledger].concat(transaction).splice(-10,10);

            //update balance and ledger
            await account.findOneAndUpdate({_id: id}, {"balance": new_balance.toFixed(2), "ledger": new_ledger});
            
            //get user by id
            const user_new = await account.findById(id);
            
            res.send(user_new);

        }catch(err){
            return next(err);
        }
    });

    //protected route, update user balance with token and id
    app.put('/withdraw/:id', rjwt({secret: config.JWT_SECRET}), async (req,res,next)=>{
        
        const { amount} = req.body;
        const {id} = req.params;

        try{
            //get user by id
            const user_old = await account.findById(id);

            //calculate balance after transaction
            const new_balance = user_old.balance - parseFloat(amount);
         
            //new balance < 0 throw error, protect over withdraw on server side
            if(new_balance < 0){throw 'not enough $ in account';}

            //record transaction
            const d = new Date();
            const transaction = d.toISOString() + ' withdraw $' + amount;

            //only save last 10 transactions in ledger
            const new_ledger = [...user_old.ledger].concat(transaction).splice(-10,10);

            //update balance and ledger
            await account.findOneAndUpdate({_id: id}, {"balance": new_balance.toFixed(2), "ledger": new_ledger});
            
            //get user by id
            const user_new = await account.findById(id);

            res.send(user_new);

        }catch(err){
            //return next(err);
            res.send({error: err.toString()});
        }
    });

    //protected route, update user balance with token and id
    app.put('/wireTransfer/:id', rjwt({secret: config.JWT_SECRET}), async (req,res,next)=>{
        
        const { amount, recipient} = req.body;
        const {id} = req.params;

        try{
            //if recipient not found, throw error
            const recipientAccount = await account.findOne({name: recipient});
            if(recipientAccount===null){throw 'wire transfer recipient not found';}

            //get user by id
            const user_old = await account.findById(id);

            // balances 
            const new_balance = user_old.balance - parseFloat(amount);
            const new_balance_recipient = recipientAccount.balance + parseFloat(amount);
            
            //new balance < 0 throw error, protect over withdraw on server side
            if(new_balance < 0){throw 'not enough $ in account';}

            //record transactions
            const d = new Date();
            const transactionSend = d.toISOString() + ' send $' + amount + ' to ' + recipient;
            const transactionReceive = d.toISOString() + ' receive $' + amount + ' from ' + user_old.name;

            //only save last 10 transactions in ledger
            const new_ledgerSender = [...user_old.ledger].concat(transactionSend).splice(-10,10);
            const new_ledgerReceiver = [...recipientAccount.ledger].concat(transactionReceive).splice(-10,10);

            //update accounts
            await account.findOneAndUpdate({_id: id}, {"balance": new_balance.toFixed(2), "ledger": new_ledgerSender});  
            await account.findOneAndUpdate({_id: recipientAccount._id}, {"balance": new_balance_recipient.toFixed(2), "ledger": new_ledgerReceiver});
            
            //get user by id
            const user_new = await account.findById(id);
            
            res.send(user_new);

        }catch(err){
            //return next(err);
            res.send({error: err.toString()});
        }
    });

    //display user account
    app.post('/userAccount/:id', (req,res,next)=>{
        const {userId, name, balance, created, updated, message, ledger} = req.body;
        
        res.render('pages/userAccount', {
            id: userId,
            name,
            balance,
            created,
            updated,
            message,
            ledger: ledger.split(','),
        });
    });
}

-----------------------------------------------------------------------------------

partials/localstorage.ejs

<!-- after authentication, store user, token in localstorage -->
<% if (typeof user != 'undefined'){ %>

    <div id='user' style="display: none;"><%= user.name %></div> 
    <div id='id' style="display: none;"><%= user.id %></div>       
  <% } %>

<% if (typeof token != 'undefined'){ %>

    <div id='token' style="display: none;"><%= token %></div>       
  <% } %>

<% if (typeof expire != 'undefined'){ %>

    <div id='expire' style="display: none;"><%= expire %></div>       
  <% } %>

<% if (typeof projectURL != 'undefined'){ %>

    <div id='projectURL' style="display: none;"><%= projectURL %></div>       
  <% } %>
  
  <script>
    var user = document.getElementById("user").innerHTML.trim();
    var id = document.getElementById("id").innerHTML.trim();
    var token = document.getElementById("token").innerHTML.trim();
    var expire = document.getElementById("expire").innerHTML.trim();
    var projectURL = document.getElementById("projectURL").innerHTML.trim();
  
    if(user !== null){
        localStorage.setItem("user", user);
    }

    if(id !== null){
        localStorage.setItem("id", id);
    }

    if(token !== null){
        localStorage.setItem("token", token);
        var d = new Date();
        localStorage.setItem("issued", d.getTime());
    }

    if(expire !== null){
        localStorage.setItem("expire", expire);
    }

    if(projectURL !== null){
        localStorage.setItem("projectURL", projectURL);
    }
    
  </script>

---------------------------------------------------------------------

partials/redirecToAccount.ejs

 <!-- hidden form used for redirect to user account once authenticated -->
 <form style="display: none;" class="container" method="POST" id='redirectForm'>
    <input type="hidden" id='userId' name='userId'>
    <input type="hidden" id='userName' name='name'>
    <input type="hidden" id='userBalance' name='balance'>
    <input type="hidden" id='userCreated' name='created'>
    <input type="hidden" id='userUpdated' name='updated'>
    <input type="hidden" id='message' name='message'>
    <input type="hidden" id='ledger' name='ledger'>
    <button id='redirectButton' type="submit"></button>
  </form>

  <script>
      //retrieve user account with token and id from database, then redirect to account page
    function rediectToAccount(method, route, amount, recipient){

       var projectURL = localStorage.getItem("projectURL");
       var id = localStorage.getItem("id");
       var token = localStorage.getItem("token");

       var postData = {"amount": amount, "recipient": recipient};

       var jwt = 'Bearer ' + token;
       var headers = {
           'Content-Type': 'application/json',
           'Authorization':  jwt,
           "Access-Control-Allow-Origin": "*",
        }

       var actionURL = projectURL + route + '/' + id;

       axios({
           method: method,
           url: actionURL,
           data: postData,
           headers: headers,
        })
       .then((res)=>{
           const {_id, name, balance, createdAt, updatedAt, ledger, error} = res.data;

           //if server didn't reply user info, throw error meessage
           if(typeof _id === 'undefined'){throw error;}

           var userId = document.getElementById('userId');
           var userName = document.getElementById('userName');
           var userBalance = document.getElementById('userBalance');
           var userCreated = document.getElementById('userCreated');
           var userUpdated = document.getElementById('userUpdated');
           var successMessage = document.getElementById('message');
           var userLedger = document.getElementById('ledger');

           userId.value = _id;
           userName.value = name;
           userCreated.value = createdAt;
           userUpdated.value = updatedAt;
           successMessage.value = route.slice(1) + ' processed';
           userLedger.value = ledger;

           if(typeof balance === 'undefined'){
               userBalance.value = 0;
           }else{
               userBalance.value = balance;
           }

           //set redirect form submit route
           document.getElementById('redirectForm').action = '/userAccount/'+_id;

           //submit form, redirect to account page
           document.getElementById('redirectButton').click();
       })
       .catch((err) => {
           alert(err);
           //if token is invalid or over withdraw, logout, redirect to welcome page
           logout();
           window.location.href = projectURL;
       })
    }

    //remove user, token from local storage when logged out
    function logout(){
      localStorage.removeItem("user");
      localStorage.removeItem("token");
      localStorage.removeItem("expire");
    }
  </script>

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

-------------------------------------------------------------------

partials/headers.ejs

<!-- retrieve user account with token from database, then redirect to account page -->
<% include ./redirectToAccount%>

<nav class="navbar sticky-top navbar-expand-sm navbar-light" style="background-color: #e3f2fd;">
        <a class="navbar-brand" href="/">Bank</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
          <span class="navbar-toggler-icon"></span>
        </button>
      
        <div class="collapse navbar-collapse" id="navbarNav">
          <ul class="navbar-nav mr-auto">           

            <li class="nav-item">
              <a class="nav-link" href="/users">Customers</a>
            </li> 

          </ul>
          
          <ul id='menuRight' class="navbar-nav my-2 my-lg-0">

          </ul>
        </div>
      </nav>

  <script> 
    var user = localStorage.getItem("user");
    
    if(user !== null){
      //redirectToAccount() included in redirectToAccount.ejs
      document.getElementById('menuRight').innerHTML = 
      "<li><a class='nav-link' onclick='toAccount()'>" + user + "</a></li>" + 
      "<li><a class='nav-link' href='/' onclick='logout()'>Logout</a></li>";
    }else{
      document.getElementById('menuRight').innerHTML = 
      "<li><a class='nav-link' href='/signup'>Signup</a></li>" + 
      "<li><a class='nav-link' href='/auth'>Login</a></li>";
    }

    function toAccount(){
      rediectToAccount('post', '/balance');
    }
  </script>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>

--------------------------------------------------------

views/userAccount.ejs

<!-- views/pages/index.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
    <% include ../partials/head %>
</head>
<body>

<header>

    <% include ../partials/header %>
</header>

<main>
    <div class="jumbotron">
        <button class="btn btn-warning btn-sm" data-toggle="collapse" data-target="#token">Token</button>
        Valid for <span class="text-danger" id='timer'></span>S 
        <button class="btn btn-danger btn-sm" onclick="revoke()">Revoke</button><br><br>
        <span id="token" class="collapse"></span>

        <span>Customer ID: <%= id %></span><br>
        <span>Customer Since: <%= created %></span><br>
        <span>Last Transaction: <%= updated %></span><br><br>       
        <span>Balance: $<span id='balance'><%= balance %></span></span><br> 

        <% if (typeof message != 'undefined'){ %>
            <span class="text-info">Status: <%= message %></span><br>      
          <% } %>

        <table class="table">
            <thead>
                <tr>
                    <th scope="col">Action</th>
                    <th scope="col">Parameter</th>
                </tr>
                <tbody>
                    <tr>
                        <td> <button class="btn btn-success btn-sm" onclick="deposite()">Deposite</button></td>
                        <td>$<input type="number" id="depositeAmount" onchange="abs(this)" value=0></td>
                    </tr>
                    <tr>
                        <td><button class="btn btn-success btn-sm" onclick="withdraw()">Withdraw</button> </td>
                        <td>$<input type="number" id="withdrawAmount" onchange="abs(this)" value=0></td>
                    </tr>
                    <tr>
                            <td><button class="btn btn-success btn-sm" onclick="wireTransfer()">Wire Transfer</button> </td>
                            <td>
                                <div class="row">
                                    <div class="col">
                                         $<input type="number" id="transferAmount" onchange="abs(this)" value=0 >
                                    </div>
                                    <div class="col">
                                        To <input type="text" id="recipient" placeholder="recipient name">
                                    </div>
                                </div>
                            </td>
                        </tr>
                </tbody>
            </thead>
        </table>

        <h4>Transaction History</h4>

        <% ledger.forEach(function(transaction) { %>
            <span><%= transaction %></span><br>
          <% }); %>

    </div>
    <script>
        document.getElementById('token').innerHTML = localStorage.getItem('token');

        //token expire timer
        var duration = localStorage.getItem('expire');
        var issued = localStorage.getItem('issued');
        var timer = document.getElementById('timer');

        var counter = setInterval(()=>{
            //second elapsed since token issued
            var d = new Date();
            var elapsed = parseInt((d.getTime() - parseInt(issued))/1000);
            var countdown = parseInt(duration) - elapsed;

            timer.innerHTML = countdown;

            //stop timer when token expire
            if(countdown <= 0){
                clearInterval(counter);
             
            }
        },1000)

        //revoke token when button clicked
        function revoke(){
            localStorage.removeItem("token");
            clearInterval(counter);
            timer.innerHTML = 0;
            document.getElementById('token').innerHTML = 'revoked';
        }

        //get absolute 2 decimal precision of amount
        function abs(obj){
            obj.value = Math.abs(obj.value).toFixed(2);
        }

        //redirect to deposite route in userAccount.js
        function deposite(){
            var amount = document.getElementById('depositeAmount').value;
            rediectToAccount('put', '/deposite', amount);
        }

        //redirect to withdraw route in userAccount.js
        function withdraw(){
            var amount = document.getElementById('withdrawAmount').value;
            //check if account has enough $
            if(!checkWithdraw(amount)){return}

            rediectToAccount('put', '/withdraw', amount);
        }

        //check if $ is enough in account for withdraw
        function checkWithdraw(withdrawAmount){
            var balance = document.getElementById('balance').innerHTML;

            //protect over withdraw from client side
            if(withdrawAmount > parseFloat(balance)){
                alert('short of $ in account');
                return false;
            }
            return true;
        }

        //redirect to wire transfer route in userAccount.js
        function wireTransfer(){
            var recipient = document.getElementById('recipient').value;
            var amount = document.getElementById('transferAmount').value;

            //if recipient is not entered, return
            if(recipient===''){alert('recipient empty'); return}

            //check if account has enough $
            if(!checkWithdraw(amount)){return}

            rediectToAccount('put', '/wireTransfer', amount, recipient);
        }
    </script>
    
</main>

<footer>
    <% include ../partials/footer %>
</footer>

</body>
</html>

---------------------------------------------------------------------------

pages/login.ejs

<!-- views/pages/index.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
    <% include ../partials/head %>
</head>
<body>

<header>
    <!-- authentication passed, store user, token in local storage -->
    <% include ../partials/localStorage %>

    <% include ../partials/header %>
   
</header>

<main>  
    <form class="container" method="POST" action="/auth">
        <h4>Login</h4>  
        <div class="form-group">
          <label for="exampleInputName">User Name</label>
          <input type="text" class="form-control" name="name" id="exampleInputName" placeholder="User Name" oninput="inputChange()">
        </div>
        <div class="form-group">
          <label for="exampleInputPassword1">Password</label>
          <input type="password" class="form-control" name="password" id="exampleInputPassword1" placeholder="Password" oninput="inputChange()">
        </div>
        
        <button type="submit" id='submit_button' disabled=true class="btn btn-primary">Login</button>
        
        <% if (typeof message != 'undefined'){ %>
            <p class="text-info"><%= message %>
            <br><span id='timer'></span></p>
        <% } %>
      </form>

</main>

<footer>
    <% include ../partials/footer %>
</footer>

<script>
   
    function inputChange() {
        var name = document.getElementById('exampleInputName').value;
        var password = document.getElementById('exampleInputPassword1').value;
        var submit_button = document.getElementById('submit_button');

        if(name !== '' && password !== ''){
            submit_button.disabled = false;
        }else{
            submit_button.disabled = true;
        }
    }

    //redirect to user account if authentication is successful
    var user = localStorage.getItem("user");

    if(user !== null){
        //redirect timer
        var seconds = 0;
        var timer = document.getElementById('timer');
        timer.innerHTML = 'redirecting to my account ';

        var counter = setInterval(()=>{
            timer.innerHTML += '$';
            seconds++;

            if(seconds === 10){
                clearInterval(counter);
                
                //wait for 1 seconds before redirect to account
                //function included in redirectToAccount.ejs in header.ejs
                rediectToAccount('post', '/balance');
                
            }
        },100)
    }

</script>
</body>
</html>

-------------------------------------------------------------

config.js

module.exports={
    ENV:process.env.NODE_ENV || 'development',
    PORT:process.env.PORT || 3000,
    URL: process.env.BASE_URL || 'http://localhost:3000',
    MONGODB_URI: process.env.MONGODB_URI || 
    'mongodb://xxxxxx@ds131954.mlab.com:31954/bank',
    JWT_SECRET: process.env.JWT_SECRET || 'abc'
}

---------------------------------------------

pages/user.ejs

<!-- views/pages/index.ejs -->

<!DOCTYPE html>
<html lang="en">
<head>
    <% include ../partials/head %>
</head>
<body>

<header>
    <% include ../partials/header %>
</header>

<main>       
  <table class="table">
    <thead class="thead-light">
      <tr>
        <th scope="col">Name</th>
        <th scope="col">Hashed Password</th>
      </tr>
    </thead>
    <tbody>
     <% users.forEach(function(user) { %>
         <tr>
           <td> <%= user.name %> </td>
           <td> <%= user.password %></td> 
         </tr>
       <% }); %>
          
    </tbody>
  </table>
</main>

<footer>
    <% include ../partials/footer %>
</footer>

</body>
</html>

-----------------------------------------------------

partials/head.ejs

<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">

<title>Authentication</title>

--------------------------------------------

partials/footer.ejs

<p class="text-center text-muted">© chuanshuoge 2018</p>

-------------------------------------------

.gitignore

node_modules

-----------------------------------------
reference: