Sunday, 30 December 2018

Bank react + redux + express + mongodb + heroku

project site: https://chuanshuoge-bank-react-express.herokuapp.com/

welcome page

customers page, password are hashed

login page


login bob, password abc

obtain a token from server for transaction. need log in again after expiration

account details

withdraw modal

wire transfer modal

cloud database


package.json

{
  "name": "react-redux-express",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js",
    "dev": "nodemon index.js",
    "heroku-postbuild": "cd client && npm install && npm run build"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "express": "^4.16.4",
    "express-jwt": "^5.3.1",
    "jsonwebtoken": "^8.4.0",
    "mongoose": "^5.4.0",
    "mongoose-timestamp": "^0.6.0"
  }
}

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

client/package.json

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "antd": "^3.11.6",
    "axios": "^0.18.0",
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-redux": "^6.0.0",
    "react-router-dom": "^4.3.1",
    "react-scripts": "2.1.2",
    "redux": "^4.0.1",
    "redux-logger": "^3.0.6",
    "redux-promise-middleware": "^5.1.1",
    "redux-thunk": "^2.3.0"
  },
  "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"
  ],
  "proxy": "http://localhost:5000"
}

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

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 path = require('path');

const app = express();

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

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

//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.send({users: user_login})
    }catch(err){
        return next(err);
    }
})

// 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'));
  });

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);
});

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

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.send( 
            {
             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.send(  {message: err.toString()});
        }
    });
}

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

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.send({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.send({message: 'user created'});
                }catch(err){
                    //return next(err);
                    res.send({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);
            res.send({errorMessage: err.toString()});
        }
    });

    //protected route, update user balance with token and id
    app.put('/deposit/: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);
            res.send({errorMessage: err.toString()});
        }
    });

    //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({errorMessage: 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({errorMessage: err.toString()});
        }
    });

}

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

client/src/app.js

import React, { Component } from 'react';
import './App.css';
import { Switch, Route, Link } from 'react-router-dom';
import  Header  from './partials/header';
import Body from './partials/body';
import Footer from './partials/footer';

class App extends Component {
    render() {
        return (
            <div>
                <Header />
                <Body />
                <Footer/>
            </div>
        );
    }
}

export default App;

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

client/src/partials/header.js

import React, { Component } from 'react';
import { Switch, Route, Link } from 'react-router-dom'
import 'antd/dist/antd.css';
import { Menu, Icon } from 'antd';
import '../App.css';
import { connect } from 'react-redux';
import { resetStorage } from '../redux/actions/reset';
import { Redirect } from 'react-router-dom'

class Header extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            menu_mode: null,
            current: 'bank',
            redirectURL: '',
        };
    }
    
    componentWillMount(){
        //change menu mode based on screen width
        const mode = window.innerWidth > 434 ? "horizontal" : "inline";
        this.setState({menu_mode: mode});
    }

    handleClick = (e) => {

        this.setState({
          current: e.key,
        });
      }

    handleLogout = async () =>{

       await this.props.dispatch(resetStorage());
       //redirect to welcome page
       this.setState({redirectURL:'/'});
    }

    render() {
        const {redirectURL} = this.state;
        //if redirect url is available, redirect
        if (redirectURL !== '') {
            //clear redirect url
            this.setState({redirectURL: ''});
            return <Redirect to={redirectURL} />
          }

        //check if user has logged in
        let user = this.props.localStorage.name;
        const accountURL = '/account/' + this.props.localStorage.id;

        return (
            <Menu
                style = {{backgroundColor: '#E5E8E8'}}
                onClick={this.handleClick}
                selectedKeys={[this.state.current]}
                mode= {this.state.menu_mode}
              >
                <Menu.Item key="bank">
                    <Link to='/'> <Icon type="bank" /><b>Bank</b></Link>
                </Menu.Item>
                <Menu.Item key="customers">
                    <Link to='/customers'> <Icon type="team" />Customers</Link>
                </Menu.Item>

                {/*if not loged in show login, otherwise show logout*/}
                {user === ''? 
                <Menu.Item key="login" style={{float: 'right'}}>
                     <Link to='/login'> <Icon type="login" />Login</Link>
                 </Menu.Item>
                 :
                 <Menu.Item key="logout" onClick={()=>this.handleLogout()} style={{float: 'right'}}>
                      <Icon type="logout" />Logout
                 </Menu.Item>
                }

                {/*if not loged in show signup, otherwise show user  */}
                {user === ''? 
                 <Menu.Item key="signup" style={{float: 'right'}}>
                     <Link to='/signup'> <Icon type="user-add" />Signup</Link>
                 </Menu.Item>          
                 :
                 <Menu.Item key="user" style={{float: 'right'}}>
                     <Link to={accountURL}> <Icon type="smile" /> {user}</Link>
                 </Menu.Item>            
                }
                
            </Menu>   
        );
    }
}

