Skip to content

Commit

Permalink
Merge branch 'master' into dragtoadd
Browse files Browse the repository at this point in the history
  • Loading branch information
nikotj1 authored Apr 19, 2020
2 parents 75bf48f + bfae994 commit 8a2e843
Show file tree
Hide file tree
Showing 32 changed files with 653 additions and 199 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,20 @@ It is recommended that you use **VsCode** when contributing to this project. Ple

There are a number of application secrets and credentials which are needed before you begin development. These secrets will be given to you by the Repo maintainer when you start contributing. To set up these envrionment variables you will need to make a `.env` file sitting in the doto-backend folder. An example of this file can be found in the repository `.env.example` contains all the variables that will need to be set (just make a copy of the file and rename to `.env` then copy and paste all the secrets given by the repo maintainer)

The VAPID keys can be generated by running:
```
npx web-push generate-vapid-keys
```
Copy the public and private keys to the backend `.env` then copy the public key to the frontend `.env`.

## Running the code
We are using [lerna](https://lerna.js.org/) and [yarn](https://yarnpkg.com/) for this project.

### Installing dependencies
Run `yarn install --frozen-lockfile` in the project root.

### Starting the frontend and backend
To get the project running locally, run `yarn start` in both `doto-backend` and `doto-frontend` (in separate terminals).
To get the project running locally, run `yarn start` in the project root.

By default, the react-app is hosted on port 3000 and the local server is hosted on port 3001. Do not change these numbers as we have added the addresses as authorized redirect uri's in our google credentials.

Expand Down
2 changes: 2 additions & 0 deletions doto-backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ DEVELOPMENT_DB_CONN=
ACCESS_TOKEN_SECRET=
GOOGLE_API_SECRET=
GOOGLE_API_CLIENT=
VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
5 changes: 5 additions & 0 deletions doto-backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ const user = require("./src/routes/user-route");
app.use("/user", user);
const authRoute = require("./src/routes/auth-route");
app.use("/auth", authRoute);
const reminders = require("./src/routes/reminder-route");
app.use("/reminders", reminders);

// Setup reminder service
require("./src/webpush/reminder-service");

// Swagger UI Setup
const swaggerUi = require("swagger-ui-express");
Expand Down
2 changes: 2 additions & 0 deletions doto-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
"mocha": "^7.1.0",
"mongoose": "^5.9.4",
"mongoose-unique-validator": "^2.0.3",
"node-cron": "^2.0.3",
"nodemon": "^2.0.2",
"passport": "^0.4.1",
"passport-google-oauth20": "^2.0.0",
"swagger-ui-express": "^4.1.3",
"web-push": "^3.4.3",
"winston": "^3.2.1"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions doto-backend/src/models/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const taskSchema = mongoose.Schema({
},
isComplete: {
type: Boolean,
default: false,
},
});

Expand Down
11 changes: 11 additions & 0 deletions doto-backend/src/routes/reminder-route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const express = require("express");
const router = express.Router();
const authenticateToken = require("../config/token-setup").authenticateToken;
const reminderService = require("../webpush/reminder-service");

router.post("/subscribe", authenticateToken, (req, res) => {
reminderService.subscribe(req.user.email, req.body);
res.status(201).json({});
});

module.exports = router;
17 changes: 3 additions & 14 deletions doto-backend/src/routes/task-route.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ router.post("/post", authenticateToken, function (req, res) {
});

