├── .DS_Store ├── .gitignore ├── Procfile ├── README.md ├── app.js ├── controllers ├── allControllers.js ├── authController.js └── helper.js ├── middleware └── authMiddleware.js ├── models ├── Balance.js ├── User.js └── Webhook.js ├── package-lock.json ├── package.json ├── public ├── smoothie.png └── styles.css ├── routes └── authRoutes.js └── views ├── alltransactions.ejs ├── balances (copy).ejs ├── balances.ejs ├── dashboard.ejs ├── home.ejs ├── index.html ├── login.ejs ├── monoreauth.ejs ├── partials ├── footer.ejs ├── header.ejs ├── mono_dialog.ejs ├── mono_reauth_dialog.ejs ├── sidebar.ejs └── sidebar_end.ejs ├── signup.ejs └── transactions.ejs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withmono/mono-data-sync-demo/719be4ff125b740790fec6dbab7156a9b03cfc01/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mono-Connect API Implementation 2 | 3 | ## Quick Links 4 | 5 | [1. Overview](#1-overview) 6 | 7 | [2. Implementation](#2-implementation) 8 | 9 | [3. Installation](#3-installation) 10 | 11 | 12 | ## 1. Overview 13 | 14 | Project #sweet-loans [(link)](https://sweet-loans.herokuapp.com/) is a simple web application that allows its users to connect their financial account, see their information, transactions, balances and also fetch real time data that happens on their financial account. 15 | It is built with NodeJS Express, which basically implements the core features of the Mono-Connect [API](https://docs.mono.co/reference). 16 | 17 | ### Walkthrough
18 | 1. The web application has a basic authentication system, where a user can [Login](https://sweet-loans.herokuapp.com/login), [Signup](https://sweet-loans.herokuapp.com/signup) and Logout of the system.
19 | 2. Once signed in, a user is faced with a dashboard where he has to link his account through the mono widget.
20 | 3. On successful linkup, the page forces reload and fetches all the user's connected information right on the dashboard.
21 | 4. Also from the side navigation, a user can view his account balance, his recent transaction history, and then all transaction histories with pagination.
22 | 5. Lastly, you can force refresh by Data syncing manually on the Balances page.
23 | 24 | ## 2. Implementation 25 | 1. Firstly, the application has Mono's widget [embedded](https://github.com/kingkenway/mono/blob/master/views/partials/mono_dialog.ejs#L1), for users to connect their bank account. Once successful, the application retrieves a code sent by Mono.
26 | 27 | 2. After user has his/her account connected successfully, his Mono ID. is needed which leads to the application making a request with the provided code in 1, to Mono's Authentication Endpoint -> https://api.withmono.com/account/auth through POST Method [here](https://github.com/kingkenway/mono/blob/master/controllers/allControllers.js#L32)
28 | 29 | 3. Once the user's Mono ID. is fetched and stored in the db, his connected user information is immediately fetched and loaded on the dashboard through Mono's API Identity Endpoint -> https://api.withmono.com/accounts/id/identity through GET Method right [here](https://github.com/kingkenway/mono/blob/master/controllers/allControllers.js#L8)
30 | 31 | 4. The user can view his account balance through Mono's API Information Endpoint -> https://api.withmono.com/accounts/id through GET Method right [here](https://github.com/kingkenway/mono/blob/master/controllers/allControllers.js#L69)
32 | 33 | 5. The user views his recent (last 20) transactions, through Mono's API transaction Endpoint -> https://api.withmono.com/accounts/id/transaction through GET Method right [here](https://github.com/kingkenway/mono/blob/master/controllers/allControllers.js#L92)
34 | 35 | 6. Also, all transaction history with pagination is viewed, through Mono's API transaction Endpoint -> https://api.withmono.com/accounts/id/transaction?page=1 through GET Method right [here](https://github.com/kingkenway/mono/blob/master/controllers/allControllers.js#L121)
36 | 37 | 7. Lastly, you can force refresh by Data syncing manually, which can be triggered with the button displayed on the Balances page. 38 | 39 | You can register [here](https://sweet-loans.herokuapp.com/signup) to give this application a shot. 40 | 41 | ## 3. Installation 42 | 43 | The first thing to do is to clone the repository: 44 | 45 | ```sh 46 | $ git clone https://github.com/kingkenway/mono.git 47 | $ cd mono 48 | ``` 49 | 50 | ## Local Environment Variables 51 | Ensure you have your .env file created in the root directory, with the following parameters provided: 52 | 53 | DATABASE_URL='Your Mongo DB URL' 54 | MONO_SECRET_KEY='Your Mono Secret Key on your dashboard' 55 | MONO_PUBLIC_KEY='Your Mono Public Key on your dashboard' 56 | MONO_WEBHOOK_SEC='Your Mono Webhook Secret Key on your dashboard' 57 | TOKEN='A random key identifier for JWT Verification' 58 | 59 | ## Project setup 60 | ``` 61 | npm install 62 | ``` 63 | 64 | ### Compiles and hot-reloads for development 65 | ```javascript 66 | npm start 67 | ``` -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const mongoose = require('mongoose'); 3 | const cookieParser = require('cookie-parser'); 4 | const authRoutes = require('./routes/authRoutes'); 5 | const { requireAuth, checkUser, verifyWebhook, requireMonoReauthToken } = require('./middleware/authMiddleware'); 6 | const controllers = require('./controllers/allControllers'); 7 | const moment = require('moment'); 8 | 9 | const port = process.env.PORT || 8000 10 | 11 | require('dotenv').config(); 12 | 13 | const app = express(); 14 | 15 | app.locals.getPage = function(page) { 16 | const page_number = page.split("page=")[1]; 17 | return `?page=${page_number}` 18 | } 19 | 20 | app.locals.setCurrency = function(amount) { 21 | let res = parseFloat(amount)*0.01 // Convert to naira from kobo 22 | return res.toLocaleString() 23 | } 24 | 25 | app.locals.formatTime = function(time) { 26 | return moment(time).format("DD-MM-YYYY h:mm:ss"); 27 | } 28 | 29 | // middleware 30 | app.use(express.static('public')); 31 | app.use(express.json()); 32 | app.use(cookieParser()); 33 | 34 | // view engine 35 | app.set('view engine', 'ejs'); 36 | 37 | // database connection 38 | const dbURI = process.env['DATABASE_URL']; 39 | mongoose.connect(dbURI, { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex:true }) 40 | .then((result) => { 41 | console.log(`Launched @ port ${port}`); 42 | app.listen(port); 43 | }) 44 | .catch((err) => console.log(err)); 45 | 46 | // routes 47 | app.get('*', checkUser); 48 | app.get('/', (req, res) => res.render('home')); 49 | 50 | app.get('/dashboard', requireAuth, requireMonoReauthToken, controllers.dashboard, (req, res) => res.render('dashboard')); 51 | 52 | app.post('/dashboard', controllers.dashboardPost); 53 | 54 | app.get('/manualsync', controllers.manualSync); 55 | 56 | app.get('/balances', requireAuth, requireMonoReauthToken, controllers.balances, (req, res) => res.render('balances')); 57 | 58 | app.get('/transactions', requireAuth, requireMonoReauthToken, controllers.transactions, (req, res) => res.render('transactions')); 59 | 60 | app.get('/alltransactions', requireAuth, requireMonoReauthToken, controllers.alltransactions, (req, res) => res.render('alltransactions')); 61 | 62 | app.get('/monoReauth', requireAuth, (req, res) => res.render('monoreauth')); 63 | 64 | app.post('/monoReauth', controllers.monoReauth); 65 | 66 | app.post('/webhook', verifyWebhook, controllers.webhook); 67 | 68 | 69 | // app.get('/force-refresh', requireAuth, (req, res) => res.render('smoothies')); 70 | app.use(authRoutes); -------------------------------------------------------------------------------- /controllers/allControllers.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const User = require('../models/User'); 3 | const Balance = require('../models/Balance'); 4 | const WebH = require('../models/Webhook'); 5 | const {isDataAvailable} = require('../controllers/helper'); 6 | 7 | module.exports.dashboard = async (req,res, next) => { 8 | 9 | if(res.locals.data.user.monoId){ 10 | 11 | const url = `https://api.withmono.com/accounts/${res.locals.data.user.monoId}/identity` 12 | 13 | const response = await fetch(url, { 14 | method: 'GET', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 18 | } 19 | }); 20 | 21 | const data = await response.json(); 22 | res.locals.dashboard = data; 23 | 24 | next(); 25 | }else{ 26 | next(); 27 | } 28 | 29 | } 30 | 31 | module.exports.dashboardPost = async (req,res, next) => { 32 | // Retrieve code and user id from front end 33 | const { code, id } = req.body; 34 | 35 | url = "https://api.withmono.com/account/auth"; 36 | 37 | if(code){ 38 | // Retrieve mono id from front end 39 | const response = await fetch(url, { 40 | method: 'POST', 41 | body: JSON.stringify({ code }), 42 | headers: { 43 | 'Content-Type': 'application/json', 44 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 45 | } 46 | }).then(res_ => res_.json()) 47 | .then(function (res_) { 48 | 49 | const dispatch = { 50 | $set: { 51 | monoId: res_.id, 52 | monoCode: code, 53 | monoStatus: false 54 | } 55 | } 56 | 57 | // Update collection with mono id and code 58 | User.updateOne({_id: id}, dispatch, {new: true}, function(err, res) {}); 59 | // Create instance in our balance collection 60 | Balance({ monoId: res_.id }).save(); 61 | 62 | res.status(200).json('done') 63 | }) 64 | .catch(err => res.status(501).send("Error fetching id")); 65 | 66 | 67 | }else{ 68 | res.status(500).json({ error: "Error somewhere" }) 69 | } 70 | 71 | 72 | 73 | // next(); 74 | } 75 | 76 | 77 | module.exports.balances = async (req,res, next) => { 78 | if(res.locals.data.user.monoId){ 79 | const url = `https://api.withmono.com/accounts/${res.locals.data.user.monoId}` 80 | 81 | const response = await fetch(url, { 82 | method: 'GET', 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 86 | } 87 | }); 88 | 89 | const data = await response.json(); 90 | res.locals.balances = data; 91 | next(); 92 | } 93 | else{ 94 | res.locals.balances = "" 95 | next(); 96 | } 97 | } 98 | 99 | module.exports.transactions = async (req,res, next) => { 100 | if(res.locals.data.user.monoId){ 101 | 102 | if (await isDataAvailable(res.locals.data.user.monoId)) { 103 | 104 | const url = req.query.page || `https://api.withmono.com/accounts/${res.locals.data.user.monoId}/transactions` 105 | 106 | const response = await fetch(url, { 107 | method: 'GET', 108 | headers: { 109 | 'Content-Type': 'application/json', 110 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 111 | } 112 | }); 113 | 114 | const data = await response.json(); 115 | res.locals.transactions = data; 116 | next(); 117 | 118 | } 119 | 120 | res.locals.transactions = "PROCESSING"; 121 | next() 122 | 123 | } 124 | else{ 125 | res.locals.transactions = null; 126 | next(); 127 | } 128 | 129 | } 130 | 131 | module.exports.alltransactions = async (req,res, next) => { 132 | 133 | if(res.locals.data.user.monoId){ 134 | 135 | // Check if data is still processing 136 | if (await isDataAvailable(res.locals.data.user.monoId)) { 137 | 138 | let url = `https://api.withmono.com/accounts/${res.locals.data.user.monoId}/transactions` 139 | let page = req.query.page || 1 140 | let finalUrl = url + `?page=${page}` 141 | 142 | const response = await fetch(finalUrl, { 143 | method: 'GET', 144 | headers: { 145 | 'Content-Type': 'application/json', 146 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 147 | } 148 | }); 149 | 150 | const data = await response.json(); 151 | res.locals.transactions = data; 152 | next(); 153 | } 154 | 155 | res.locals.transactions = "PROCESSING"; 156 | next() 157 | 158 | } 159 | else{ 160 | res.locals.transactions = null; 161 | next(); 162 | } 163 | 164 | } 165 | 166 | module.exports.reauthorise = async function(id){ 167 | let url = `https://api.withmono.com/accounts/${id}/reauthorise` 168 | 169 | const response = await fetch(url, { 170 | method: 'POST', 171 | headers: { 172 | 'Content-Type': 'application/json', 173 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 174 | } 175 | }); 176 | 177 | const data = await response.json(); 178 | 179 | return data.token; 180 | } 181 | 182 | // NOTE 183 | // This feature is only available to select partners. Reach out to us on slack about your product feature and why this should be enabled for your business. 184 | 185 | // By default, all connected accounts are automatically refreshed once every 24 hours. 186 | // You can contact us at hi@mono.co if you want to change the update frequency to: 187 | 188 | // 6h, all connected accounts will be refreshed every 6h (4 times a day) 189 | // 12h, all connected accounts will be refreshed every 12h (2 times a day) 190 | 191 | module.exports.webhook = async (req,res, next) => { 192 | 193 | const webhook = req.body; 194 | 195 | if (webhook.event == "mono.events.account_updated") { 196 | await WebH.create({test: "updated"}); 197 | 198 | if (webhook.data.meta.data_status == "AVAILABLE") { // AVAILABLE, PROCESSING, FAILED 199 | 200 | const data = webhook.data.account; 201 | 202 | // You can update your records on success 203 | 204 | const query = { 205 | monoId: data._id 206 | }; 207 | 208 | const result = { 209 | $set: { 210 | monoId: data._id, 211 | institution: data.institution.name, // name:bankCode:type 212 | name: data.name, 213 | accountNumber: data.accountNumber, 214 | type: data.type, 215 | currency: data.currency, 216 | balance: data.balance, 217 | bvn: data.bvn 218 | } 219 | } 220 | 221 | await Balance.updateOne(query, result, {new: true}, function(err, res) {}); 222 | 223 | await WebH.create({test: "updated___available_id: "+data._id}); 224 | 225 | // webhook.data.account 226 | } 227 | else if (webhook.data.meta.data_status == "PROCESSING") { 228 | await WebH.create({test: "updated___processing"}); 229 | // Lol! Just chill and wait 230 | } 231 | } 232 | 233 | else if (webhook.event == "mono.events.reauthorisation_required") { 234 | // webhook.data.account._id 235 | 236 | // You can retrieve your token here for re-authentication 237 | // reauthorise(webhook.data.account._id) 238 | const query = { 239 | monoId: data._id 240 | }; 241 | 242 | const result = { 243 | $set: { 244 | monoStatus: true, 245 | } 246 | } 247 | 248 | await User.updateOne(query, result, {new: true}, function(err, res) {}); 249 | 250 | await WebH.create({test: "reauthorisation_required"}); 251 | 252 | } 253 | 254 | else if (webhook.event == "mono.events.account_reauthorized") { 255 | // webhook.data.account._id 256 | 257 | // Account Id. will be sent on successful reauthorisation. Nothing much to do here. 258 | await WebH.create({test: "account_reauthorized"}); 259 | 260 | } 261 | 262 | return res.sendStatus(200); 263 | 264 | } 265 | 266 | module.exports.manualSync = async (req,res, next) => { 267 | 268 | if(res.locals.data.user.monoId){ 269 | const url = `https://api.withmono.com/accounts/${res.locals.data.user.monoId}/sync` 270 | 271 | // console.log(123412345); 272 | 273 | const response = await fetch(url, { 274 | method: 'GET', 275 | headers: { 276 | 'Content-Type': 'application/json', 277 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 278 | } 279 | }); 280 | 281 | const data = await response.json(); 282 | 283 | console.log(data); 284 | // res.locals.dashboard = data; 285 | 286 | next(); 287 | }else{ 288 | next(); 289 | } 290 | 291 | } 292 | 293 | 294 | module.exports.monoReauth = async (req,res, next) => { 295 | 296 | const query = { 297 | monoId: req.body.id 298 | }; 299 | 300 | const result = { 301 | $set: { 302 | monoStatus: true, 303 | } 304 | } 305 | 306 | await User.updateOne(query, result, {new: true}, function(err, res) {}); 307 | 308 | res.status(201).json({status: "redirect"}); 309 | 310 | } -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const Balance = require('../models/Balance'); 3 | const jwt = require('jsonwebtoken'); 4 | 5 | // JWT and Cookie expiry 6 | const maxAge = 3*24*60*60; 7 | 8 | // handle errors 9 | const handleErrors = (err) => { 10 | let errors = { email: '', password: '' }; 11 | 12 | // handle login errors 13 | if (err.message === 'incorrect email') { 14 | errors.email ="This email is not registered" 15 | } 16 | 17 | if (err.message === 'incorrect password') { 18 | errors.password ="Wrong password" 19 | } 20 | 21 | // duplicate error code 22 | if (err.code === 11000) { 23 | errors.email = "Email is already taken."; 24 | return errors; 25 | } 26 | 27 | // validation errors 28 | if (err.message.includes('user validation failed')) { 29 | Object.values(err.errors).forEach(({properties}) => { 30 | errors[properties.path] = properties.message; 31 | }) 32 | } 33 | return errors; 34 | } 35 | 36 | const createToken = (id) =>{ 37 | return jwt.sign({ id }, process.env['TOKEN'], { 38 | expiresIn: maxAge 39 | }); 40 | } 41 | 42 | module.exports.signup_get = (req,res) => { 43 | res.render('signup'); 44 | } 45 | 46 | module.exports.login_get = (req,res) => { 47 | res.render('login'); 48 | } 49 | 50 | module.exports.signup_post = async (req,res) => { 51 | const { email, password } = req.body; 52 | 53 | try{ 54 | const user = await User.create({email, password}); 55 | const token = createToken(user._id); 56 | res.cookie('jwt', token, { httpOnly: true, maxAge: maxAge * 1000 }); 57 | // res.status(201).json(user); 58 | res.status(201).json({user: user._id}); 59 | } 60 | catch(err){ 61 | const errors = handleErrors(err); 62 | res.status(400).json({ errors }); 63 | } 64 | } 65 | 66 | module.exports.login_post = async (req,res) => { 67 | const { email, password } = req.body; 68 | 69 | try{ 70 | const user = await User.login(email,password); 71 | const token = createToken(user._id); 72 | res.cookie('jwt', token, { httpOnly: true, maxAge: maxAge * 1000 }); 73 | res.status(200).json({ user: user._id }) 74 | } 75 | catch (err){ 76 | const errors = handleErrors(err); 77 | res.status(400).json({ errors }); 78 | } 79 | } 80 | 81 | module.exports.logout_get = (req, res) => { 82 | res.cookie('jwt', '', { maxAge: 1 }); 83 | res.redirect('/'); 84 | } -------------------------------------------------------------------------------- /controllers/helper.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const isDataAvailable = async (id) => { 4 | const response = await fetch(`https://api.withmono.com/accounts/${id}`, { 5 | method: 'GET', 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | 'mono-sec-key': process.env['MONO_SECRET_KEY'] 9 | } 10 | }); 11 | const data = await response.json(); 12 | if (data.meta && data.meta.data_status == "AVAILABLE") { 13 | return true 14 | }return false 15 | } 16 | 17 | module.exports = { isDataAvailable }; -------------------------------------------------------------------------------- /middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('../models/User'); 3 | const Balance = require('../models/Balance'); 4 | const {reauthorise} = require('../controllers/allControllers'); 5 | 6 | const secret = process.env.MONO_WEBHOOK_SEC; 7 | 8 | const requireAuth = ( req, res, next) => { 9 | const token = req.cookies.jwt; 10 | 11 | // check if jwt exists & is verified 12 | if (token) { 13 | jwt.verify(token, process.env['TOKEN'], (err, decodedToken) => { 14 | if (err) { 15 | console.log(err.message); 16 | res.redirect('/login'); 17 | }else{ 18 | // console.log(decodedToken); 19 | next(); 20 | } 21 | }); 22 | }else{ 23 | res.redirect('/login'); 24 | } 25 | } 26 | 27 | const checkUser = (req, res, next) => { 28 | const token = req.cookies.jwt; 29 | 30 | if (token) { 31 | jwt.verify(token, process.env['TOKEN'], async (err, decodedToken) => { 32 | if (err) { 33 | console.log(err.message); 34 | res.locals.data = null; 35 | res.locals.reauth = null; 36 | next(); 37 | }else{ 38 | let monoBalance = "" 39 | let user = await User.findById(decodedToken.id) 40 | 41 | if (user && user['monoId']){ 42 | monoBalance = await Balance.findOne({ monoId:user.monoId }) 43 | } 44 | 45 | res.locals.reauth = null; 46 | res.locals.data = { 47 | user, 48 | monoBalance, 49 | publicKey: process.env['MONO_PUBLIC_KEY'], 50 | secretKey: process.env['MONO_SECRET_KEY'] 51 | } 52 | 53 | next(); 54 | } 55 | }); 56 | }else{ 57 | res.locals.data = null; 58 | res.locals.reauth = null; 59 | next(); 60 | } 61 | } 62 | 63 | const verifyWebhook = (req, res, next) => { 64 | if (req.headers['mono-webhook-secret'] !== secret) { 65 | return res.status(401).json({ 66 | message: "Unauthorized request." 67 | }); 68 | } 69 | 70 | next(); 71 | } 72 | 73 | const requireMonoReauthToken = async ( req, res, next) => { 74 | if (res.locals.data.user.monoStatus) { 75 | const reauthoriseToken = await reauthorise(res.locals.data.user.monoId) 76 | 77 | const query = { 78 | monoId: res.locals.data.user.monoId 79 | }; 80 | 81 | const result = { 82 | $set: { 83 | monoReauthToken: reauthoriseToken, 84 | } 85 | } 86 | 87 | await User.updateOne(query, result, {new: true}, function(err, res) {}); 88 | 89 | // res.locals.reauth = reauthoriseToken 90 | res.redirect('/monoReauth'); 91 | }else{ 92 | next(); 93 | } 94 | } 95 | 96 | module.exports = { requireAuth, checkUser, verifyWebhook, requireMonoReauthToken }; -------------------------------------------------------------------------------- /models/Balance.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // Create Schema 4 | const balanceSchema = new mongoose.Schema({ 5 | monoId: { 6 | type: String, 7 | default: '' 8 | }, 9 | institution: { 10 | type: String, 11 | default: '' 12 | }, 13 | name: { 14 | type: String, 15 | default: '' 16 | }, 17 | accountNumber: { 18 | type: String, 19 | default: '' 20 | }, 21 | type: { 22 | type: String, 23 | default: '' 24 | }, 25 | currency: { 26 | type: String, 27 | default: '' 28 | }, 29 | balance: { 30 | type: String, 31 | default: '' 32 | }, 33 | bvn: { 34 | type: String, 35 | default: '' 36 | }, 37 | }, 38 | { timestamps: true } 39 | ); 40 | 41 | const Balance = mongoose.model('balance', balanceSchema); 42 | 43 | module.exports = Balance; -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcrypt'); 3 | const { isEmail } = require('validator'); 4 | 5 | 6 | // Create Schema 7 | const userSchema = new mongoose.Schema({ 8 | email: { 9 | type: String, 10 | required: [true, 'Please enter an email'], 11 | unique: true, 12 | lowercase: true, 13 | // validate: [(val) => {}, 'Please enter a valid email'] 14 | validate: [isEmail, 'Please enter a valid email'] 15 | }, 16 | password: { 17 | type: String, 18 | required: [true, 'Please enter a password'], 19 | minlength: [6, 'Minimum password length is 6 characters'] 20 | }, 21 | monoId: { 22 | type: String, 23 | default: '' 24 | }, 25 | monoCode: { 26 | type: String, 27 | default: '' 28 | }, 29 | monoStatus: { 30 | type: Boolean, 31 | default: false 32 | }, 33 | monoReauthToken: { 34 | type: String, 35 | default: '' 36 | }, 37 | }); 38 | 39 | 40 | // fire a function after doc saved to db 41 | // userSchema.post('save', function(doc, next) { 42 | // console.log('new user was C & S', doc); 43 | // next(); 44 | // }); 45 | 46 | // fire a function after doc saved to db 47 | userSchema.pre('save', async function(next) { 48 | const salt = await bcrypt.genSalt(); 49 | this.password = await bcrypt.hash(this.password, salt); 50 | // console.log('user about to be created', this); 51 | next(); 52 | }); 53 | 54 | // Static method to login user 55 | userSchema.statics.login = async function(email, password) { 56 | const user = await this.findOne({ email }); 57 | if (user) { 58 | const auth = await bcrypt.compare(password, user.password); 59 | if (auth) { 60 | return user; 61 | } 62 | throw Error('incorrect password'); 63 | } 64 | throw Error('incorrect email'); 65 | } 66 | 67 | 68 | const User = mongoose.model('user', userSchema); 69 | 70 | module.exports = User; -------------------------------------------------------------------------------- /models/Webhook.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | // Create Schema 4 | const webhookSchema = new mongoose.Schema({ 5 | test: { 6 | type: String, 7 | default: '' 8 | }, 9 | createdAt: { 10 | type: Date, 11 | default: Date.now 12 | } 13 | }); 14 | 15 | const Webhook = mongoose.model('webhook', webhookSchema); 16 | 17 | module.exports = Webhook; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mono", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon app.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcrypt": "^5.0.0", 14 | "body-parser": "^1.19.0", 15 | "cookie-parser": "^1.4.5", 16 | "csurf": "^1.11.0", 17 | "dotenv": "^8.2.0", 18 | "ejs": "^3.1.5", 19 | "express": "^4.17.1", 20 | "jsonwebtoken": "^8.5.1", 21 | "moment": "^2.29.1", 22 | "mongoose": "^5.10.15", 23 | "node-fetch": "^2.6.1", 24 | "nodemon": "^2.0.6", 25 | "validator": "^13.1.17" 26 | }, 27 | "engines": { 28 | "node": "12.16.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /public/smoothie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/withmono/mono-data-sync-demo/719be4ff125b740790fec6dbab7156a9b03cfc01/public/smoothie.png -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | /* google fonts */ 2 | @import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300;400;500;600;700&display=swap'); 3 | 4 | body{ 5 | margin: 20px 40px; 6 | font-size: 1.2rem; 7 | letter-spacing: 1px; 8 | background: #fafafa; 9 | } 10 | h1, h2, h3, h4, ul, li, a, p, input, label, button, div, footer{ 11 | margin: 0; 12 | padding: 0; 13 | font-family: 'Quicksand', sans-serif; 14 | color: #444; 15 | } 16 | ul{ 17 | list-style-type: none; 18 | } 19 | a{ 20 | text-decoration: none; 21 | } 22 | nav{ 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: flex-end; 26 | margin-bottom: 40px; 27 | } 28 | nav ul{ 29 | display: flex; 30 | align-items: center; 31 | } 32 | nav li{ 33 | margin-left: 20px; 34 | } 35 | nav li a{ 36 | text-transform: uppercase; 37 | font-weight: 700; 38 | font-size: 0.8em; 39 | display: block; 40 | padding: 10px 16px; 41 | letter-spacing: 2px; 42 | } 43 | .btn{ 44 | border-radius: 36px; 45 | background: #FEE996; 46 | } 47 | form h2{ 48 | font-size: 2.4em; 49 | font-weight: 900; 50 | margin-bottom: 40px; 51 | } 52 | form{ 53 | width: 360px; 54 | margin: 0 auto; 55 | padding: 30px; 56 | box-shadow: 1px 2px 3px rgba(0,0,0,0.1); 57 | border-radius: 10px; 58 | background: white; 59 | } 60 | input{ 61 | padding: 10px 12px; 62 | border-radius: 4px; 63 | border: 1px solid #ddd; 64 | font-size: 1em; 65 | width: 100%; 66 | } 67 | label{ 68 | display: block; 69 | margin: 20px 0 10px; 70 | } 71 | button{ 72 | margin-top: 30px; 73 | border-radius: 36px; 74 | background: #FEE996; 75 | border:0; 76 | text-transform: uppercase; 77 | font-weight: 700; 78 | font-size: 0.8em; 79 | display: block; 80 | padding: 10px 16px; 81 | letter-spacing: 2px; 82 | } 83 | .error{ 84 | color: #ff0099; 85 | margin: 10px 2px; 86 | font-size: 0.8em; 87 | font-weight: bold; 88 | } 89 | header{ 90 | display: flex; 91 | align-items: center; 92 | } 93 | header img{ 94 | width: 250px; 95 | margin-right: 40px; 96 | } 97 | header h2{ 98 | font-size: 3em; 99 | margin-bottom: 10px; 100 | } 101 | header h3{ 102 | font-size: 1.6em; 103 | margin-bottom: 10px; 104 | margin-left: 2px; 105 | color: #999; 106 | } 107 | header .btn{ 108 | margin-top: 20px; 109 | padding: 12px 18px; 110 | text-transform: uppercase; 111 | font-weight: bold; 112 | display: inline-block; 113 | font-size: 0.8em; 114 | } 115 | .recipes{ 116 | display: grid; 117 | grid-template-columns: 1fr 1fr 1fr; 118 | column-gap: 30px; 119 | row-gap: 80px; 120 | margin: 80PX AUTO; 121 | max-width: 1200px; 122 | } 123 | .recipe{ 124 | display: inline-block; 125 | border-radius: 20px; 126 | background: white; 127 | position: relative; 128 | text-align: center; 129 | padding: 0 20px 30px 20px 130 | } 131 | .recipe img{ 132 | width: 100px; 133 | margin: -30px auto 20px; 134 | } 135 | footer{ 136 | text-align: center; 137 | margin-top: 120px; 138 | color: #aaa; 139 | } 140 | 141 | .inp > span { 142 | font-weight: 100; 143 | } 144 | 145 | .inp{ 146 | font-weight: 800; 147 | margin: 20px 0 0 0; 148 | } 149 | 150 | .sp{ 151 | max-width:100%; 152 | white-space:nowrap; 153 | } -------------------------------------------------------------------------------- /routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const {Router} = require('express'); 2 | const authController = require('../controllers/authController'); 3 | const router = Router(); 4 | 5 | router.get('/signup', authController.signup_get); 6 | router.post('/signup', authController.signup_post); 7 | router.get('/login', authController.login_get); 8 | router.post('/login', authController.login_post); 9 | router.get('/logout', authController.logout_get); 10 | 11 | module.exports = router; -------------------------------------------------------------------------------- /views/alltransactions.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | <%- include('partials/sidebar'); -%> 3 | 4 |
5 | Your Transactions 6 |
7 | 8 | <% if ( transactions && transactions == "PROCESSING" ) { %> 9 | 10 |
11 | Transaction data is still processing. 12 |
13 | 14 | <% } else if ( data['user']['monoId'] ) { %> 15 | 16 |

17 | All Transactions 18 |
19 |

20 | 21 |
22 |
23 |
24 | <% if (transactions.paging.previous) { %> 25 | « 26 | <% } %> 27 | 28 | Page <%= transactions.paging.page %> 29 | 30 | <% if (transactions.paging.next) { %> 31 | » 32 | <% } %> 33 | 34 |
35 |
36 | 37 |
38 | Page <%= transactions.paging.page %> of <%= Math.floor(transactions.paging.total/20) %> 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | <% for(var i=0; i < transactions.data.length; i++) { %> 52 | 53 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | <% } %> 63 | 64 |
S/NTypeDateAmountBalanceNarration
54 | <%= (i + 1) + ( transactions.paging.page * 20) -20 %> 55 | <%= transactions.data[i].type %><%= formatTime(transactions.data[i].date) %>NGN <%= setCurrency(transactions.data[i].amount) %>NGN <%= setCurrency(transactions.data[i].balance) %><%= transactions.data[i].narration %>
65 | 66 | 67 |
68 |
69 | <% if (transactions.paging.previous) { %> 70 | « 71 | <% } %> 72 | 73 | Page <%= transactions.paging.page %> 74 | 75 | <% if (transactions.paging.next) { %> 76 | » 77 | <% } %> 78 | 79 |
80 |
81 | 82 |
83 | 84 | 85 | <% } else { %> 86 |
87 | Your account is not linked yet 88 |
89 | <% } %> 90 | 91 | 92 |
93 | 94 | <%- include('partials/sidebar_end'); -%> 95 | 96 | 103 | 104 | <%- include('partials/footer'); -%> -------------------------------------------------------------------------------- /views/balances (copy).ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | <%- include('partials/sidebar'); -%> 3 | 4 |
5 | Account Balances 6 |
7 | 8 | <% if (mono['data']) { %> 9 | 10 |

11 | Your Account Balances 12 |
13 |

14 | 15 |

16 | NAME: 17 |

18 | 19 |

20 | BVN: 21 |

22 | 23 |

24 | ACCOUNT NUMBER: 25 |

26 | 27 |

28 | CURRENCY: 29 |

30 | 31 |

32 | BALANCE: 33 |

34 | 35 |

36 | TYPE: 37 |

38 | 39 | 40 | 41 | <% } else { %> 42 |
43 | Your account is not linked yet 44 |
45 | <% } %> 46 | 47 | 48 |
49 | 50 | <%- include('partials/sidebar_end'); -%> 51 | 52 | <% if (mono['data']) { %> 53 | 89 | <% } %> 90 | 91 | <%- include('partials/footer'); -%> -------------------------------------------------------------------------------- /views/balances.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | <%- include('partials/sidebar'); -%> 3 | 4 |
5 | Account Balances 6 | 7 | <% if ( data['user']['monoId'] ) { %> 8 | Refresh 9 | <% } %> 10 |
11 | 12 |

13 | Your Account Balance 14 |
15 |

16 | 17 | <% if ( data['user']['monoBalance'] && data['user']['monoId'] ) { %> 18 | 19 |

20 | NAME: 21 | <%= data['monoBalance']['name'] %> 22 |

23 | 24 |

25 | BVN: 26 | <%= data['monoBalance']['bvn'] %> 27 |

28 | 29 |

30 | ACCOUNT NUMBER: 31 | <%= data['monoBalance']['accountNumber'] %> 32 |

33 | 34 |

35 | CURRENCY: 36 | <%= data['monoBalance']['currency'] %> 37 |

38 | 39 |

40 | BALANCE: 41 | <%= setCurrency(data['monoBalance']['balance']) %> 42 |

43 | 44 |

45 | TYPE: 46 | <%= data['monoBalance']['type'] %> 47 |

48 | 49 | 50 | 51 | <% } else if (balances) { %> 52 | 53 |

54 | NAME: 55 | <%= balances['account']['name'] %> 56 |

57 | 58 |

59 | BVN: 60 | <%= balances['account']['bvn'] %> 61 |

62 | 63 |

64 | ACCOUNT NUMBER: 65 | <%= balances['account']['accountNumber'] %> 66 |

67 | 68 |

69 | CURRENCY: 70 | <%= balances['account']['currency'] %> 71 |

72 | 73 |

74 | BALANCE: 75 | <%= setCurrency(balances['account']['balance']) %> 76 |

77 | 78 |

79 | TYPE: 80 | <%= balances['account']['type'] %> 81 |

82 | 83 | 84 | <% } else { %> 85 |
86 | Your account is not linked yet 87 |
88 | <% } %> 89 | 90 | 91 |
92 | 93 | 138 | 139 | <%- include('partials/sidebar_end'); -%> 140 | 141 | <%- include('partials/footer'); -%> -------------------------------------------------------------------------------- /views/dashboard.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | <%- include('partials/sidebar'); -%> 3 | 4 | 5 |
6 | Your Dashboard 7 |
8 | 9 | <% if (data['user']['monoId'] ) { %> 10 | 11 |

12 | Your User Information 13 | 14 | ACCOUNT CONNECTED 15 | 16 |
17 |

18 | 19 |

20 | TITLE: 21 | <%= dashboard['title'] %> 22 |

23 | 24 |

25 | FIRST NAME: 26 | <%= dashboard['firstName'] %> 27 |

28 | 29 |

30 | MIDDLE NAME: 31 | <%= dashboard['middleName'] %> 32 |

33 | 34 |

35 | LAST NAME: 36 | <%= dashboard['lastName'] %> 37 |

38 | 39 |

40 | PHONE NUMBER 1: 41 | <%= dashboard['phoneNumber1'] %> 42 |

43 | 44 |

45 | PHONE NUMBER 2: 46 | <%= dashboard['phoneNumber2'] %> 47 |

48 | 49 |

50 | EMAIL: 51 | <%= dashboard['email'] %> 52 |

53 | 54 |

55 | GENDER: 56 | <%= dashboard['gender'] %> 57 |

58 | 59 |

60 | LGA OF ORIGIN: 61 | <%= dashboard['lgaOfOrigin'] %> 62 |

63 | 64 |

65 | LGA OF RESIDENCE: 66 | <%= dashboard['lgaOfResidence'] %> 67 |

68 | 69 |

70 | MARITAL STATUS: 71 | <%= dashboard['maritalStatus'] %> 72 |

73 | 74 |

75 | NIN: 76 | <%= dashboard['nin'] %> 77 |

78 | 79 |

80 | NATIONALITY: 81 | <%= dashboard['nationality'] %> 82 |

83 | 84 |

85 | RESIDENTIAL ADDRESS: 86 | <%= dashboard['residentialAddress'] %> 87 |

88 | 89 |

90 | STATE OF ORIGIN: 91 | <%= dashboard['stateOfOrigin'] %> 92 |

93 | 94 |

95 | STATE OF RESIDENCE: 96 | <%= dashboard['stateOfResidence'] %> 97 |

98 | 99 |

100 | WATCH LISTED: 101 | <%= dashboard['watchListed'] %> 102 |

103 | 104 |

105 | BVN: 106 | <%= dashboard['bvn'] %> 107 |

108 | 109 | 110 | 111 | 112 | <% } else { %> 113 |
114 | 115 |
116 | <% } %> 117 | 118 | 119 |
120 | 121 | 122 | <%- include('partials/sidebar_end'); -%> 123 | 124 | <%- include('partials/mono_dialog'); -%> 125 | 126 | 134 | 135 | <%- include('partials/footer'); -%> -------------------------------------------------------------------------------- /views/home.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | 3 | 4 |


5 | 6 |
7 |
8 | 9 |
10 |
11 |

Sweet Loans.

12 |

Get access to sweet loans, quickly in no time

13 | Get In! 14 |
15 |
16 | 17 | 18 | <%- include('partials/footer'); -%> -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mono Connect test 5 | 10 | 11 | 12 | 13 |
14 |

Welcome to Mono Connect.

15 | 16 |
17 | 58 | 59 | -------------------------------------------------------------------------------- /views/login.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | 3 |
4 |

Login

5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | <%- include('partials/footer'); -%> 17 | 18 | -------------------------------------------------------------------------------- /views/monoreauth.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | <%- include('partials/sidebar'); -%> 3 | 4 | 5 |
6 | Your Dashboard 7 |
8 | 9 | <% if (data['user']['monoStatus'] ) { %> 10 |
11 | 12 |
13 | <% } else { %> 14 |
15 | Nothing to do here. No pending re-authorisation! 16 |
17 | <% } %> 18 | 19 | 20 |
21 | 22 | 23 | <%- include('partials/sidebar_end'); -%> 24 | 25 | <%- include('partials/mono_reauth_dialog'); -%> 26 | 27 | 35 | 36 | <%- include('partials/footer'); -%> -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | S.w.e.e.t Loans 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /views/partials/mono_dialog.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/partials/mono_reauth_dialog.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /views/partials/sidebar.ejs: -------------------------------------------------------------------------------- 1 |
2 | Dashboard 3 | Balances 4 | Latest Transactions 5 | All Transactions 6 | Request Loan 7 | Soon 8 | 9 |
10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /views/partials/sidebar_end.ejs: -------------------------------------------------------------------------------- 1 |
2 |
-------------------------------------------------------------------------------- /views/signup.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 |
3 |

Sign up

4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | 58 | <%- include('partials/footer'); -%> -------------------------------------------------------------------------------- /views/transactions.ejs: -------------------------------------------------------------------------------- 1 | <%- include('partials/header'); -%> 2 | <%- include('partials/sidebar'); -%> 3 | 4 |
5 | Your Transactions 6 |
7 | 8 | <% if ( transactions && transactions == "PROCESSING" ) { %> 9 | 10 |
11 | Transaction data is still processing. 12 |
13 | 14 | <% } else if ( data['user']['monoId'] ) { %> 15 |

16 | Last 20 Transactions 17 |
18 |

19 | 20 |
21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | <% for(var i=0; i < transactions.data.length; i++) { %> 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | <% } %> 47 | 48 |
S/NTypeDateAmountBalanceNarration
<%= i+1 %><%= transactions.data[i].type %><%= formatTime(transactions.data[i].date) %>NGN <%= setCurrency(transactions.data[i].amount) %>NGN <%= setCurrency(transactions.data[i].balance) %><%= transactions.data[i].narration %>
49 | 50 | 51 | 58 |
59 | 60 | 61 | <% } else { %> 62 |
63 | Your account is not linked yet 64 |
65 | <% } %> 66 | 67 | 68 |
69 | 70 | <%- include('partials/sidebar_end'); -%> 71 | 72 | 79 | 80 | <%- include('partials/footer'); -%> --------------------------------------------------------------------------------