export default connect(
    (store) => {
        return {
            localStorage: store.localStorage,
        };
    }
)(Header);

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

client/src/partial/body.js

import React, { Component } from 'react';
import '../App.css';
import { Switch, Route, Link } from 'react-router-dom';
import Home from '../pages/home';
import Customers from '../pages/customers';
import Login from '../pages/login';
import Signup from '../pages/signup';
import Account from '../pages/account';


export default class Body extends React.Component {

    render() {
        return (
            <main style={{backgroundColor: '#E5E8E8'}}>
                <Switch>
                    <Route exact path='/' component={Home} />
                    <Route path='/customers' component={Customers} />
                    <Route path='/login' component={Login} />
                    <Route path='/signup' component={Signup} />
                    <Route path='/account/:id' component={Account} />
                </Switch>
             </main>
        );
    }
}

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

client/src/partials/footer.js

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

export default class Footer extends React.Component {

    render() {
        return (
            <p style={{textAlign: 'center'}}>© chuanshuoge 2018</p>
        );
    }
}

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

client/src/pages/home.js

import React, { Component } from 'react';
import '../App.css';
import { Carousel, Icon } from 'antd';
import 'antd/dist/antd.css';

export default class Home extends React.Component {

    render() {
        return (
            <div>
                <Carousel autoplay>
                  <div><h3><Icon type="lock" /> Secure Banking</h3></div>
                  <div><h3><Icon type="swap" /> Easy Transfer</h3></div>
                  <div><h3><Icon type="calculator" /> Clear Transactions</h3></div>
                </Carousel>
                <div style={{padding: '16px'}}>
                    <p>Authentication with JWT and Bcrypt. Powered by React, Redux, Express, and Mongodb.</p>
                    <p>For testing, login password for bob is abc, login password for sam is 123</p>
                </div>
                 
            </div>
        );
    }
}

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

client/src/pages/customers.js

import React, { Component } from 'react';
import '../App.css';
import axios from 'axios';
import { Table } from 'antd';
import 'antd/dist/antd.css';

export default class Customers extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            customers: [],
            error: null,
            loading: true,
        };
    }
    
    componentWillMount(){
        axios({
            method: 'get',
            url: '/users',
        }).then((res)=>{
            this.setState({customers: res.data.users, loading: false});
        })
        .catch((err)=>{
            this.setState({error: err.toString()});
        })
    }

    render() {
        const message = this.state.error !== null ? this.state.error : '';

        const columns = [{
            title: 'Name',
            dataIndex: 'name',
            key: 'name',
          }, {
            title: 'Hashed Password',
            dataIndex: 'password',
            key: 'password',
          }];

        let data =[];
        
        this.state.customers.map((item, index)=>{
            data.push({
                key: index,
                name: item.name,
                password: item.password,
            });
        });

        return (
            <div style={{backgroundColor: '#E5E8E8'}}>
                <Table columns = {columns} dataSource={data} loading={this.state.loading}/>
                {message}
            </div>
        );
    }
}

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

client/src/pages/signup.js

