Skip to content
This repository has been archived by the owner on Jul 20, 2020. It is now read-only.

Commit

Permalink
feat(notification): users get notified of approvals
Browse files Browse the repository at this point in the history
added a table for notification
installed socket.io
added tests for the feature
[Finishes #170947562]
  • Loading branch information
Mnickii authored and Baraka-Mugisha committed Mar 11, 2020
1 parent 4bb6dee commit 731389a
Show file tree
Hide file tree
Showing 28 changed files with 384 additions and 137 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"sequelize": "^5.21.3",
"sinon": "^8.1.1",
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"swagger-jsdoc": "^3.5.0",
"swagger-ui-express": "^4.1.3",
"uuid": "^3.4.0"
Expand Down
Binary file added public/bareicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 40 additions & 6 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,16 @@
}
#notif-title {
background-color: #eee;
text-align: center;
font-size: 25px;
text-align: center;

}
.notifs {
color: rgb(13, 13, 46);
font-size: 20px;
list-style: none;
margin:auto;
text-align: center;
}
</style>
</head>
Expand All @@ -25,13 +33,39 @@
</div>
<script src="/socket.io/socket.io.js"></script>
<script>
import dotenv from 'dotenv';
dotenv.config();
const baseURL = process.env.BASE_URL
const socket = io(baseURL);
Notification.requestPermission();

const socket = io({
transportOptions: {
polling: {
extraHeaders: {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijc5NjYwZTZmLTRiN2QtNGcyMS04MXJlLTc0ZjU0ams5MWM4YSIsImlzVmVyaWZpZWQiOnRydWUsImVtYWlsIjoiamRldkBhbmRlbGEuY29tIiwicm9sZSI6InJlcXVlc3RlciIsImlhdCI6MTU4MzczNDEzNCwiZXhwIjoxNTgzODIwNTM0fQ.lB2g5G4xQB8cLqRPVbrJnZB8DoSCBeH8azYWWxjkx0A'
}
}
}
});
socket.on('initialize', (data) => {
const parsedData = JSON.parse(data);
let notifDiv = document.querySelector('.notifications');
notifDiv.innerHTML = '';
for(obj in parsedData.notif) {
let node = document.createElement('div');
node.innerHTML = `<a href = ${parsedData.notif[obj].link} target="_blank"><li class = "notifs">${parsedData.notif[obj].content}</li></a>`
notifDiv.appendChild(node)
}});

socket.on('notification', (data) => {
socket.emit('messageFromClient', 'This is from the client')
const parsedData = JSON.parse(data);
let notifDiv = document.querySelector('.notifications');
let node = document.createElement('div');
node.innerHTML = `<a href = ${parsedData.link} target="_blank"><li class = "notifs">${parsedData.content}</li></a>`
notifDiv.appendChild(node)
var notification = new Notification("Barefoot Nomad", {
body: parsedData.content,
icon: "./bareicon.png"
})
});

