├── .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 |
S/N | 44 |Type | 45 |Date | 46 |Amount | 47 |Balance | 48 |Narration | 49 |
---|---|---|---|---|---|
54 | <%= (i + 1) + ( transactions.paging.page * 20) -20 %> 55 | | 56 |<%= transactions.data[i].type %> | 57 |<%= formatTime(transactions.data[i].date) %> | 58 |NGN <%= setCurrency(transactions.data[i].amount) %> | 59 |NGN <%= setCurrency(transactions.data[i].balance) %> | 60 |<%= transactions.data[i].narration %> | 61 |
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 |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 |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 |Welcome to Mono Connect.
15 | 16 |