import React, { Component } from 'react';
import '../App.css';
import {Form, Input, Button, Icon, message} from 'antd';
import 'antd/dist/antd.css';
import axios from 'axios';

export default class Signup extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            nameValid: null,
            passwordValid: null,
            name: '',
            password: '',
            submitButton: true,
            redirectURL: '',
        };
    }

    inputChange = async (e, p) =>{
        const pValid = p + 'Valid';

        //input empty return error
        if(e.target.value === ''){
            await this.setState({[pValid]: 'error', submitButton: true});
            return;
        }

        //password validation, require at least 3 keys input
        if(p==='password' && e.target.value.length <3){
            await this.setState({[pValid]: 'error', [p]: e.target.value, submitButton: true});
            return;
        }

        await this.setState({[p]: e.target.value, [pValid]: 'success'});

        //if all inputs ok, enable submit button
        if(this.state.nameValid==='success' && this.state.passwordValid==='success'){
            await this.setState({submitButton: false});
        }
    }

    handleSubmit = (e) =>{
        e.preventDefault();
        
        axios({
            method: 'post',
            url: '/signup',
            headers: {'Content-Type': 'application/json',  "Access-Control-Allow-Origin": "*",},
            data: {"name": this.state.name, "password": this.state.password}
        })
        .then(async (res)=>{
            res.data.message==='user created'?
            message.success(res.data.message):
            message.error(res.data.message);
            
        })
        .catch((err)=>{
            message.error(err.toString());
        })
    }

    render() {
     
        return (
            
            <Form onSubmit={(e)=>this.handleSubmit(e)} style={{backgroundColor: '#F8C471', padding: '16px'}}>
                <h3 style={{textAlign: 'center'}}>Signup</h3>
                <Form.Item
                    
                    hasFeedback
                    validateStatus= {this.state.nameValid}
                    help="don't leave empty"
                    >
                    <Input onChange={(e)=>this.inputChange(e,'name')} 
                        prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} 
                        placeholder="Username"></Input>
                </Form.Item>
                <Form.Item
                  
                    hasFeedback
                    validateStatus= {this.state.passwordValid}
                    help="minimum password contains 3 keys"
                    >
                    <Input onChange={(e)=>this.inputChange(e,'password')}
                        prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} 
                        type="password" placeholder="Password"></Input>
                </Form.Item>
                <Form.Item
                 
                >
                    <Button disabled={this.state.submitButton} type="primary" htmlType="submit">Submit</Button>
                </Form.Item>
                
            </Form>   
        );
    }
}

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

client/src/pages/login.js

import React, { Component } from 'react';
import '../App.css';
import {Form, Input, Button, Icon, message} from 'antd';
import 'antd/dist/antd.css';
import axios from 'axios';
import { connect } from 'react-redux';
import { updateStorage } from '../redux/actions/update';
import { Redirect } from 'react-router-dom'