</script>
</body>
</html>
19 changes: 1 addition & 18 deletions src/controllers/notificationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,6 @@ import Response from '../utils/ResponseHandler';
* @class notificationController
*/
export default class notificationController {
/**
* @param {object} req
* @param {object} res
* @param {object} next
* @return {object} notification
*/
static async getAllNotifications(req, res) {
const { user } = req;
const notifications = await db.Notifications.findAll({
where: {
email: user.email
}
});
const allNotifs = notifications.reverse();
return Response.success(res, 200, 'success', allNotifs);
}

/**
* @param {object} req
* @param {object} res
Expand All @@ -32,7 +15,7 @@ export default class notificationController {
static async markAllNotificationsAsRead(req, res) {
try {
const { user } = req;
await db.Notifications.update({ status: 'read' }, { where: { recieverId: user.id } });
await db.Notifications.update({ status: 'read' }, { where: { receiverId: user.id } });
return Response.success(res, 200, res.__('all unread notifications marked as read'));
} catch (error) {
return Response.errorResponse(res, 500, res.__('server error'));
Expand Down
17 changes: 16 additions & 1 deletion src/controllers/tripsController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import db from '../models';
import Response from '../utils/ResponseHandler';
import TripsService from '../services/tripServices';
import stringHelper from '../utils/stringHelper';
import notifService from '../services/notificationService';
import SendNotification from '../utils/sendNotification';

/**
* @description RequestController Controller
Expand Down Expand Up @@ -37,6 +39,7 @@ export default class requestController {
id: uuid(),
type: 'one way',
managerId: user.managerId,
userId: user.id,
location,
destination,
reason,
Expand Down Expand Up @@ -255,6 +258,17 @@ export default class requestController {
return Response.errorResponse(res, 400, res.__('the request is already re-confirmed'));
}
const updatedRequest = await request.update({ confirm: true });

const result = await notifService.createNotif(updatedRequest.userId, updatedRequest.email, `the trip to ${updatedRequest.destination} on ${updatedRequest.departureDate} that you requested has been ${updatedRequest.status}`, '#');

const content = {
intro: `${req.__('the trip to')} ${updatedRequest.destination} ${req.__('on')} ${request.departureDate} ${req.__('that you requested has been')} ${req.__(request.status)}`,
instruction: req.__('To view this %s request you made click below', req.__(request.status)),
text: req.__('View request'),
signature: req.__('signature')
};
await SendNotification.SendNotif(result, req, content);

return Response.success(res, 200, res.__('request re-confirmed'), updatedRequest);
} catch (err) {
return Response.errorResponse(res, 500, res.__('server error'));
Expand Down Expand Up @@ -344,9 +358,10 @@ export default class requestController {
if (approvedRequest === stringHelper.approveRequestNotFound) {
return Response.errorResponse(res, 404, res.__(approvedRequest));
}

return Response.success(res, 200, res.__('request approved'), approvedRequest);
} catch (err) {
return Response.errorResponse(res, 200, res.__('server error'));
return Response.errorResponse(res, 500, res.__('server error'));
}
}
}
26 changes: 14 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import commentsRouter from './routes/commentsRoutes';
import facilitiesRouter from './routes/facilityRoute';
import notificationsRouter from './routes/notifications';
import './config/passport';
import { ioMiddleware } from './middlewares/io';

dotenv.config();
i18n.configure({
Expand Down Expand Up @@ -45,6 +46,19 @@ app.use('/public', express.static(path.join(__dirname, '../public')));

const port = process.env.PORT || 3000;

export const server = app.listen(port, () => process.stdout.write(`Server is running on http://localhost:${port}/api`));

const io = socketio(server);

io.use(async (socket, next) => {
ioMiddleware(socket);
next();
});

app.use((req, res, next) => {
req.io = io;
next();
});
app.use('/', welcome);
app.use('/api-doc', swagger);
app.use('/api/v1/auth', authRouter);
Expand All @@ -58,16 +72,4 @@ app.use((req, res) => res.status(404).send({ status: 404, error: res.__('Route %

app.use((err, req, res) => res.status(500).send({ status: 500, error: res.__('server error') }));


const server = app.listen(port, () => process.stdout.write(`Server is running on http://localhost:${port}/api`));

const io = socketio(server);
io.on('connection', (socket) => {
process.stdout.write('\nThere is connection ...........\n');
socket.emit('notification', { data: 'this is coming from a server' });
socket.on('messageFromClient', (data) => {
process.stdout.write(data);
});
});

export default app;
27 changes: 27 additions & 0 deletions src/middlewares/io.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import dotenv from 'dotenv';
import db from '../models';
import { verifyToken } from '../utils/tokenHandler';

dotenv.config();

export const connectedUsers = {};

export const ioMiddleware = async (socket) => {
const { token } = socket.handshake.headers;
const decoded = verifyToken(token);
if (!decoded.error) {
if (!connectedUsers[decoded.id]) {
connectedUsers[decoded.id] = [];
}
connectedUsers[decoded.id].push(socket.id);
socket.emit('initialize', JSON.stringify({ notif: await db.Notifications.findAll({ where: { receiverId: decoded.id } }) }));
socket.on('disconnect', () => {
process.stdout.write('a user is disconnected');
connectedUsers[decoded.id].forEach((el, index, arr) => {
if (arr[index] === socket.id) {
arr.splice(index, 1);
}
});
});
}
};
2 changes: 1 addition & 1 deletion src/middlewares/protectRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export default class protectRoutes {
static async checkUnreadNotifications(req, res, next) {
const { user } = req;
const unreadUsersNotifications = await db.Notifications.findAll({
where: { recieverId: user.id, status: 'unread' }
where: { receiverId: user.id, status: 'unread' }
});

if (unreadUsersNotifications.length === 0) {
Expand Down
9 changes: 7 additions & 2 deletions src/migrations/20200302132135-create-notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@ module.exports = {
defaultValue: 'unread',
allowNull: false
},
recieverEmail: {
receiverId: {
type: Sequelize.STRING,
allowNull: false
},
recieverId: {
receiverEmail: {
type: Sequelize.STRING,
allowNull: false
},
content: {
type: Sequelize.STRING,
allowNull: false
},
link: {
type: Sequelize.STRING,
allowNull: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
Expand Down
12 changes: 9 additions & 3 deletions src/models/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ module.exports = (sequelize, DataTypes) => {
const Notifications = sequelize.define('Notifications', {
status: DataTypes.ENUM('read, unread'),
content: DataTypes.STRING,
recieverEmail: DataTypes.STRING,
recieverId: DataTypes.STRING
receiverId: DataTypes.STRING,
receiverEmail: DataTypes.STRING,
link: DataTypes.STRING
}, {});
Notifications.associate = () => {
Notifications.associate = (models) => {
Notifications.belongsTo(models.User, {
foreignKey: 'receiverId',
as: 'user',
onDelete: 'CASCADE'
});
};
return Notifications;
};
1 change: 0 additions & 1 deletion src/routes/notifications.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import protectRoute from '../middlewares/protectRoute';

const router = express.Router();

router.get('/', protectRoute.verifyUser, validationResult, notificationController.getAllNotifications); // include validations too
router.patch('/all-read', protectRoute.verifyUser, protectRoute.checkUnreadNotifications, validationResult, notificationController.markAllNotificationsAsRead);

export default router;
1 change: 1 addition & 0 deletions src/seeders/20200222234112-requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ module.exports = {
reason: 'meeting with engineers write',
departureDate: '2020-10-01',
email: 'jdev@andela.com',
userId: '79660e6f-4b7d-4g21-81re-74f54jk91c8a',
status: 'open',
type: 'two way',
confirm: false,
Expand Down
23 changes: 15 additions & 8 deletions src/seeders/20200304092534-notifications.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
const dotenv = require('dotenv');

dotenv.config();

module.exports = {
up: (queryInterface) => queryInterface.bulkInsert(
'Notifications', [
{
id: '51j74d57-1910-4f50-9h15-b23740331od5',
status: 'unread',
recieverEmail: 'jdev@andela.com',
recieverId: '79660e6f-4b7d-4g21-81re-74f54jk91c8a',
receiverId: '79660e6f-4b7d-4g21-81re-74f54jk91c8a',
receiverEmail: 'jdev@andela.com',
content: 'Your trip to Gisenyi on 2020-02-01 has been approved',
link: `${process.env.BASE_URL}/api/v1/trips/t1e74db7-h610-4f50-9f45-e2371j331ld4`,
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '01q74d57-1995-h45h-592f-b23740331od5',
status: 'read',
recieverEmail: 'jdev@andela.com',
recieverId: '79660e6f-4b7d-4g21-81re-74f54jk91c8a',
content: 'You have been assign Jamie Jules as your manager',
receiverId: '79660e6f-4b7d-4g21-81re-74f54jk91c8a',
receiverEmail: 'jdev@andela.com',
link: `${process.env.BASE_URL}/api/v1/trips/t1e74db7-h610-4f50-9f45-e2371j331ld4`,
content: 'Your trip has been rejected',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: '91q74d57-1695-h40h-590f-b25b4063tod5',
status: 'unread',
recieverEmail: 'jeanne@andela.com',
recieverId: '79660e6f-4b7d-4g21-81re-74f54e9e1c8a',
content: 'You have been assign Jamie Jules as your manager',
receiverId: '79660e6f-4b7d-4g21-81re-74f54e9e1c8a',
receiverEmail: 'jeanne@andela.com',
link: `${process.env.BASE_URL}/api/v1/trips/t1e74db7-h610-4f50-9f45-e2371j331ld4`,
content: 'Your trip has been rejected',
createdAt: new Date(),
updatedAt: new Date(),
},],
Expand Down
Loading

0 comments on commit 731389a

Please sign in to comment.