├── nodemon.json.example ├── .idea ├── misc.xml ├── vcs.xml ├── modules.xml └── notifications.iml ├── Dockerfile ├── server.js ├── mongoose.js ├── firebase.js ├── api ├── routes │ ├── refreshToken.js │ ├── sms.js │ ├── subscribe.js │ └── notifications.js ├── models │ └── user.js └── controllers │ ├── refreshToken.js │ ├── subscribe.js │ ├── sms.js │ └── notifications.js ├── serviceAccountKey.json.example ├── docker-compose.yml ├── package.json ├── test ├── refreshToken.js ├── sms.js ├── subscribe.js └── notifications.js ├── app.js ├── README.md └── .gitignore /nodemon.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "MONGO_ATLAS_PW": "" 4 | } 5 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json package.json 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 3000 12 | 13 | RUN npm install -g nodemon 14 | 15 | CMD ["npm","start"] 16 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const app = require('./app'); 3 | const port = process.env.port || 3000; 4 | 5 | const server = http.createServer(app); 6 | 7 | server.listen(port, function () { 8 | console.log(`Running on ${port}`); 9 | }); -------------------------------------------------------------------------------- /mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.connect('mongodb+srv://mohamed1refaie:' + process.env.MONGO_ATLAS_PW + '@notifications-u3azq.mongodb.net/test?retryWrites=true', { 4 | useNewUrlParser: true 5 | }); 6 | 7 | module.exports = mongoose; -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /firebase.js: -------------------------------------------------------------------------------- 1 | let admin = require("firebase-admin"); 2 | 3 | let serviceAccount = require("./serviceAccountKey"); 4 | 5 | admin.initializeApp({ 6 | credential: admin.credential.cert(serviceAccount), 7 | databaseURL: "https://notificationsdemoapp-a1f6a.firebaseio.com" 8 | }); 9 | 10 | 11 | module.exports = admin; -------------------------------------------------------------------------------- /api/routes/refreshToken.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const RefreshTokenController = require('../controllers/refreshToken'); 5 | 6 | /* 7 | PUT Request that takes id and the new token for the user 8 | id: string ,required, 9 | token: string, required 10 | */ 11 | router.put('/', RefreshTokenController.refresh_user_token); 12 | 13 | 14 | module.exports = router; -------------------------------------------------------------------------------- /serviceAccountKey.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "", 4 | "private_key_id": "", 5 | "private_key": "", 6 | "client_email": "", 7 | "client_id": "", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://oauth2.googleapis.com/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "" 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | container_name: notification-service 5 | restart: always 6 | build: 7 | context: ./ 8 | args: 9 | port: "3000" 10 | ports: 11 | - "3000:3000" 12 | volumes: 13 | - .:/app 14 | - /app/node_modules 15 | links: 16 | - mongo 17 | mongo: 18 | container_name: mongo 19 | image: mongo 20 | ports: 21 | - "27017:27017" 22 | -------------------------------------------------------------------------------- /.idea/notifications.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /api/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | /* 4 | _id : mongoose primary key 5 | token : (the firebase cloud messaging -fcm- token) 6 | phoneNumber : the user's phone number 7 | userdId : the user's Id in the original Database (foreign key) 8 | email : the user's email 9 | 10 | */ 11 | const userSchema = mongoose.Schema({ 12 | _id: mongoose.Schema.Types.ObjectId, 13 | token: {type: String, required: true}, 14 | phoneNumber: {type: String, required: true}, 15 | userId: {type: String, required: true}, 16 | email: String 17 | }); 18 | 19 | module.exports = mongoose.model('User', userSchema); -------------------------------------------------------------------------------- /api/routes/sms.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const SMSController = require('../controllers/sms'); 4 | 5 | /* 6 | Post request that sends SMS to all users, 7 | It takes message in the request body. 8 | message : string, required 9 | */ 10 | router.post('/toAll', SMSController.send_sms_to_all); 11 | 12 | 13 | /* 14 | Post request that sends SMS to a specific user or users, 15 | It takes message and ids in the request body. 16 | message: string, required 17 | ids: string or array of strings, required 18 | */ 19 | router.post('/', SMSController.send_sms_to_specific); 20 | 21 | module.exports = router; -------------------------------------------------------------------------------- /api/routes/subscribe.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const subscribeController = require("../controllers/subscribe"); 5 | 6 | /* 7 | Post request that takes the user's info and saves 8 | the info. 9 | email : string,required, 10 | phoneNumber : string, required, 11 | token: (fcm token) string,required, 12 | userId: string,required 13 | */ 14 | router.post('/', subscribeController.subscribe); 15 | 16 | 17 | /* 18 | Post request that takes the topic of the subscription 19 | in the params and ids in the body 20 | ids: string or array of strings, required 21 | */ 22 | router.post('/:topic', subscribeController.subscribe_to_topic); 23 | 24 | 25 | module.exports = router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notifications", 3 | "version": "1.0.0", 4 | "description": "A node.js restful API for notifications", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --timeout 10000", 8 | "start": "nodemon server.js" 9 | }, 10 | "keywords": [ 11 | "node", 12 | "restful", 13 | "api" 14 | ], 15 | "author": "Mohamed Refaie", 16 | "license": "ISC", 17 | "dependencies": { 18 | "body-parser": "^1.19.0", 19 | "express": "^4.16.4", 20 | "firebase": "^5.11.0", 21 | "firebase-admin": "^7.3.0", 22 | "mongoose": "^5.5.5", 23 | "morgan": "^1.9.1" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^1.18.11", 27 | "chai": "^3.5.0", 28 | "chai-http": "^2.0.1", 29 | "mocha": "^2.4.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /api/controllers/refreshToken.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | 3 | 4 | /* 5 | refresh_user_token function updates the token of the user 6 | given the id and the new token. It extracts the id and token 7 | from the request body then updates the token of the associated user 8 | */ 9 | exports.refresh_user_token = (req, res, next) => { 10 | 11 | if (req.body.id === undefined || req.body.token === undefined || req.body.id === "" || req.body.token === "") { 12 | res.status(400).json({error: "id and token are required"}); 13 | } else { 14 | const id = req.body.id; 15 | const newToken = req.body.token; 16 | User.update({userId: id}, {$set: {token: newToken}}).exec().then(response => { 17 | res.status(200).json(response); 18 | }).catch(err => { 19 | res.status(500).json({error: err}); 20 | } 21 | ); 22 | } 23 | }; -------------------------------------------------------------------------------- /test/refreshToken.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const User = require('../api/models/user'); 4 | 5 | let chai = require('chai'); 6 | let chaiHttp = require('chai-http'); 7 | let app = require('../app'); 8 | let should = chai.should(); 9 | 10 | 11 | chai.use(chaiHttp); 12 | 13 | //Our parent block 14 | describe('Refresh Token', () => { 15 | 16 | /* 17 | * Test the /PUT refreshToken route 18 | */ 19 | describe('Update user token', () => { 20 | it('it should not update the user without a token field', (done) => { 21 | let data = { 22 | id: "73", 23 | }; 24 | chai.request(app) 25 | .put('/refreshToken') 26 | .send(data) 27 | .end((err, res) => { 28 | res.should.have.status(400); 29 | res.body.should.be.a('object'); 30 | res.body.should.have.property('error').eql("id and token are required"); 31 | done(); 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /api/routes/notifications.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const NotificationsController = require('../controllers/notifications'); 5 | 6 | 7 | /* 8 | Post request that send Notification for all users, 9 | It takes title, message and data in the request body. 10 | title: string, required, 11 | message: string, required, 12 | data: object, required 13 | */ 14 | router.post('/toAll', NotificationsController.send_notification_to_all); 15 | 16 | 17 | /* 18 | Post request that send Notification for a specific user or users 19 | It takes ids, title, message, and data in the request body. 20 | ids: string or array of strings, required, 21 | title: string, required, 22 | message: string, required, 23 | data: object, required 24 | */ 25 | router.post('/', NotificationsController.send_notification_to_specific); 26 | 27 | 28 | /* 29 | Post request that send Notification for a specific group with a common topic 30 | It takes topic, title, message, and data in the request body. 31 | topic: string, required, 32 | title: string, required, 33 | message: string, required, 34 | data: object, required 35 | */ 36 | router.post('/toGroup', NotificationsController.send_notification_to_group); 37 | 38 | module.exports = router; -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const morgan = require('morgan'); 4 | const bodyParser = require('body-parser'); 5 | const mongoose = require('./mongoose'); 6 | const notificationsRoutes = require('./api/routes/notifications'); 7 | const smsRoutes = require('./api/routes/sms'); 8 | const subscribeRoutes = require('./api/routes/subscribe'); 9 | const refreshTokenRoutes = require('./api/routes/refreshToken'); 10 | 11 | app.use(morgan('dev')); 12 | app.use(bodyParser.urlencoded({extended: false})); 13 | app.use(bodyParser.json()); 14 | 15 | app.use((req, res, next) => { 16 | res.header('Access-Control-Allow-Origin', '*'); 17 | res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); 18 | if (req.method === 'OPTIONS') { 19 | res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET'); 20 | return res.status(200).json({}); 21 | } 22 | next(); 23 | }); 24 | 25 | //Routes which handle the requests 26 | app.use('/notifications', notificationsRoutes); 27 | app.use('/sms', smsRoutes); 28 | app.use('/subscribe', subscribeRoutes); 29 | app.use('/refreshToken', refreshTokenRoutes); 30 | 31 | app.use((req, res, next) => { 32 | const error = new Error("not found"); 33 | error.status = 404; 34 | next(error); 35 | }); 36 | 37 | app.use((error, req, res, next) => { 38 | res.status(error.status || 500); 39 | res.json({ 40 | error: { 41 | message: error.message 42 | } 43 | }) 44 | }); 45 | 46 | module.exports = app; -------------------------------------------------------------------------------- /test/sms.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const User = require('../api/models/user'); 4 | 5 | let chai = require('chai'); 6 | let chaiHttp = require('chai-http'); 7 | let app = require('../app'); 8 | let should = chai.should(); 9 | 10 | 11 | chai.use(chaiHttp); 12 | 13 | //Our parent block 14 | describe('SMS', () => { 15 | 16 | /* 17 | * Test the /POST sms/toAll route 18 | */ 19 | describe('SMS to all users', () => { 20 | it('it should not send sms without a message field', (done) => { 21 | let sms = {}; 22 | chai.request(app) 23 | .post('/sms/toAll') 24 | .send(sms) 25 | .end((err, res) => { 26 | res.should.have.status(400); 27 | res.body.should.be.a('object'); 28 | res.body.should.have.property('error').eql("message is required"); 29 | done(); 30 | }); 31 | }); 32 | 33 | }); 34 | 35 | /* 36 | * Test the /POST sms/ route 37 | */ 38 | describe('SMS to a specific user or users', () => { 39 | it('it should not send sms without an ids field', (done) => { 40 | let sms = { 41 | message: "Dummy message" 42 | }; 43 | chai.request(app) 44 | .post('/sms') 45 | .send(sms) 46 | .end((err, res) => { 47 | res.should.have.status(400); 48 | res.body.should.be.a('object'); 49 | res.body.should.have.property('error').eql('message and ids are required'); 50 | done() 51 | }); 52 | }); 53 | }); 54 | 55 | 56 | }); 57 | 58 | -------------------------------------------------------------------------------- /test/subscribe.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const User = require('../api/models/user'); 4 | 5 | let chai = require('chai'); 6 | let chaiHttp = require('chai-http'); 7 | let app = require('../app'); 8 | let should = chai.should(); 9 | 10 | 11 | chai.use(chaiHttp); 12 | 13 | //Our parent block 14 | describe('Subscribe', () => { 15 | 16 | /* 17 | * Test the /POST subscribe/route 18 | */ 19 | describe('Subscribe the user', () => { 20 | it('it should not save a user without userId', (done) => { 21 | let user = { 22 | token: "4555", 23 | phoneNumber: "+201111111111", 24 | email: "mohamed1refaie@hotmail.com" 25 | }; 26 | chai.request(app) 27 | .post('/subscribe') 28 | .send(user) 29 | .end((err, res) => { 30 | res.should.have.status(401); 31 | res.body.should.be.a('object'); 32 | res.body.should.have.property('errors'); 33 | res.body.errors.should.have.property('userId'); 34 | res.body.errors.userId.should.have.property('message'); 35 | res.body.errors.userId.should.have.property('message').eql('Path `userId` is required.'); 36 | done(); 37 | }); 38 | }); 39 | 40 | }); 41 | 42 | /* 43 | * Test the /POST subscribe/:topic route 44 | */ 45 | describe('Subscribe the user or users to a topic', () => { 46 | it('it should not make a subscription to a topic without an ids field', (done) => { 47 | let object = {}; 48 | chai.request(app) 49 | .post('/subscribe/sports') 50 | .send(object) 51 | .end((err, res) => { 52 | res.should.have.status(400); 53 | res.body.should.be.a('object'); 54 | res.body.should.have.property('error').eql('ids field is required'); 55 | done() 56 | }); 57 | }); 58 | }); 59 | 60 | 61 | }); 62 | 63 | -------------------------------------------------------------------------------- /api/controllers/subscribe.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | const admin = require('../../firebase'); 3 | const mongoose = require('mongoose'); 4 | 5 | /* 6 | subscribe function extracts the user's info 7 | from the request body ,creates a new user 8 | and try to save the user in the database 9 | */ 10 | exports.subscribe = (req, res, next) => { 11 | 12 | const user = new User({ 13 | _id: new mongoose.Types.ObjectId(), 14 | token: req.body.token, 15 | phoneNumber: req.body.phoneNumber, 16 | userId: req.body.userId, 17 | email: req.body.email 18 | }); 19 | 20 | user.save().then(result => { 21 | res.status(201).json(result) 22 | }).catch(err => { 23 | res.status(401).json(err); 24 | } 25 | ); 26 | 27 | }; 28 | 29 | 30 | /* 31 | subscribe_to_topic function extracts the topic of subscription 32 | from request params , the Ids string or array of strings from 33 | request body. Then find the users with those Ids, get their 34 | tokens and subscribe the tokens to the topic 35 | */ 36 | exports.subscribe_to_topic = (req, res, next) => { 37 | const topic = req.params.topic; 38 | const Ids = req.body.ids; 39 | if (Ids === undefined || Ids === "") { 40 | res.status(400).json({error: "ids field is required"}); 41 | } 42 | User.find({'userId': Ids}).select('token -_id').then(users => { 43 | let tokens = users.map((user) => { 44 | return user.token; 45 | }); 46 | if (tokens.length > 0) { 47 | admin.messaging().subscribeToTopic(tokens, topic) 48 | .then(function (response) { 49 | res.status(200).json(response); 50 | }) 51 | .catch(function (error) { 52 | res.status(500).json(error); 53 | }); 54 | } else { 55 | res.status(400).json({error: "not valid Ids"}); 56 | } 57 | 58 | }).catch(err => { 59 | console.log(err); 60 | res.status(500).json({ 61 | error: err, 62 | }); 63 | }); 64 | 65 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notification Service 2 | 3 | This is a Notification service build with [nodejs](https://nodejs.org/en/), it uses [express](https://expressjs.com/), [mongoose](https://mongoosejs.com/) and [firebase-admin sdk](https://firebase.google.com/docs/admin/setup) to send notifications. It let you 4 | 1. subscribe a user to the notifications, subscribe a user to a certain topic. 5 | 2. send notification to all the subscribed users, to a specific user or users, to a group of users with a common topic. 6 | 3. send sms to all the subscribed users, to a specific user or users. (you have to integrate with an sms provider) 7 | 4. refresh a certain user's firebase token. 8 | 9 | ## Instructions 10 | 11 | To Run the Project: 12 | * clone the repo or download it 13 | * cd into the directory of the project from the terminal 14 | * head to the [Firebase console](https://console.firebase.google.com/u/0/) 15 | * create a new project from the firebase console and give it a name, or select an existing project. 16 | * go to your project's settings, then to service accounts, then generate new private key and download it. 17 | * rename `serviceAccountKey.json.example` to `serviceAccountKey.json` 18 | * copy the content of the private key file you just downloaded into `serviceAccountKey.json` 19 | * configure your mongo database in `mongoose.js` in my case i use a free cluster at [MongoDB Atlas](https://www.mongodb.com/cloud/atlas), and it's password will be in `nodemon.json.example` then rename it to `nodemon.json` 20 | * install all project dependencies with `npm install` 21 | * run the project with `npm start` and it will be listening at localhost:3000 22 | * test the project with `npm test` 23 | 24 | ***To Run the Service with Docker:*** 25 | * make sure that docker and docker-compose are installed. 26 | * run `sudo docker-compose up` it will be listening at localhost:3000 27 | 28 | The API contains 8 endpoints 29 | **You can see a full Documentation for the API and examples from [here](https://documenter.getpostman.com/view/3845720/S1Lwy7kT)** 30 | 31 | 1. /subscribe 32 | * / 33 | * /:topic 34 | 2. /notifications 35 | * / 36 | * /notifications/toAll 37 | * /notifications/toGroup 38 | 3. /sms 39 | * / 40 | * /toAll 41 | 4. /refreshToken 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/notifications.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | const User = require('../api/models/user'); 4 | 5 | let chai = require('chai'); 6 | let chaiHttp = require('chai-http'); 7 | let app = require('../app'); 8 | let should = chai.should(); 9 | 10 | 11 | chai.use(chaiHttp); 12 | 13 | //Our parent block 14 | describe('Notifications', () => { 15 | 16 | /* 17 | * Test the /POST notifications/toAll route 18 | */ 19 | describe('Notification to All users', () => { 20 | it('it should not send a notification without a title field', (done) => { 21 | let notification = { 22 | message: "Test Notification body", 23 | data: {} 24 | }; 25 | chai.request(app) 26 | .post('/notifications/toAll') 27 | .send(notification) 28 | .end((err, res) => { 29 | res.should.have.status(400); 30 | res.body.should.be.a('object'); 31 | res.body.should.have.property('error').eql("title, message, and data are required"); 32 | done(); 33 | }); 34 | }); 35 | 36 | }); 37 | 38 | /* 39 | * Test the /POST notifications/ route 40 | */ 41 | describe('Notification to a specific user or users', () => { 42 | it('it should not send a notification without an ids field', (done) => { 43 | let notifcation = { 44 | title: "Dummy title", 45 | message: "Dummy message", 46 | data: {} 47 | }; 48 | chai.request(app) 49 | .post('/notifications') 50 | .send(notifcation) 51 | .end((err, res) => { 52 | res.should.have.status(400); 53 | res.body.should.be.a('object'); 54 | res.body.should.have.property('error').eql('ids, title, message, and data are required'); 55 | done() 56 | }); 57 | }); 58 | }); 59 | 60 | 61 | /* 62 | * Test the /POST notifications/toGroup route 63 | */ 64 | describe('Notification to a specific group', () => { 65 | it('it should not send a notification without an topic field', (done) => { 66 | let notifcation = { 67 | title: "Dummy title", 68 | message: "Dummy message", 69 | data: {} 70 | }; 71 | chai.request(app) 72 | .post('/notifications/toGroup') 73 | .send(notifcation) 74 | .end((err, res) => { 75 | res.should.have.status(400); 76 | res.body.should.be.a('object'); 77 | res.body.should.have.property('error').eql('topic, title, message, and data are required'); 78 | done() 79 | }); 80 | }); 81 | }); 82 | 83 | 84 | }); 85 | 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | 84 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 85 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 86 | 87 | # User-specific stuff 88 | .idea/**/workspace.xml 89 | .idea/**/tasks.xml 90 | .idea/**/usage.statistics.xml 91 | .idea/**/dictionaries 92 | .idea/**/shelf 93 | 94 | # Generated files 95 | .idea/**/contentModel.xml 96 | 97 | # Sensitive or high-churn files 98 | .idea/**/dataSources/ 99 | .idea/**/dataSources.ids 100 | .idea/**/dataSources.local.xml 101 | .idea/**/sqlDataSources.xml 102 | .idea/**/dynamic.xml 103 | .idea/**/uiDesigner.xml 104 | .idea/**/dbnavigator.xml 105 | 106 | # Gradle 107 | .idea/**/gradle.xml 108 | .idea/**/libraries 109 | 110 | # Gradle and Maven with auto-import 111 | # When using Gradle or Maven with auto-import, you should exclude module files, 112 | # since they will be recreated, and may cause churn. Uncomment if using 113 | # auto-import. 114 | # .idea/modules.xml 115 | # .idea/*.iml 116 | # .idea/modules 117 | 118 | # CMake 119 | cmake-build-*/ 120 | 121 | # Mongo Explorer plugin 122 | .idea/**/mongoSettings.xml 123 | 124 | # File-based project format 125 | *.iws 126 | 127 | # IntelliJ 128 | out/ 129 | 130 | # mpeltonen/sbt-idea plugin 131 | .idea_modules/ 132 | 133 | # JIRA plugin 134 | atlassian-ide-plugin.xml 135 | 136 | # Cursive Clojure plugin 137 | .idea/replstate.xml 138 | 139 | # Crashlytics plugin (for Android Studio and IntelliJ) 140 | com_crashlytics_export_strings.xml 141 | crashlytics.properties 142 | crashlytics-build.properties 143 | fabric.properties 144 | 145 | # Editor-based Rest Client 146 | .idea/httpRequests 147 | 148 | # Android studio 3.1+ serialized cache file 149 | .idea/caches/build_file_checksums.ser 150 | 151 | # DynamoDB Local files 152 | .dynamodb/ 153 | 154 | 155 | serviceAccountKey.json 156 | nodemon.json -------------------------------------------------------------------------------- /api/controllers/sms.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | 3 | 4 | /* 5 | send_sms function receives the phoneNumbers and 6 | the message and returns a promise 7 | TODO: INTEGRATE WITH A REAL SMS PROVIDER 8 | */ 9 | let send_sms = (phoneNumbers, message) => { 10 | 11 | return new Promise(function (resolve, reject) { 12 | // integrate with an sms provider 13 | resolve({message: "success"}); 14 | 15 | }) 16 | 17 | }; 18 | 19 | 20 | /* 21 | send_sms_to_all function sends sms to all users, 22 | it extracts the message from the request body, 23 | gets the phone numbers for all users, 24 | divide the phone numbers in loads that sms provider 25 | can handle in one minute, iterate over the loads and 26 | send sms to each load, wait a minute and send again. 27 | */ 28 | exports.send_sms_to_all = (req, res, next) => { 29 | 30 | const msg = req.body.message; 31 | if (msg === undefined || msg === "") { 32 | res.status(400).json({error: "message is required"}) 33 | } else { 34 | User.find().exec().then(users => { 35 | 36 | let phoneNumbers = users.map((user) => { 37 | return user.phoneNumber; 38 | }); 39 | 40 | if (phoneNumbers.length > 0) { 41 | let returnJson = []; 42 | let statusCode = 200; 43 | let maximumCapacity = 1000; 44 | let noOfLoads = phoneNumbers.length / maximumCapacity; 45 | if (phoneNumbers.length % maximumCapacity !== 0) { 46 | noOfLoads++; 47 | } 48 | noOfLoads = Math.floor(noOfLoads); 49 | 50 | for (let i = 0; i < noOfLoads; i++) { 51 | let start = i * maximumCapacity, end = (i + 1) * maximumCapacity; 52 | let batchPhoneNumbers = phoneNumbers.slice(start, end); 53 | send_sms(batchPhoneNumbers, msg).then(jsonObj => { 54 | returnJson.push({loadNumber: i + 1, success: jsonObj}); 55 | if (i + 1 === noOfLoads) { 56 | res.status(statusCode).json(returnJson); 57 | } 58 | }).catch(error => { 59 | returnJson.push({loadNumber: i + 1, error: error}); 60 | statusCode = 500; 61 | 62 | if (i + 1 === noOfLoads) { 63 | res.status(statusCode).json(returnJson); 64 | } 65 | }); 66 | 67 | setTimeout(() => { 68 | 69 | }, 60000); 70 | 71 | } 72 | } else { 73 | res.status(200).json({message: "no phone numbers to send to"}) 74 | } 75 | 76 | 77 | }).catch(err => { 78 | console.log(err); 79 | res.status(500).json({ 80 | error: err, 81 | }); 82 | }); 83 | } 84 | }; 85 | 86 | 87 | /* 88 | send_sms_to_specific function sends sms to a specific 89 | user or users, it extracts the message and ids from 90 | the request body, get the phone numbers associated 91 | with those ids then send sms to them 92 | */ 93 | exports.send_sms_to_specific = (req, res, next) => { 94 | const msg = req.body.message; 95 | const Ids = req.body.ids; 96 | 97 | if (msg === undefined || msg === "" || Ids === undefined) { 98 | res.status(400).json({error: "message and ids are required"}); 99 | } else { 100 | User.find({'userId': Ids}).select('phoneNumber -_id').then(users => { 101 | let phoneNumbers = users.map((user) => { 102 | return user.phoneNumber; 103 | }); 104 | 105 | if (phoneNumbers.length > 0) { 106 | send_sms(phoneNumbers, msg).then(jsonObj => { 107 | res.status(200).json(jsonObj); 108 | }).catch(error => { 109 | res.status(500).json(error); 110 | }) 111 | } else { 112 | res.status(200).json({message: "No valid ids"}); 113 | } 114 | 115 | }).catch(err => { 116 | console.log(err); 117 | res.status(500).json({ 118 | error: err, 119 | }); 120 | }); 121 | } 122 | }; -------------------------------------------------------------------------------- /api/controllers/notifications.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/user'); 2 | const admin = require('../../firebase'); 3 | 4 | 5 | /* 6 | send_notification function sends notification for 7 | the given tokens with the given payload. it receives 8 | the tokens and payload then return a promise, it tries 9 | to send notification to the tokens with fcm admin sdk 10 | */ 11 | let send_notification = (tokens, payload) => { 12 | 13 | let options = { 14 | priority: "normal", 15 | timeToLive: 60 * 60 16 | }; 17 | return new Promise(function (resolve, reject) { 18 | admin.messaging().sendToDevice(tokens, payload, options) 19 | .then(function (response) { 20 | resolve({message: response}); 21 | 22 | }) 23 | .catch(function (error) { 24 | console.log("Error sending message:", error); 25 | reject({error: error}); 26 | }); 27 | }) 28 | 29 | }; 30 | 31 | 32 | /* 33 | send_notification_to_all function sends a notification 34 | to all users. it extracts the message,title and data from 35 | the request body. initialize the payload, gets the tokens 36 | of all users, divide the tokens in loads of 1000's (maximum 37 | number of tokens fcm can handle in one request. then sends 38 | notification for each load. 39 | */ 40 | exports.send_notification_to_all = (req, res, next) => { 41 | 42 | const title = req.body.title; 43 | const msg = req.body.message; 44 | const data = req.body.data; 45 | if (msg === undefined || msg === "" || title === undefined || title === "" || data === undefined) { 46 | res.status(400).json({error: "title, message, and data are required"}); 47 | } else { 48 | let payload = { 49 | notification: { 50 | title: title, 51 | body: msg 52 | }, 53 | data: data 54 | }; 55 | 56 | User.find().exec().then(users => { 57 | 58 | let tokens = users.map((user) => { 59 | return user.token; 60 | }); 61 | 62 | if (tokens.length > 0) { 63 | let returnJson = []; 64 | let statusCode = 200; 65 | let noOfLoads = tokens.length / 1000; 66 | if (tokens.length % 1000 !== 0) { 67 | noOfLoads++; 68 | } 69 | noOfLoads = Math.floor(noOfLoads); 70 | 71 | for (let i = 0; i < noOfLoads; i++) { 72 | let start = i * 1000, end = (i + 1) * 1000; 73 | let batchTokens = tokens.slice(start, end); 74 | send_notification(batchTokens, payload).then(jsonObj => { 75 | returnJson.push({loadNumber: i + 1, success: jsonObj}); 76 | if (i + 1 === noOfLoads) { 77 | res.status(statusCode).json(returnJson); 78 | } 79 | }).catch(error => { 80 | returnJson.push({loadNumber: i + 1, error: error}); 81 | statusCode = 500; 82 | 83 | if (i + 1 === noOfLoads) { 84 | res.status(statusCode).json(returnJson); 85 | } 86 | }); 87 | } 88 | } else { 89 | res.status(200).json({message: "no tokens to send to"}) 90 | } 91 | 92 | 93 | }).catch(err => { 94 | console.log(err); 95 | res.status(500).json({ 96 | error: err, 97 | }); 98 | }); 99 | } 100 | }; 101 | 102 | /* 103 | send_notification_to_specific function send a notification 104 | to a specific user or users. it extracts the ids, message, 105 | title and data from the request body. Initialize the payload, 106 | get the users tokens then sends the notification to them. 107 | */ 108 | exports.send_notification_to_specific = (req, res, next) => { 109 | const Ids = req.body.ids; 110 | const title = req.body.title; 111 | const msg = req.body.message; 112 | const data = req.body.data; 113 | 114 | if (Ids === undefined || msg === undefined || title === undefined || data === undefined || msg === "" || title === "") { 115 | res.status(400).json({error: "ids, title, message, and data are required"}); 116 | } else { 117 | let payload = { 118 | notification: { 119 | title: title, 120 | body: msg 121 | }, 122 | data: data 123 | }; 124 | 125 | User.find({'userId': Ids}).select('token -_id').then(users => { 126 | let tokens = users.map((user) => { 127 | return user.token; 128 | }); 129 | 130 | if (tokens.length > 0) { 131 | send_notification(tokens, payload).then(jsonObj => { 132 | res.status(200).json(jsonObj); 133 | }).catch(error => { 134 | res.status(500).json(error); 135 | }) 136 | } else { 137 | res.status(200).json({message: "No valid ids"}); 138 | } 139 | 140 | }).catch(err => { 141 | console.log(err); 142 | res.status(500).json({ 143 | error: err, 144 | }); 145 | }); 146 | } 147 | 148 | }; 149 | 150 | 151 | /* 152 | send_notification_to_group function sends notification 153 | to a specific group with a common topic. it extracts the 154 | topic, message, title and data from the request body. 155 | Initialize the the payload and sends the notification for 156 | the users subscribed to the given topic. 157 | */ 158 | exports.send_notification_to_group = (req, res, next) => { 159 | const topic = req.body.topic; 160 | const title = req.body.title; 161 | const msg = req.body.message; 162 | const data = req.body.data; 163 | 164 | if (topic === undefined || topic === "" || msg === undefined || title === undefined || data === undefined || msg === "" || title === "") { 165 | res.status(400).json({error: "topic, title, message, and data are required"}); 166 | } else { 167 | 168 | let payload = { 169 | notification: { 170 | title: title, 171 | body: msg 172 | }, 173 | data: data 174 | }; 175 | 176 | admin.messaging().sendToTopic(topic, payload) 177 | .then(function (response) { 178 | res.status(200).json(response); 179 | }) 180 | .catch(function (error) { 181 | res.status(500).json(error); 182 | }); 183 | } 184 | }; 185 | --------------------------------------------------------------------------------