class Login extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            nameValid: null,
            passwordValid: null,
            name: '',
            password: '',
            submitButton: true,
            redirectURL: '',
        };
    }

    inputChange = async (e, p) =>{
        const pValid = p + 'Valid';

        //input empty return error
        if(e.target.value === ''){
            await this.setState({[pValid]: 'error', submitButton: true});
            return;
        }

        //password validation, require at least 3 keys input
        if(p==='password' && e.target.value.length <3){
            await this.setState({[pValid]: 'error', [p]: e.target.value, submitButton: true});
            return;
        }

        await this.setState({[p]: e.target.value, [pValid]: 'success'});

        //if all inputs ok, enable submit button
        if(this.state.nameValid==='success' && this.state.passwordValid==='success'){
            await this.setState({submitButton: false});
        }
    }

    handleSubmit = (e) =>{
        e.preventDefault();
        
        axios({
            method: 'post',
            url: '/auth',
            headers: {'Content-Type': 'application/json',  "Access-Control-Allow-Origin": "*",},
            data: {"name": this.state.name, "password": this.state.password}
        })
        .then(async (res)=>{
            res.data.message==='login successful'?
            message.success(res.data.message):
            message.error(res.data.message);

            //login successful, record user, token in storage
            if(typeof res.data.user !== 'undefined' ){
                await this.props.dispatch(updateStorage(res.data));

                //redirect to account page
                const url = '/account/' + res.data.user.id;
                this.setState({redirectURL: url})
            }
        })
        .catch((err)=>{
            message.error(err.toString());
        })
    }

    render() {
        //if redirect url is available, redirect
        if (this.state.redirectURL !== '') {
            return <Redirect to={this.state.redirectURL} />
          }
     
        return (
            
            <Form onSubmit={(e)=>this.handleSubmit(e)} style={{backgroundColor: '#F8C471', padding: '16px'}}>
                <h3 style={{textAlign: 'center'}}>Login</h3>
                <Form.Item
                    
                    hasFeedback
                    validateStatus= {this.state.nameValid}
                    help="don't leave empty"
                    >
                    <Input onChange={(e)=>this.inputChange(e,'name')} 
                        prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />} 
                        placeholder="Username"></Input>
                </Form.Item>
                <Form.Item
                  
                    hasFeedback
                    validateStatus= {this.state.passwordValid}
                    help="minimum password contains 3 keys"
                    >
                    <Input onChange={(e)=>this.inputChange(e,'password')}
                        prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />} 
                        type="password" placeholder="Password"></Input>
                </Form.Item>
                <Form.Item
                 
                >
                    <Button disabled={this.state.submitButton} type="primary" htmlType="submit">Submit</Button>
                </Form.Item>
                
            </Form>   
        );
    }
}

export default connect(
    (store) => {
        return {
            localStorage: store.localStorage,
        };
    }
)(Login);

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

client/src/redux/store.js

import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import promiseMiddleware from 'redux-promise-middleware';

import reducer from "./reducers/storageReducer";

const middleware = applyMiddleware(promiseMiddleware(), thunk, logger);

export default createStore(reducer, middleware);

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

client/src/redux/actions/reset.js

export function resetStorage() {

    return {
        type: "reset",
        payload: {
        }
    }
}

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

client/src/redux/actions/update.js

export function updateStorage(data) {
    const d = new Date();

    return {
        type: "update",
        payload: {
            id: data.user.id,
            name: data.user.name,
            token: data.token,
            issued: d.getTime(),
            expire: data.expire,
            projectURL: data.projectURL,
        }
    }
}

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

client/src/redux/reducers/index.js


import { combineReducers } from 'redux';
import localstorage from "./storageReducer";

export default combineReducers(
    {
        localstorage
    })

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

client/src/redux/reducers/storageReducer.js

export default function reducer(
    state = {
        localStorage: {
            id: '',
            name: '',
            token: '',
            issued: '',
            expire: '',
            projectURL: '',
        }
    },
    action
) {
    switch (action.type) {
        case "update": {
            const newLocalStorage= {
                id: action.payload.id,
                name: action.payload.name,
                token: action.payload.token,
                issued: action.payload.issued,
                expire: action.payload.expire,
                projectURL: action.payload.projectURL,
            }
                    
            return { ...state, localStorage: newLocalStorage }
        }

        case 'reset': {
            const  newLocalStorage= {
                id: '',
                name: '',
                token: '',
                issued: '',
                expire: '',
                projectURL: '',
            }

            return { ...state, localStorage: newLocalStorage }
        }


        default:
            break;
    }
    return state;
}

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

client/src/pages/account.js

import React, { Component } from 'react';
import '../App.css';
import 'antd/dist/antd.css';
import { connect } from 'react-redux';
import { Tooltip, message } from 'antd';
import Counter from '../partials/tokenCounter';
import { accountQuery } from '../partials/accountQuery';
import TransactionPanel from '../partials/transactionPanel';

