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