// UPDATE task
// TO DO: Authentication should be applied to this route too.
router.put("/:taskId", function (req, res) {
// TO DO: This is not integrated with the frontend.
// Authentication should be applied to this route too.
router.put("/:taskId", authenticateToken, function (req, res) {
Task.updateOne({ taskId: req.params.taskId }, req.body, { new: true }, function (err, updatedTask) {
logger.info(updatedTask);
if (err || !updatedTask) {
Expand All @@ -51,18 +52,6 @@ router.put("/:taskId", function (req, res) {

// DELETE task
router.delete("/:taskId", authenticateToken, function (req, res) {
const task = Task.findOne({ taskId: req.params.taskId }, function (err) {
if (err) {
res.sendStatus(response.BADREQUEST);
}
});
logger.info("task " + task.user);
logger.info("return " + req.user.email);

if (task.user !== req.user.email) {
return res.sendStatus(response.FORBIDDEN);
}

Task.remove({ taskId: req.params.taskId }, function (err) {
if (err) {
logger.error(err);
Expand Down
57 changes: 57 additions & 0 deletions doto-backend/src/webpush/reminder-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const cron = require("node-cron");
const Task = require("../models/Task");
const webpush = require("web-push");
const { logger } = require("../common/logging");

webpush.setVapidDetails("mailto:[email protected]", process.env.VAPID_PUBLIC_KEY, process.env.VAPID_PRIVATE_KEY);

// Maps user.email to a push manager subscription so we know which client to
// send a reminder to.
const subscriptions = new Map();

// Every minute query the database to check if there are tasks that should be
// fired off via web push. Note this means a notification will be delivered
// one minute late in the worst case.
cron.schedule("* * * * *", () => {
Task.find(
{ reminderDate: { $lte: new Date() }, user: { $in: [...subscriptions.keys()] }, isComplete: false },
(err, tasks) => {
if (err) {
logger.error(err);
return;
}
for (let task of tasks) {
const subscription = subscriptions.get(task.user);
if (subscription) {
webpush
.sendNotification(subscription, JSON.stringify(task))
.then(() => {
logger.info(`Fired notification id=${task.id} title=${task.title}`);
// This is a bit of a hack.
// Unsetting the field means the notification is fired so we can avoid duplicating.
task.reminderDate = undefined;
task.save();
})
.catch((err) => {
logger.error(err.stack);
});
} else {
logger.error("Subscription not found. This should never occur.");
}
}
},
);
});

const subscribe = (id, subscription) => {
if (typeof id === "string" && subscription && subscription.endpoint) {
subscriptions.set(id, subscription);
logger.info(`Registered subscription for ${id}`);
}
};
const unsubscribe = (id) => {
subscriptions.delete(id);
logger.info(`Removed subscription for ${id}`);
};

module.exports = { subscribe, unsubscribe };
127 changes: 127 additions & 0 deletions doto-backend/test/task.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const UserModel = require("../src/models/User");
const assert = require("assert");

const validUser = new UserModel({
email: "[email protected]",
name: "john",
picture: "profile.png",
themePreference: "dark",
Expand Down Expand Up @@ -152,6 +153,26 @@ describe("Task Model Tests", function () {
});
});

it('update task sucessfully', async function () {
await validTask.save();
const savedTask = await TaskModel.findOne({_id: validTask._id});

await savedTask.update({ title: 'updated title' });

const updatedTask = await TaskModel.findOne({_id: validTask._id});
assert(updatedTask.title === 'updated title');
});

it("delete task successfully.", async function () {
await validTask.save();
const savedTask = await TaskModel.findOne();

await savedTask.remove();
const newSavedTask = await TaskModel.findOne({_id: validTask._id});

assert(newSavedTask === null);
});

it("update one isComplete status to true", async function () {
TaskModel.updateOne({ taskId: validTask.taskId }, { isComplete: true })
.then(() => TaskModel.findOne({ taskId: validTask.taskId }))
Expand All @@ -175,4 +196,110 @@ describe("Task Model Tests", function () {
assert(task.isComplete === true);
});
});

// Begin reminder service tests
//
// TODO - We should clear the database after each unit test.
//
// Current workaround is to create new model objects and
// increment the (unique) id so that tests are 'stateless'
// i.e. do not depend on order of execution.
it("retrieves tasks with reminderDate lte to current date", async function () {
const testTask = new TaskModel({
user: "[email protected]",
taskId: "2",
title: "title",
description: "Re-Doing all the things",
location: "science building",
priority: 0,
duration: 120,
reminderDate: "2020-07-14T07:50:00+12:00",
startDate: "2020-08-14T08:50:00+12:00",
endDate: "2020-08-14T07:50:00+12:00",
isComplete: false,
});
await testTask.save();
const [retrievedTask] = await TaskModel.find({
taskId: "2",
reminderDate: { $lte: new Date(testTask.reminderDate.getTime() + 1) },
user: { $in: [testTask.user] },
isComplete: false,
}).exec();

assert.equal(retrievedTask.taskID, testTask.taskID);
});

it("does not retrieve tasks with unset reminder date", async function () {
const testTask = new TaskModel({
user: "[email protected]",
taskId: "3",
title: "title",
description: "Re-Doing all the things",
location: "science building",
priority: 0,
duration: 120,
startDate: "2020-08-14T08:50:00+12:00",
endDate: "2020-08-14T07:50:00+12:00",
isComplete: false,
});
await testTask.save();
const retrievedTasks = await TaskModel.find({
taskId: "3",
reminderDate: { $lte: new Date(validTask.reminderDate) },
user: { $in: [testTask.user] },
isComplete: false,
}).exec();

assert(retrievedTasks.length === 0);
});

it("does not retrieve tasks with future reminder date", async function () {
const testTask = new TaskModel({
user: "[email protected]",
taskId: "4",
title: "title",
description: "Re-Doing all the things",
location: "science building",
priority: 0,
duration: 120,
reminderDate: "2020-07-14T07:50:00+12:00",
startDate: "2020-08-14T08:50:00+12:00",
endDate: "2020-08-14T07:50:00+12:00",
isComplete: false,
});
await testTask.save();
const retrievedTasks = await TaskModel.find({
taskId: "4",
reminderDate: { $lte: new Date(testTask.reminderDate.getTime() - 1) },
user: { $in: [testTask.user] },
isComplete: false,
}).exec();

assert(retrievedTasks.length === 0);
});

it("does not retrieve tasks which are completed", async function () {
const testTask = new TaskModel({
user: "[email protected]",
taskId: "5",
title: "title",
description: "Re-Doing all the things",
location: "science building",
priority: 0,
duration: 120,
reminderDate: "2020-07-14T07:50:00+12:00",
startDate: "2020-08-14T08:50:00+12:00",
endDate: "2020-08-14T07:50:00+12:00",
isComplete: true,
});
await testTask.save();
const retrievedTasks = await TaskModel.find({
taskId: "5",
reminderDate: { $lte: new Date(testTask.reminderDate) },
user: { $in: [testTask.user] },
isComplete: false,
}).exec();

assert(retrievedTasks.length === 0);
});
});
Loading

0 comments on commit 8a2e843

Please sign in to comment.