class Account extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            errorMessage:'',
            balance: '',
            created: '',
            ledger: [],
        };
    }

    componentWillMount(){
        //fetch customer account
        const url = '/balance/' + this.props.localStorage.id;
        this.transact('post', url);
    }

    transact = async (method, url, data) =>{
        this.setState({errorMessage: ''});

        if(this.props.localStorage.id === ''){
            this.setState({errorMessage: 'Please login first'});
            return;
        }

        const res = await accountQuery(this.props.localStorage.token, method, url, data);

        if(res.errorMessage){
            this.setState({errorMessage: res.errorMessage})
        }else{
            this.setState({balance: res.balance, created: res.created, ledger: res.ledger});
        }
    }

    withdraw = (amount) =>{

        const url = '/withdraw/' + this.props.localStorage.id;

        this.transact('put', url, {amount: amount});
    }

    deposit = (amount) =>{
        
        const url = '/deposit/' + this.props.localStorage.id;

        this.transact('put', url, {amount: amount});
    }

    transfer = (amount, recipiant) =>{
        const url = '/wireTransfer/' + this.props.localStorage.id;

        this.transact('put', url, {amount: amount, recipient: recipiant});
    }

    render() {    

        return (
           <div >
            <div style={{backgroundColor: '#F9E79F', padding: '16px'}}>
               
               {this.state.errorMessage!==''?
                <h3>Status: <span style={{color: 'red'}}>{this.state.errorMessage}</span></h3>:
                <h3>Status: <span style={{color: 'green'}}>Request processed</span></h3>
               }

               Time left <span style={{color: 'red'}}>{<Counter/>}S</span> to complete transaction. 
               Sign in again after <Tooltip placement="bottom" title={this.props.localStorage.token}><a>token</a></Tooltip> expires.
            </div>
            
            <TransactionPanel balance={this.state.balance} created={this.state.created}
                ledger={this.state.ledger} deposit={this.deposit} withdraw = {this.withdraw}
                transfer={this.transfer}/>
           </div>
        );
    }
}

export default connect(
    (store) => {
        return {
            localStorage: store.localStorage,
        };
    }
)(Account);

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

client/src/partials/accountQuery.js

import axios from 'axios';

export async function accountQuery(token, method, url, data){

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

    let result = null;

    await axios({
        method: method,
        url: url,
        headers: headers,
        data: data
    })
    .then((res)=>{
       if(res.data.errorMessage){
           result = {errorMessage: res.data.errorMessage};
        }else{
          result = {
               created: res.data.createdAt,
               balance: res.data.balance,
               ledger: res.data.ledger,
           }
        }
    })
    .catch((err)=>{
        result = {errorMessage: err.toString()}
    })

    return result;
}

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

client/src/partials/transactionPanel.js

import React, { Component } from 'react';
import '../App.css';
import { connect } from 'react-redux';
import {Row, Col, Modal, Button, Icon, Input } from 'antd';
import 'antd/dist/antd.css';
import Counter from '../partials/tokenCounter';

