Skip to content

Commit

Permalink
feature(notifications): manager can get notification
Browse files Browse the repository at this point in the history
Manager can opt in to certain notification mode(email / in-app)
Manager can opt out to certain notification mode

[Finishes #169817550]
  • Loading branch information
victor-abz committed Jan 9, 2020
1 parent b204c16 commit 239733f
Show file tree
Hide file tree
Showing 43 changed files with 1,504 additions and 52 deletions.
Binary file modified .DS_Store
Binary file not shown.
333 changes: 333 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"nyc": {
"exclude": [
"src/database/*",
"src/models"
"src/models",
"src/helpers/eventEmmiters/emitter.js",
"src/tests"
],
"sourceMap": false,
"instrument": false
Expand Down Expand Up @@ -80,6 +82,9 @@
"sequelize-cli": "^5.5.1",
"sinon": "^8.0.1",
"sinon-chai": "^3.3.0",
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"socketio-jwt": "^4.5.0",
"swagger-jsdoc": "^3.4.0",
"swagger-ui-express": "^4.1.2",
"test-exclude": "^5.2.3"
Expand Down
4 changes: 4 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import express from 'express';
import morgan from 'morgan';
import passport from 'passport';
import routes from './routes/index';
import NotificationListeners from './helpers/notifications/index';

const app = express();
app.use(morgan('dev'));
Expand All @@ -11,6 +12,9 @@ app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(passport.initialize());

// running all event listeners
NotificationListeners();

app.use('/', routes);

export default app;
7 changes: 7 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import UserService from '../services/user.service';
import BcryptService from '../services/bcrypt.service';
import JwtService from '../services/jwt.service';
import SendEmailService from '../services/send-email.service';
import PreferenceService from '../services/preference.service';

/**
*
Expand Down Expand Up @@ -35,6 +36,12 @@ class AuthController {
const data = {
id, firstName, lastName, email, role, isVerified
};
// Set default notification
await PreferenceService.createPreference({
userId: id,
isEmailNotification: false,
isInAppNotification: true
});
SendEmailService.sendAccVerificationLink(email);
ResponseService.setSuccess(201, 'User created successfully, Visit Your Email To Activate Account', data);
return ResponseService.send(res);
Expand Down
46 changes: 46 additions & 0 deletions src/controllers/notification.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import ResponseService from '../services/response.service';
import PreferenceService from '../services/preference.service';
import NotificationService from '../services/notification.service';

/**
* @class NotificationController
* @description Handles notifications.
* */
class NotificationController {
/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* */
static async setNotificationMode(req, res) {
if (req.body.isInAppNotification) { // If request is isInAppNotification
await PreferenceService
.updatePreference(
{ userId: req.userData.id },
{ isInAppNotification: req.body.isInAppNotification }
);
ResponseService.setSuccess(200, 'Successfully updated your in-app notification preferences.');
return ResponseService.send(res);
}
// If request is isEmailNotification
await PreferenceService
.updatePreference(
{ userId: req.userData.id },
{ isEmailNotification: req.body.isEmailNotification }
);
ResponseService.setSuccess(200, 'Successfully updated your email notification preferences.');
return ResponseService.send(res);
}

/**
* @param {object} req request
* @param {object} res response
* @return {function} requests
*/
static async getAllNotifications(req, res) {
const data = await NotificationService.getAllNotifications({ userId: req.userData.id });
ResponseService.setSuccess(200, 'Your Notifications have been retrieved successfully', data);
return ResponseService.send(res);
}
}
export default NotificationController;
3 changes: 3 additions & 0 deletions src/helpers/eventEmmiters/emitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import EventEmmiter from 'events';

export default new EventEmmiter();
30 changes: 30 additions & 0 deletions src/helpers/eventEmmiters/socket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const io = require('socket.io')();
const socketioJwt = require('socketio-jwt');

const users = {};
const startSocket = (server) => {
io.attach(server);
let userName;

io.sockets
.on('connection', socketioJwt.authorize({ // Verify authorization for token
secret: process.env.SECRET_KEY,
timeout: 15000 // 15 seconds to send the authentication message
}))
.on('authenticated', (socket) => {
socket.emit('success', socket.decoded_token.email); // Emit success message for front-end
socket.on('new-user', (user) => { // save a new user for future needs
userName = user;
users[user] = socket;
io.sockets.emit('user-connected', `${user} has joined.`);
});
socket.on('private message', (msg) => {
const fromMsg = { from: userName, txt: msg.txt };
users[msg.to].emit('private message', fromMsg);
});
socket.on('disconnect', () => {
delete users[userName];
});
});
};
export { startSocket, io, users };
46 changes: 46 additions & 0 deletions src/helpers/mails/trip-notification.email.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const tripEmailBody = (managerNames, notification, unsubscribeUrl, data) => `<body style="margin: 0; padding: 0;">
<table border="0" cellpadding="0" cellspacing="0" width="900px" style="padding: 0 40px 0 40px; background-color:#f1f2f3;">
<tr><td align="center" style="background-color:#7041EE; margin: 0 50px 0 50px;">
<a><p style="color: #ffffff; font-family: Arial, sans-serif; font-size: 32px; line-height: 40px;">Barefoot Nomad<p></a></td>
</tr><tr><td align = "center" style="padding: 0 50px 0 50px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color:#ffffff; padding: 0 0 0 20px;">
<tr><td align="left" style="font-family: Arial, sans-serif; font-size: 24px; color: #050505;">
<p>Dear ${managerNames},</p></td></tr><tr>
<td align="left" style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 20px;">
<p>${notification}</p>
</td></tr>
<tr><td align="left" style="font-family: Arial, sans-serif; font-size: 18px; color: #050505;">
<p>With the following details:</p></td></tr>
<table style="font-family: arial, sans-serif; width: 70%;">
<tr style = "background-color: #dddddd;">
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight:bold;"> From:</td>
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px;"> ${data.departure}</td>
</tr>
<tr>
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight:bold;"> Destination:</td>
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px;"> ${data.destination}</td>
</tr>
<tr style = "background-color: #dddddd;">
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight:bold;"> Departure date:</td>
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px;"> ${data.travelDate}</td>
</tr>
<tr>
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight:bold;"> Travel reasons:</td>
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px;">${data.travelReasons}</td>
</tr>
<tr style = "background-color: #dddddd;">
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px; font-weight:bold;"> Accomodation:</td>
<td style = "border: 1px solid #dddddd; text-align: left; padding: 8px;"> ${data.accommodation}</td>
</tr>
</table>
<tr><td align="left" style="color: #153643; font-family: Arial, sans-serif; font-size: 16px; line-height: 20px;">
<p> You are receiving this email because you subscribed to receive email from Barefoot Nomad. </p>
<p> contact <a href=#>support@barefoot.com</a> if you feel this is wrong</p>
<p>Thank you for using Bare Foot Nomad</p>
<p>Don't want to receive such emails from barefootNomad? <a href="${unsubscribeUrl}">Unsubscribe from email notification</a></p>
</td></tr></table></tr><tr>
<td align="center" style="padding: 30px 30px 30px 30px;">&copy; BAREFOOT NOMAD, 2020<br/>
</td></tr></table></body>`;

export default tripEmailBody;
7 changes: 7 additions & 0 deletions src/helpers/notifications/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import TripNotification from '../../services/trip-notification.service';

const NotificationListeners = () => {
TripNotification.sendTripNotification();
};

export default NotificationListeners;
16 changes: 16 additions & 0 deletions src/helpers/transformErrorHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ResponseService from '../services/response.service';

const transformErrorHandler = (schema, body, res, next) => {
const { error } = schema.validate(body);

if (error) {
// const errors = [];
const { details } = error;
const errors = details.map(({ message }) => message);
ResponseService.setError(400, errors);
return ResponseService.send(res);
}
next();
};

export default transformErrorHandler;
8 changes: 8 additions & 0 deletions src/middlewares/auth.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ const authMiddleware = {
}
next();
},
checkIfUserHaveManager: async (req, res, next) => {
const { lineManagerId } = await UserService.findUserByProperty({ email: req.userData.email });
if (lineManagerId === null || lineManagerId === undefined) {
ResponseService.setError(401, 'You don\'t have manager assigned yet. Please contact Admin');
return ResponseService.send(res);
}
next();
},
checkUserLoggedIn: async (req, res, next) => {
const token = req.headers.authorization;

Expand Down
21 changes: 21 additions & 0 deletions src/middlewares/notification.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import PreferenceService from '../services/preference.service';
import ResponseService from '../services/response.service';

const checkIfOptionExists = async (req, res, next) => {
const prefs = await PreferenceService.findPreferences({ userId: req.userData.id });
const { dataValues } = prefs;

if (req.body.isInAppNotification
&& req.body.isInAppNotification.toString() === dataValues.isInAppNotification.toString()) {
ResponseService.setSuccess(200, 'This option has been already set for this notification mode');
return ResponseService.send(res);
}
if (req.body.isEmailNotification
&& req.body.isEmailNotification.toString() === dataValues.isEmailNotification.toString()) {
ResponseService.setSuccess(200, 'This option has been already set for this notification mode');
return ResponseService.send(res);
}
next();
};

export default checkIfOptionExists;
3 changes: 0 additions & 3 deletions src/migrations/20191218075931-create-users.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ export function up(queryInterface, Sequelize) {
type: Sequelize.BOOLEAN,
defaultValue: false
},
lineManagerId: {
type: Sequelize.INTEGER
},
token: {
allowNull: true,
type: Sequelize.STRING
Expand Down
35 changes: 35 additions & 0 deletions src/migrations/20191230093025-create-notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

const notificationMigrations = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Notifications', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER
},
message: {
type: Sequelize.TEXT
},
isRead: {
type: Sequelize.BOOLEAN,
defaultValue: false
},
requestId: {
type: Sequelize.INTEGER
},
type: Sequelize.STRING,
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: (queryInterface) => queryInterface.dropTable('Notifications')
};
export default notificationMigrations;
33 changes: 33 additions & 0 deletions src/migrations/20200103080032-create-preferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

const preferenceMigrations = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Preferences', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER
},
isEmailNotification: {
type: Sequelize.BOOLEAN,
defaultValue: 'false'
},
isInAppNotification: {
type: Sequelize.BOOLEAN,
defaultValue: 'true'
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: (queryInterface) => queryInterface.dropTable('Preferences')
};

