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>
-------------------------------------------------------------------
<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:
No comments:
Post a Comment