class TransactionPanel extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            visible: [false,false,false,false],
            amount: 0,
            recipiant: '',
        };
    }

    handleOpen = (n) =>{
        let v =[false,false,false, false];
        v[n] = true;
        this.setState({visible: v});
    }

    handleClose = () =>{
        let v =[false,false,false, false];
        this.setState({visible: v, amount: 0, recipiant: ''});
        //clear input fields
        if(document.getElementById('input1')){document.getElementById('input1').value = 0;}
        if(document.getElementById('input2')){document.getElementById('input2').value = 0;}
        if(document.getElementById('input3')){document.getElementById('input3').value = 0;}
        if(document.getElementById('input4')){document.getElementById('input4').value = '';}
    }

    inputChange = (e, name) => {
        //set precision to amount
        let amount =0;
        if(name==='amount'){
            amount = Math.abs(e.target.value).toFixed(2);
            this.setState({[name]: amount});
        }else{
            this.setState({[name]: e.target.value});
        }
    }

    render() {
        return (
            <div>
                 <div style={{padding: '16px'}}>
                <Row gutter={16}>
                    <Col span={12} className='col-style'><div onClick={()=>this.handleOpen(0)} className='button-style'><span>Account Detail</span></div></Col>
                    <Col span={12} className='col-style'><div onClick={()=>this.handleOpen(1)} className='button-style'><span>Withdraw</span></div></Col>
                    <Col span={12} className='col-style'><div onClick={()=>this.handleOpen(2)} className='button-style'><span>Deposit</span></div></Col>
                    <Col span={12} className='col-style'><div onClick={()=>this.handleOpen(3)} className='button-style'><span>Wire Transfer</span></div></Col>
                </Row>
             </div>

             <Modal title='Account Detail' visible={this.state.visible[0]} 
                onCancel={()=>this.handleClose()} 
                footer={[<Button key="back" onClick={()=>this.handleClose()}>Close</Button>]}>

                <p>Account {this.props.localStorage.id}</p>
                <p>Opened on {this.props.created}</p>
                <h2>Balance ${this.props.balance}</h2>
                <h3>Transaction History</h3>
                {
                    this.props.ledger.map((item, index)=>{
                        return <div key={index}>{item}</div>
                })}
             </Modal>

             <Modal title='Withdraw' visible={this.state.visible[1]} 
                onCancel={()=>this.handleClose()} 
                footer={[
                    <Button key="cancel" onClick={()=>this.handleClose()}>Cancel</Button>,
                    <Button key="submit" type="primary" 
                        onClick={()=>{
                            this.props.withdraw(this.state.amount); 
                            this.handleClose();}}

                            >Withdraw
                    </Button>,
                  ]}>
                
                <p><Icon type="exclamation-circle" theme='twoTone'
                 style={{ fontSize: '20px'}}/> Transaction expire in {<Counter/>}S</p>

                <p><Icon type="question-circle" theme='twoTone' 
                style={{fontSize: '20px'}}/> How much to withdraw?</p>

                <Input addonBefore='$' type='number' size='small' id='input1'
                     defaultValue={0} onChange={(e)=>this.inputChange(e, 'amount')}/>
                
             </Modal>

             <Modal title='Deposit' visible={this.state.visible[2]} 
                onCancel={()=>this.handleClose()} 
                footer={[
                    <Button key="cancel" onClick={()=>this.handleClose()}>Cancel</Button>,
                    <Button key="submit" type="primary" 
                        onClick={()=>{
                            this.props.deposit(this.state.amount);
                            this.handleClose();}}>
                            
                            Deposit
                    </Button>,
                  ]}>
                
                <p><Icon type="exclamation-circle" theme='twoTone'
                 style={{ fontSize: '20px'}}/> Transaction expire in {<Counter/>}S</p>

                <p><Icon type="question-circle" theme='twoTone' 
                style={{fontSize: '20px'}}/> How much to deposit?</p>

                <Input addonBefore='$' type='number' size='small' id='input2'
                     defaultValue={0} onChange={(e)=>this.inputChange(e, 'amount')}/>
                
             </Modal>

             <Modal title='Wire Transfer' visible={this.state.visible[3]} 
                onCancel={()=>this.handleClose()} 
                footer={[
                    <Button key="cancel" onClick={()=>this.handleClose()}>Cancel</Button>,
                    <Button key="submit" type="primary" 
                        onClick={()=>{
                            this.props.transfer(this.state.amount, this.state.recipiant);
                            this.handleClose()}}>
                            
                            Transfer
                    </Button>,
                  ]}>
                
                <p><Icon type="exclamation-circle" theme='twoTone'
                 style={{ fontSize: '20px'}}/> Transaction expire in {<Counter/>}S</p>

                <p><Icon type="question-circle" theme='twoTone' 
                style={{fontSize: '20px'}}/> How much to transfer?</p>

                <p><Input addonBefore='$' type='number' size='small' id='input3'
                     defaultValue={0} onChange={(e)=>this.inputChange(e, 'amount')}/></p>

                <p><Icon type="question-circle" theme='twoTone' 
                style={{fontSize: '20px'}}/> Who is the recipiant?</p>

                <Input addonBefore={<Icon type="user" />} size='small' id='input4'
                     placeholder='Recipiant' onChange={(e)=>this.inputChange(e, 'recipiant')}/>
                
             </Modal>
            </div>
        );
    }
}