export default preferenceMigrations;
20 changes: 20 additions & 0 deletions src/models/notification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default (sequelize, DataTypes) => {
const Notifications = sequelize.define('Notifications', {
userId: DataTypes.INTEGER,
message: {
type: DataTypes.STRING,
allowNull: false,
},
isRead: DataTypes.BOOLEAN,
type: DataTypes.BOOLEAN,
requestId: DataTypes.STRING
}, {});
Notifications.associate = (models) => {
// associations can be defined here
Notifications.belongsTo(models.Users, {
foreignKey: 'userId',
targetKey: 'id',
});
};
return Notifications;
};
15 changes: 15 additions & 0 deletions src/models/preferences.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default (sequelize, DataTypes) => {
const Preferences = sequelize.define('Preferences', {
userId: DataTypes.INTEGER,
isEmailNotification: DataTypes.STRING,
isInAppNotification: DataTypes.STRING
}, {});
Preferences.associate = (models) => {
// associations can be defined here
Preferences.belongsTo(models.Users, {
foreignKey: 'userId',
targetKey: 'id',
});
};
return Preferences;
};
5 changes: 5 additions & 0 deletions src/models/trip.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import emitter from '../helpers/eventEmmiters/emitter';

export default (sequelize, DataTypes) => {
const Trip = sequelize.define('Trip', {
tripType: DataTypes.STRING,
Expand All @@ -15,5 +17,8 @@ export default (sequelize, DataTypes) => {
Trip.belongsTo(models.Location, { foreignKey: 'originId', targetKey: 'id' });
Trip.belongsTo(models.Location, { foreignKey: 'destinationId', targetKey: 'id' });
};
Trip.afterCreate(({ dataValues }) => {
emitter.emit('request-created', dataValues);
});
return Trip;
};
Loading

0 comments on commit 239733f

Please sign in to comment.