export default connect(
    (store) => {
        return {
            localStorage: store.localStorage,
        };
    }
)(TransactionPanel);

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

client/src/partials/tokenCounter.js

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

class Counter extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            tokenExpire: null
        };
    }

    componentDidMount(){
        //token expire countdown
        const counter = setInterval(()=>{
            //second elapsed since token issued
            const d = new Date();
            const elapsed = parseInt((d.getTime() - this.props.localStorage.issued)/1000);
            const countdown = this.props.localStorage.expire - elapsed;

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

    render() {
        return (
            this.state.tokenExpire
        );
    }
}

export default connect(
    (store) => {
        return {
            localStorage: store.localStorage,
        };
    }
)(Counter);

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

client/src/app.css

.App {
  text-align: center;
}

.App-logo {
  animation: App-logo-spin infinite 20s linear;
  height: 40vmin;
}

.App-header {
  background-color: #282c34;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}

.App-link {
  color: #61dafb;
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.ant-carousel .slick-slide {
  text-align: center;
  height: 160px;
  line-height: 160px;
  background-color: rgb(203, 119, 236);
  overflow: hidden;
}

.ant-carousel .slick-slide h3 {
  color: black;
}

thead[class*="ant-table-thead"] th{
  background-color: rgb(159, 221, 58) !important;
}

.button-style{
  background-color: rgb(26, 66, 247);
  cursor: pointer;
  text-align:  center;
  height: 50px;
  border-radius: 10px;
  transition: all 0.5s;
  font-size: 20px;
  color: #FFFFFF;
  padding-top: 8px;
}

.button-style span{
  position: relative;
  transition: 0.5s;
}

.button-style span:after{
  content: '\00bb';
  position: absolute;
  opacity: 0;
  top: 0;
  right: -20px;
  transition: 0.5s;
}

.button-style:hover span{
  padding-right: 25px;
}

.button-style:hover span:after{
  opacity: 1;
  right: 0;
}

.col-style{
  height: 60px;
}

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

config.js

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

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

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.15.0...
remote:        Using default npm version: 6.4.1
remote:
remote: -----> Restoring cache
remote:        - node_modules
remote:
remote: -----> Building dependencies
remote:        Installing node modules (package.json + package-lock)
remote:        audited 200 packages in 1.167s
remote:        found 0 vulnerabilities
remote:
remote:        Running heroku-postbuild
remote:
remote:        > react-redux-express@1.0.0 heroku-postbuild /tmp/build_5d9f7c407a93dd483d0069209ace34a0
remote:        > cd client && npm install && npm run build
remote:
remote:        added 1844 packages from 822 contributors and audited 37654 packages in 42.123s
remote:        found 0 vulnerabilities
remote:
remote:
remote:        > client@0.1.0 build /tmp/build_5d9f7c407a93dd483d0069209ace34a0/client
remote:        > react-scripts build
remote:
remote:        Creating an optimized production build...

remote:        File sizes after gzip:
remote:
remote:          332.14 KB  build/static/js/1.1f33494b.chunk.js
remote:          51.38 KB   build/static/css/1.82b129ae.chunk.css
remote:          4.2 KB     build/static/js/main.36db12fd.chunk.js
remote:          781 B      build/static/css/main.8c2bac26.chunk.css
remote:          763 B      build/static/js/runtime~main.229c360f.js
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 200 packages in 1.153s
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: 89.6M
remote: -----> Launching...
remote:        Released v7
remote:        https://chuanshuoge-bank-react-express.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/chuanshuoge-bank-react-express.git
   ffa2c91..209f49a  master -> master

----------------------------------------
reference:
https://chuanshuoge2.blogspot.com/2018/12/bank-react-redux-express-heroku.html

No comments:

Post a Comment