diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..cc7747aae
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+*.DS_Store
+node_modules/
+.history/
+package-lock.json
+.env
+public/js/moment.js
+public/js/moment.min.js
diff --git a/README.md b/README.md
index 6faca9f83..03b4f9c14 100644
--- a/README.md
+++ b/README.md
@@ -1,56 +1,50 @@
-# cs4241-FinalProject
+# cs4241-FinalProject:Taskinator
-For your final project, you'll implement a course project that exhibits your mastery of the course materials.
-This project should provide an opportunity to be creative and to pursue individual research and learning.
+README
-## General description
+Project Description
+==========================
+https://webware-team19-final.herokuapp.com/login
-Your project should consist of a complete Web application, exhibiting facets of the three main sections of the course material:
+Taskinator is a website that allows users to keep track of their tasks for a project. This website provides a simplistic yet useful interface to allow users to create groups, add team members to the groups, and assign tasks to each member. Hence, it specifically targets people who are working on a team project and need an organizational tool.
-- Static Web page content and design. You should have a project that is accessible, easily navigable, and features significant content.
-- Dynamic behavior implemented with JavaScript (TypeScript is also allowed if your group wants to explore it).
-- Server-side programming *using Node.js*. Typically this will take the form of some sort of persistent data, authentication, and possibly server-side computation. Ideally it will also include support for realtime commmunication as discussed below.
-- Groups are *highly encouraged* to consider including some type of realtime communication technology in their projects (chat, networked multiplayer games, collaborative coding/editing, video/audio via WebRTC etc.) We'll be discussing many of these technologies in class next week.
-- A video (less than five minutes) where each group member explains some aspect of the project. An easy way to produce this video is for you all the groups members to join a Zoom call that is recorded; each member can share their screen when they discuss the project or one member can "drive" the interface while other members narrate (this second option will probably work better.) The video should be posted on YouTube or some other accessible video hosting service.
+The website consists of three pages: a login page, a home page, and a task page. Logging in leads the users to the home page where they can see what groups they are a part of, or create new groups. The homepage also shows a list of tasks that are approaching their deadline or that are overdue. Clicking on each group leads to a task page corresponding to that group. Each task page allows the user to add lists of tasks where different lists can pertain to different categories of task. On this page, the user can also invite other team members. Additionally, the task page has a chat feature that allows real-time communication for the team members, making it easier for them to discuss their project as they organize their tasks.
-## Project ideation
-Excellent projects serve someone/some group; for this assignment you need to define your users and stakeholders. I encourage you to identify projects that will have impact, either artistically, politically, or in terms of productivity. Consider creating something useful for a cause or hobby you care about.
+Additional Instructions
+========================
-## Logistics
+Taskinator requires the users to have a GitHub or a Google account to login with.
-### Team size
-Students are will work in teams of 3-5 students for the project; teams of two can be approved with the permission of the instructor. Working in teams will allow you to build a good project in a limited amount of time.
+Utilized Technologies
+======================
+- Materialize Framework
+ We used Google's Materialize framework to stylize the entire website and better the functionalities of the html elements.
+- Javascript
+- Node.js
+ We used Node.js for server-side programming.
+- MongoDB for Server + Mongoose
+ We used a combination of MongoDB and Mongoose to ensure persistence of data for each user.
+- OAuth Authentication (from multiple services Google, GitHub)
+ We used Google and GitHub OAuth Authentications to have the users log into the website.
+- Socket io
+ We used Socket io for the live chat feature on the task page.
+- Moment.js
+ We used Moment.js to handle due dates of each task.
-### Deliverables
+Challenges Faced
+=====================
+An expected challenge we faced was deciding on the most appropriate technologies for this project. Along the same lines, it was also a bit difficult to make sure we found a good balance between having this website do all we desired while also meeting the requirements of the assignment. Implementing our back-end API in conjuction with our front-end code took longer than expected. Additionally the number of features we were adding and items we were keeping track of/sending data between added a growing level of complexity as we worked, drawing out or development time with testing and debugging. The chat functionality took time to implement particularly with keeping the messages displayed in the correct order. Additionally we didn't have enough time to schedule regular meetings during finals week as a team but were able to use Microsoft Teams for effective communication.
-__Proposal:__
-Provide an outline of your project direction and the names of the team members.
-The outline should have enough detail so that staff can determine if it meets the minimum expectations, or if it goes too far to be reasonable by the deadline.
-This file must be named proposal.md so we can find it.
-Submit a PR to turn it in by Monday, 11:59 PM
+Member Responsibilities
+=======================
+- Benny: Front-end, page layout and styling home and login page + chat styling
+- Luke: Back-end, databases and authentication + chat-based functionality (socket.io)
+- Nikhil: Front-end, Javascript for task-list functionality, group invites
+- Rimsha: Front-end, task page layout and styling
+- Adam: Front-end, Javascript for home page functionality and group invite handling
-There are no other scheduled checkpoints for your project.
+Project video
+===============
+https://youtu.be/fCMFh9jnc3g
-#### Turning in Your Outline / Project
-
-**NOTE: code is due before the project presentation day due to the end of term / grading schedule constraints**
-Submit a second PR on the final project repo to turn in your app and code.
-
-Deploy your app, in the form of a webpage, to Glitch/Heroku/Digital Ocean or some other service.
-Folks on the same team do not need to post the same webpage, but must instead clearly state who is on the team in their proposal.
-
-The README for your second pull request doesn’t need to be a formal report, but it should contain:
-
-1. A brief description of what you created, and a link to the project itself.
-2. Any additional instructions that might be needed to fully use your project (login information etc.)
-3. An outline of the technologies you used and how you used them.
-4. What challenges you faced in completing the project.
-5. What each group member was responsible for designing / developing.
-6. A link to your project video.
-
-Think of 1,3, and 4 in particular in a similar vein to the design / tech achievements for A1—A4… make a case for why what you did was challenging and why your implementation deserves a grade of 100%.
-
-## FAQs
-
-- **Can I use XYZ framework?** You can use any web-based frameworks or tools available, but for your server programming you need to use node.js. Your client-side language should be either JavaScript or TypeScript.
diff --git a/config/passport-config.js b/config/passport-config.js
new file mode 100644
index 000000000..3260d623b
--- /dev/null
+++ b/config/passport-config.js
@@ -0,0 +1,49 @@
+const passport = require("passport");
+const GitHubStrategy = require("passport-github2").Strategy;
+const GoogleStrategy = require("passport-google-oauth2").Strategy;
+
+const setupPassport = () => {
+ const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
+ const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
+ const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
+ const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
+
+ passport.serializeUser((user, done) => done(null, user));
+ passport.deserializeUser((obj, done) => done(null, obj));
+
+ passport.use(new GitHubStrategy({
+ clientID: GITHUB_CLIENT_ID,
+ clientSecret: GITHUB_CLIENT_SECRET,
+ callbackURL: "https://webware-team19-final.herokuapp.com/auth/github/callback"
+ }, (accessToken, refreshToken, profile, done) => process.nextTick(() => {
+ done(null, profile);
+ })));
+
+ passport.use(new GoogleStrategy({
+ clientID: GOOGLE_CLIENT_ID,
+ clientSecret: GOOGLE_CLIENT_SECRET,
+ callbackURL: "https://webware-team19-final.herokuapp.com/auth/google/callback"
+ }, (accessToken, refreshToken, profile, done) => process.nextTick(() => {
+ done(null, profile);
+ })));
+};
+
+const ensureAuthenticated = (req, res, next) => {
+ if (req.isAuthenticated()) {
+ return next();
+ }
+ res.status(401).redirect("/login");
+};
+
+const getUsername = req => {
+ if (req.isAuthenticated()) {
+ if (req.user.username) {
+ return req.user.username;
+ } else if (req.user.provider === "google") {
+ return req.user.name.givenName.toLowerCase() + req.user.name.familyName.toLowerCase();
+ }
+ }
+ return undefined;
+};
+
+module.exports = {setupPassport, ensureAuthenticated, getUsername};
\ No newline at end of file
diff --git a/models/Group.js b/models/Group.js
new file mode 100644
index 000000000..3d840795e
--- /dev/null
+++ b/models/Group.js
@@ -0,0 +1,30 @@
+// @author: Luke Bodwell
+"use strict";
+
+const mongoose = require("mongoose");
+
+const Schema = mongoose.Schema;
+
+const GroupSchema = new Schema({
+ name: {
+ type: String,
+ required: true
+ },
+ adminId: {
+ type: Schema.Types.ObjectId,
+ ref: "User",
+ required: true
+ },
+ members: {
+ type: [Schema.Types.ObjectId],
+ ref: "User",
+ required: true
+ },
+ invitees: {
+ type: [Schema.Types.ObjectId],
+ ref: "User",
+ required: true
+ }
+});
+
+module.exports = mongoose.model("Group", GroupSchema);
\ No newline at end of file
diff --git a/models/Message.js b/models/Message.js
new file mode 100644
index 000000000..b6694ace5
--- /dev/null
+++ b/models/Message.js
@@ -0,0 +1,34 @@
+// @author: Luke Bodwell
+"use strict";
+
+const mongoose = require("mongoose");
+
+const Schema = mongoose.Schema;
+
+const MessageSchema = new Schema({
+ groupId: {
+ type: Schema.Types.ObjectId,
+ ref: "Group",
+ required: true
+ },
+ senderId: {
+ type: Schema.Types.ObjectId,
+ ref: "User",
+ required: true
+ },
+ content: {
+ type: String,
+ required: true
+ },
+ edited: {
+ type: Boolean,
+ required: true
+ },
+ dateSent: {
+ type: Date,
+ required: true,
+ default: Date.now
+ }
+});
+
+module.exports = mongoose.model("Message", MessageSchema);
\ No newline at end of file
diff --git a/models/Task.js b/models/Task.js
new file mode 100644
index 000000000..48c09d514
--- /dev/null
+++ b/models/Task.js
@@ -0,0 +1,47 @@
+// @author: Luke Bodwell
+"use strict";
+
+const mongoose = require("mongoose");
+
+const Schema = mongoose.Schema;
+
+const TaskSchema = new Schema({
+ name: {
+ type: String,
+ required: true
+ },
+ desc: {
+ type: String,
+ required: false
+ },
+ groupId: {
+ type: Schema.Types.ObjectId,
+ ref: "Group",
+ required: true
+ },
+ columnName: {
+ type: String,
+ required: true
+ },
+ assignees: {
+ type: String, //[Schema.Types.ObjectId],
+ ref: "User",
+ required: false
+ },
+ tags: {
+ type: [String],
+ required: false
+ },
+ dateDue: {
+ //Changed this to string, @Luke change it back to date
+ type: Date,
+ required: false
+ },
+ dateCreated: {
+ type: Date,
+ required: true,
+ default: Date.now
+ }
+});
+
+module.exports = mongoose.model("Task", TaskSchema);
diff --git a/models/User.js b/models/User.js
new file mode 100644
index 000000000..2b1ecaa1e
--- /dev/null
+++ b/models/User.js
@@ -0,0 +1,19 @@
+// @author: Luke Bodwell
+"use strict";
+
+const mongoose = require("mongoose");
+
+const Schema = mongoose.Schema;
+
+const UserSchema = new Schema({
+ username: {
+ type: String,
+ required: true
+ },
+ displayName: {
+ type: String,
+ required: true
+ }
+});
+
+module.exports = mongoose.model("User", UserSchema);
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 000000000..605f4d6b6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "final-project",
+ "version": "1.0.0",
+ "description": "For your final project, you'll implement a course project that exhibits your mastery of the course materials. \r This project should provide an opportunity to be creative and to pursue individual research and learning.",
+ "main": "server.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "node server.js",
+ "dev": "cross-env NODE_ENV=development nodemon server.js",
+ "prod": "cross-env NODE_ENV=production node server.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/nchintada/final-project.git"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/nchintada/final-project/issues"
+ },
+ "homepage": "https://github.com/nchintada/final-project#readme",
+ "devDependencies": {
+ "cross-env": "^7.0.2",
+ "nodemon": "^2.0.4"
+ },
+ "dependencies": {
+ "body-parser": "^1.19.0",
+ "compression": "^1.7.4",
+ "dotenv": "^8.2.0",
+ "express": "^4.17.1",
+ "express-session": "^1.17.1",
+ "helmet": "^4.1.1",
+ "materialize-css": "^1.0.0",
+ "method-override": "^3.0.0",
+ "moment": "^2.29.1",
+ "mongodb": "^3.6.2",
+ "mongoose": "^5.10.8",
+ "morgan": "^1.10.0",
+ "passport": "^0.4.1",
+ "passport-github2": "^0.1.12",
+ "passport-google-oauth2": "^0.2.0",
+ "socket.io": "^2.3.0"
+ }
+}
diff --git a/proposal.md b/proposal.md
new file mode 100644
index 000000000..48f67f166
--- /dev/null
+++ b/proposal.md
@@ -0,0 +1,37 @@
+# Webware Team 19
+
+# Team Members:
+- Nikhil Chintada
+- Luke Bodwell
+- Rimsha Kayastha
+- Adam Desveaux
+- Benny Klaiman
+
+# Project Description:
+A website designed for teams to collaborate in planning tasks. The website will have a login page which allows users to create an account tied to an existing service.
+There will be a home page where the user can see what groups they are a part of and make a new group. Once a group is made they can invite users by entering their username.
+The homepage will also have a list of tasks approaching their deadline or that are overdue.
+Once the user is part of a group they can click the group and go to their group's page which will contain a list of tasks (multi-column format).
+This list of tasks can be added to, modified and items can be deleted or marked completed.
+The tasks will have a name, due date, task type and assigned user(s) in which either the team or a user or group of users can be assigned a task.
+This page will also have a live chat window in which team members can speak to one another.
+
+# Demographics
+Users: Anyone who is trying to complete a group project (small to medium sized groups).
+Stakeholders: Our users.
+
+# Technical Specifications:
+- HTML + CSS Using a framework
+- Javascript
+- Node.js
+- MongoDB for Server + Mongoose
+- OAuth Authentication (from multiple services Google, GitHub)
+- Live chat feature (Socket io)
+- Moment.js for dealing with due dates
+
+# Team Assignments:
+- Benny: Front-end, page layout and styling
+- Luke: Back-end, databases and authentication + socket.io
+- Nikhil: Front-end, Javascript for authentication, task-list, group selection
+- Rimsha: Front-end, page layout and styling
+- Adam: Chat-based functionality (socket.io)
diff --git a/public/css/style.css b/public/css/style.css
new file mode 100644
index 000000000..cbfe36fc5
--- /dev/null
+++ b/public/css/style.css
@@ -0,0 +1,143 @@
+body{
+ position: relative;
+ font-family: 'Roboto', sans-serif;
+ overflow-y: hidden;
+}
+
+.task-btns {
+ padding-top: 5px;
+}
+
+
+/*button{*/
+/* background-color: black;*/
+/* color: white;*/
+/* height: 40px;*/
+/*}*/
+
+/*.header_btns{*/
+/* float: right;*/
+/* margin: 5px;*/
+/* width: 120px;*/
+/*}*/
+
+#task_container{
+ overflow-x: auto;
+ margin: 10px;
+ margin-top: 40px;
+ float: left;
+ height: 85vh;
+ width: 80vw;
+}
+
+.inner-container{
+ float: left;
+ margin: 15px;
+}
+
+#chat_box{
+ margin: 10px;
+ background-color: white;
+ height: 87vh;
+ width: 250px;
+ position: fixed;
+ right: 0;
+ /*clear: right;*/
+}
+
+.list-names{
+ cursor: pointer;
+ width: fit-content;
+}
+
+.list-names:hover{
+ box-shadow: 2px 2px 5px grey;
+}
+
+.task_lists{
+ width: 250px;
+ height: 75vh;
+ overflow-y: auto;
+ float: left;
+}
+
+.task_lists:hover{
+ cursor: pointer;
+}
+
+/*.task_contents{*/
+/* width: 95%;*/
+/* padding: 5px;*/
+/* margin: 6px;*/
+/*}*/
+
+/*h1{*/
+/* text-align: center;*/
+/*}*/
+
+.btn_container{
+ width: 300px;
+ float: right;
+ height: 75vh;
+ position: relative;
+}
+
+.btn_container button{
+ display: block;
+ position: absolute;
+ top: 50%;
+ -ms-transform: translateY(-50%);
+ transform: translateY(-50%);
+ margin: 0;
+}
+
+.task_card{
+ height: 180px;
+ width: 84%;
+ padding: 5px;
+ margin: 16px;
+}
+
+.task_card:hover{
+ cursor: pointer;
+}
+
+.btn{
+ margin: 5px;
+}
+
+/*.task_item{*/
+/* text-align: center;*/
+/*}*/
+
+/*.list_name{*/
+/* text-align: center;*/
+/*}*/
+
+/* The Modal (background) */
+/*.modal {*/
+/* display: none; !* Hidden by default *!*/
+/* position: fixed; !* Stay in place *!*/
+/* z-index: 1; !* Sit on top *!*/
+/* padding-top: 100px; !* Location of the box *!*/
+/* left: 0;*/
+/* top: 0;*/
+/* width: 100%; !* Full width *!*/
+/* height: 100%; !* Full height *!*/
+/* overflow: auto; !* Enable scroll if needed *!*/
+/* background-color: rgb(0,0,0); !* Fallback color *!*/
+/* background-color: rgba(0,0,0,0.4); !* Black w/ opacity *!*/
+/*}*/
+
+/*!* Modal Content *!*/
+/*.modal-content {*/
+/* background-color: #fefefe;*/
+/* margin: auto;*/
+/* padding: 20px;*/
+/* border: 1px solid #888;*/
+/* width: 30%;*/
+/*}*/
+
+/*input{*/
+/* margin: 3px;*/
+/*}*/
\ No newline at end of file
diff --git a/public/home.html b/public/home.html
new file mode 100644
index 000000000..f790c17e9
--- /dev/null
+++ b/public/home.html
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+ CS4241 Team 19 Final Project - Home
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This will delete all tasks on this list, are you sure you want to delete?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/routes/api/api-router.js b/routes/api/api-router.js
new file mode 100644
index 000000000..d23a49fd7
--- /dev/null
+++ b/routes/api/api-router.js
@@ -0,0 +1,18 @@
+// @author: Luke Bodwell
+"use strict";
+
+const express = require("express");
+
+const users = require("./users");
+const groups = require("./groups");
+const tasks = require("./tasks");
+const messages = require("./messages");
+
+const router = express.Router();
+
+router.use("/users", users.router);
+router.use("/groups", groups.router);
+router.use("/tasks", tasks.router);
+router.use("/messages", messages.router);
+
+module.exports = {router};
\ No newline at end of file
diff --git a/routes/api/groups.js b/routes/api/groups.js
new file mode 100644
index 000000000..a70578bf8
--- /dev/null
+++ b/routes/api/groups.js
@@ -0,0 +1,178 @@
+// @author: Luke Bodwell
+"use strict";
+
+const express = require("express");
+
+const Group = require("../../models/Group");
+const User = require("../../models/User");
+const passportConfig = require("../../config/passport-config");
+
+const router = express.Router();
+const {ensureAuthenticated, getUsername} = passportConfig;
+
+/*
+ * Route: /api/groups/
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets all groups the current user belongs to. Verified by session.
+ */
+router.get("/", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Find the groups the user with the given id belongs to
+ const groups = await Group.find({members: userId});
+
+ // Send result
+ res.status(200).json({success: true, data: groups});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/groups/invites
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets all groups the current user has been invited to. Verified by session.
+ */
+router.get("/invites", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Find the groups the user with the given id has been invited to
+ const groups = await Group.find({invitees: userId});
+
+ // Send result
+ res.status(200).json({success: true, data: groups});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ console.log(err)
+ }
+});
+
+/*
+ * Route: /api/groups/:id
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets the group with the given id. User must belong to group. Verified by session.
+ */
+router.get("/:id", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const groupId = req.params.id;
+ const username = getUsername(req);
+
+ try {
+ // Find the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Find the group with the given group id, ensuring the current user belongs to it
+ const group = await Group.findOne({_id: groupId, members: userId});
+ console.log("Group: " + group)
+ console.log("Groupid: " + groupId)
+ // Send result
+ res.status(200).json({success: true, data: group});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/groups
+ * Method: POST
+ * Auth: Required
+ * Desc: Adds a new group with the given name, adds the current user to the member list, and sets them as admin.
+ * Verified by session.
+ */
+router.post("/", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {name} = req.body;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of user with the given username
+ const adminId = (await User.findOne({username}))._id;
+ // Create a new group with the given name and admin id
+ let newGroup = new Group({name, adminId, members: [adminId], invitees: []});
+ // Save the new message to the database
+ newGroup = await newGroup.save();
+
+ // Send result
+ res.status(201).json({success: true, data: newGroup});
+ } catch (err) {
+ console.error(err);
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ console.log(err)
+ }
+});
+
+/*
+ * Route: /api/groups/:id
+ * Method: DELETE
+ * Auth: Required
+ * Desc: Deletes the group with the given id. User must be admin of the group. Verified by session.
+ */
+router.delete("/:id", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const groupId = req.params.id;
+ const username = getUsername(req);
+
+ try {
+ // Find the user with the given username
+ const currentUser = (await User.findOne({username}));
+ // Find and delete the group with the given id if the current user is the admin
+ await Group.findOneAndDelete({_id: groupId, adminId: currentUser._id});
+
+ // Send result
+ res.status(204).json({success: true});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+
+/*
+ * Route: /api/groups/:id
+ * Method: PATCH
+ * Auth: Required
+ * Updates the members of the group with the given id. User must belong to group. Verified by session.
+ */
+router.patch("/:id", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const groupId = req.params.id;
+ const {invitees, members} = req.body;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Find the group with the given id, ensuring the current user is a member of the group
+ let group = await Group.findOne({$and: [{_id: groupId}, {$or: [{members: userId}, {invitees: userId}]}]});
+ // Update fields based on request body
+ group.invitees = invitees ? invitees : group.invitees;
+ group.members = members ? members : group.members;
+
+ // Save updated group to database
+ group = await group.save();
+
+ // Send result
+ res.status(200).json({success: true, data: group});
+ } catch (err) {
+ console.log(err)
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ console.log(err)
+ }
+});
+
+module.exports = {router};
diff --git a/routes/api/messages.js b/routes/api/messages.js
new file mode 100644
index 000000000..cff54bbb2
--- /dev/null
+++ b/routes/api/messages.js
@@ -0,0 +1,159 @@
+// @author: Luke Bodwell
+"use strict";
+
+const express = require("express");
+
+const Message = require("../../models/Message");
+const Group = require("../../models/Group");
+const User = require("../../models/User");
+const passportConfig = require("../../config/passport-config");
+
+const router = express.Router();
+const {ensureAuthenticated, getUsername} = passportConfig;
+
+/*
+ * Route: /api/messages/:groupId
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets all messages in the given group. User must belong to the group. Verified by session.
+ */
+router.get("/:groupId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId} = req.params;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.findOne({_id: groupId, members: userId});
+ // Find all messages with the given group id sorted by date sent (ascending)
+ const messages = await Message.find({groupId}).sort({dateSent: 1});
+
+ // Send result
+ res.status(200).json({success: true, data: messages});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/messages/:groupId/:messageId
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets a message in the given group. User must belong to the group. Verified by session.
+ */
+router.get("/:groupId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId, messageId} = req.params;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.findOne({_id: groupId, members: userId});
+ // Find the message with the given id in the group with the given id
+ const message = await Message.find({_id: groupId, messageId});
+
+ // Send result
+ res.status(200).json({success: true, data: message});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/messages/:groupId
+ * Method: POST
+ * Auth: Required
+ * Desc: Adds a new chat message to the given group with the given content. User must belong to group. Verified by session.
+ */
+router.post("/:groupId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId} = req.params;
+ const {content} = req.body;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const senderId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.findOne({_id: groupId, members: senderId});
+ // Create a new message with the given content and sender id
+ let newMessage = new Message({groupId, senderId, content, edited: false});
+ // Save the new message to the database
+ newMessage = await newMessage.save();
+
+ // Send result
+ res.status(201).json({success: true, data: newMessage});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/messages/:groupId/:messageId
+ * Method: DELETE
+ * Auth: Required
+ * Desc: Deletes the message with the given id. User must belong to the group and have sent the message or be admin of the group.
+ * Verified by session.
+ */
+router.delete("/:groupId/:messageId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId, messageId} = req.params;
+ const username = getUsername(req);
+
+ try {
+ // Find the the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ const group = await Group.findOne({_id: groupId, members: userId});
+ if (group.adminId === userId) {
+ // If current user is group admin, find and delete the message with the given id from the group with the given id
+ await Message.findOneAndDelete({_id: messageId, groupId: group._id});
+ } else {
+ // Otherwise, find and delete the message with the given id in the given group if the current user was the sender
+ await Message.findOneAndDelete({_id: messageId, groupId: group._id, senderId: userId});
+ }
+
+ // Send result
+ res.status(204).json({success: true});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/messages/:groupId/:messageId
+ * Method: PATCH
+ * Auth: Required
+ * Desc: Updates the content and sets the edited status to true of the message with the given id.
+ * User must belong to group and be the sender of the message. Verified by session.
+ */
+router.patch("/:groupId/:messageId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId, messageId} = req.params;
+ const {content} = req.body;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const senderId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.findOne({_id: groupId, members: senderId});
+ // Find and update content and status of the message with the given id in the given group if the current user was the sender
+ const message = await Message.findOneAndUpdate({_id: messageId, groupId, senderId}, {content, edited: true});
+ // Send result
+ res.status(200).json({success: true, data: message});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+module.exports = {router};
\ No newline at end of file
diff --git a/routes/api/tasks.js b/routes/api/tasks.js
new file mode 100644
index 000000000..1ec005159
--- /dev/null
+++ b/routes/api/tasks.js
@@ -0,0 +1,176 @@
+// @author: Luke Bodwell
+"use strict";
+
+const express = require("express");
+const moment = require("moment");
+
+const Task = require("../../models/Task");
+const Group = require("../../models/Group");
+const User = require("../../models/User");
+const passportConfig = require("../../config/passport-config");
+
+const router = express.Router();
+const {ensureAuthenticated, getUsername} = passportConfig;
+
+/*
+ * Route: /api/tasks/:groupId
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets all tasks in the given group. User must belong to the group. Verified by session.
+ */
+router.get("/:groupId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId} = req.params;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.find({_id: groupId, members: userId});
+ // Find all tasks with the given group id sorted by date sent (descending)
+ const tasks = await Task.find({groupId}).sort({dateCreated: -1});
+
+ // Send result
+ res.status(200).json({success: true, data: tasks});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/tasks/:groupId/:taskId
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets the tasks with the given id in the given group. User must belong to the group. Verified by session.
+ */
+router.get("/:groupId/:taskId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId, taskId} = req.params;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.find({_id: groupId, members: userId});
+ // Find the task with the given id in the group with the given id
+ const task = await Task.findOne({_id: taskId, groupId});
+
+ // Send result
+ res.status(200).json({success: true, data: task});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/tasks/:groupId
+ * Method: POST
+ * Auth: Required
+ * Desc: Adds a new task to the given group with the given information. User must belong to the group. Verified by session.
+ */
+router.post("/:groupId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const groupId = req.params.groupId;
+ const {name, desc, columnName, assignees, tags, dateDue} = req.body;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.findOne({_id: groupId, members: userId});
+ // Create a new task with the given name, description, column name, assignees, tags, and due date
+ console.log("Updated again, trying now with assignees as a string")
+ let newTask = new Task({name, desc, groupId, columnName, assignees, tags, dateDue: formatDate(dateDue)}); //formatDate(dateDue)
+ console.log(newTask)
+ // Save the new task to the database
+ newTask = await newTask.save();
+
+ // Send result
+ res.status(201).json({success: true, data: newTask});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/tasks/:groupId/:taskId
+ * Method: DELETE
+ * Auth: Required
+ * Desc: Deletes the task with the given id. User must belong to the group. Verified by session.
+ */
+router.delete("/:groupId/:taskId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId, taskId} = req.params;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ const group = await Group.findOne({_id: groupId, members: userId});
+ // Find and delete the task with the given id from the group with the given id
+ await Task.findOneAndDelete({_id: taskId, groupId: group._id});
+
+ // Send result
+ res.status(200).json({success: true});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/tasks/:groupId/:taskId
+ * Method: PATCH
+ * Auth: Required
+ * Desc: Updates the information of the task with the given id. User must belong to group and be the sender of the message.
+ * Verified by session.
+ */
+router.patch("/:groupId/:taskId", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const {groupId, taskId} = req.params;
+ console.log("Group id: " + groupId)
+ console.log("Task id: " + taskId)
+ const {name, desc, columnName, assignees, tags, dateDue} = req.body;
+ const username = getUsername(req);
+
+ try {
+ // Find the id of the user with the given username
+ const userId = (await User.findOne({username}))._id;
+ // Verify that a group exists with the given id that the current user is a member of
+ await Group.findOne({_id: groupId, members: userId});
+ // Find the task with the given id in the group with the given id
+ let task = await Task.findOne({_id: taskId, groupId});
+
+ // Update fields based on request body
+ task.name = name ? name : task.name;
+ task.desc = desc ? desc : task.desc;
+ task.columnName = columnName ? columnName : task.columnName
+ task.assignees = assignees ? assignees : task.assignees;
+ task.tags = tags ? tags : task.tags;
+ task.dateDue = dateDue ? formatDate(dateDue) : task.dateDue;
+
+ // Save updated task to database
+ task = await task.save();
+
+ // Send result
+ res.status(200).json({success: true, data: task});
+ } catch (err) {
+ console.log(err)
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+const formatDate = dateStr => {
+ //* This is expecting a date formatted as a string exactly as outputed from a datepicker.
+ return moment(new Date(dateStr)).format("MM/DD/YYYY");
+}
+
+module.exports = {router, formatDate};
diff --git a/routes/api/users.js b/routes/api/users.js
new file mode 100644
index 000000000..e022dd8dd
--- /dev/null
+++ b/routes/api/users.js
@@ -0,0 +1,121 @@
+// @author: Luke Bodwell
+"use strict";
+
+const express = require("express");
+
+const User = require("../../models/User");
+const passportConfig = require("../../config/passport-config");
+
+const router = express.Router();
+const {ensureAuthenticated, getUsername} = passportConfig;
+
+/*
+ * Route: /api/users
+ * Method: GET
+ * Auth: Required
+ * Desc: Gets the current user based on the session.
+ */
+router.get("/", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const username = getUsername(req);
+
+ try {
+ // Find the user with the given username
+ const user = await User.findOne({username});
+
+ // Send result
+ res.status(200).json({success: true, data: user});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/users/all
+ * Method: GET
+ * Auth: Not required
+ * Desc: Gets all users.
+ */
+router.get("/all", async (req, res) => {
+ try {
+ // Finds all registered users
+ const users = await User.find({});
+
+ // Send result
+ res.status(200).json({success: true, data: users});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/users/:id
+ * Method: GET
+ * Auth: Not required
+ * Desc: Gets the user with the given id.
+ */
+router.get("/:id", async (req, res) => {
+ // Gather request parameters
+ const userId = req.params.id;
+
+ try {
+ // Find the user with the given id
+ const user = await User.findOne({_id: userId});
+
+ // Send result
+ res.status(200).json({success: true, data: user});
+ } catch (err) {
+ // Report errors
+ console.log(err)
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/users
+ * Method: POST
+ * Auth: Required
+ * Desc: Adds a new user with the display name and username determined by the session.
+ */
+router.post("/", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const username = getUsername(req);
+ const {displayName} = req.user;
+
+ try {
+ // Create a new user with the given username and display name
+ const newUser = await new User({username, displayName}).save();
+
+ // Send result
+ res.status(201).json({success: true, data: newUser});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+/*
+ * Route: /api/users/
+ * Method: DELETE
+ * Auth: Required
+ * Desc: Deletes the current user determined by the session.
+ */
+router.delete("/", ensureAuthenticated, async (req, res) => {
+ // Gather request parameters
+ const username = getUsername(req);
+
+ try {
+ // Find and delete the user with the given username
+ await User.findOneAndDelete({username});
+
+ // Send result
+ res.status(204).json({success: true});
+ } catch (err) {
+ // Report errors
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+module.exports = {router};
diff --git a/routes/auth/github-auth.js b/routes/auth/github-auth.js
new file mode 100644
index 000000000..115098356
--- /dev/null
+++ b/routes/auth/github-auth.js
@@ -0,0 +1,35 @@
+// @author: Luke Bodwell
+"use strict";
+
+const express = require("express");
+const passport = require("passport");
+
+const User = require("../../models/User");
+const passportConfig = require("../../config/passport-config");
+
+const router = express.Router();
+const {getUsername} = passportConfig;
+
+router.get("/login", passport.authenticate("github", {
+ scope: ["user:email"]
+}));
+
+router.get("/callback", passport.authenticate("github", {
+ failureRedirect: "/login"
+}), async (req, res) => {
+ const username = getUsername(req);
+ const {displayName} = req.user;
+
+ try {
+ const user = await User.findOne({username});
+ if (!user) {
+ const newUser = new User({username, displayName: displayName || username});
+ await newUser.save();
+ }
+ res.redirect("../../");
+ } catch (err) {
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+module.exports = {router};
\ No newline at end of file
diff --git a/routes/auth/google-auth.js b/routes/auth/google-auth.js
new file mode 100644
index 000000000..ca81fecb1
--- /dev/null
+++ b/routes/auth/google-auth.js
@@ -0,0 +1,35 @@
+// @author: Luke Bodwell
+"use strict";
+
+const express = require("express");
+const passport = require("passport");
+
+const User = require("../../models/User");
+const passportConfig = require("../../config/passport-config");
+
+const router = express.Router();
+const {getUsername} = passportConfig;
+
+router.get("/login", passport.authenticate("google", {
+ scope: ["profile"]
+}));
+
+router.get("/callback", passport.authenticate("google", {
+ failureRedirect: "/login"
+}), async (req, res) => {
+ const username = getUsername(req);
+ const {displayName} = req.user;
+
+ try {
+ const user = await User.findOne({username});
+ if (!user) {
+ const newUser = new User({username, displayName});
+ await newUser.save();
+ }
+ res.redirect("../../");
+ } catch (err) {
+ res.status(500).json({success: false, error: err});
+ }
+});
+
+module.exports = {router};
\ No newline at end of file
diff --git a/server.js b/server.js
new file mode 100644
index 000000000..d631d0b6d
--- /dev/null
+++ b/server.js
@@ -0,0 +1,117 @@
+// @author: Luke Bodwell
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+const http = require("http");
+const express = require("express");
+const socketio = require("socket.io");
+const mongoose = require("mongoose");
+const dotenv = require("dotenv");
+const morgan = require("morgan");
+const compression = require("compression");
+const methodOverride = require("method-override");
+const helmet = require("helmet");
+const session = require("express-session");
+const passport = require("passport");
+
+const passportConfig = require("./config/passport-config");
+const githubAuth = require("./routes/auth/github-auth");
+const googleAuth = require("./routes/auth/google-auth");
+const apiRouter = require("./routes/api/api-router");
+
+const app = express();
+const server = http.createServer(app);
+const io = socketio(server);
+const {ensureAuthenticated} = passportConfig;
+
+// Configure environment variables
+dotenv.config();
+const PORT = process.env.PORT || 3000;
+const NODE_ENV = process.env.NODE_ENV;
+const MONGO_URI = process.env.MONGO_URI;
+
+passportConfig.setupPassport();
+
+// Connect to database
+try {
+ mongoose.connect(MONGO_URI, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true
+ }).then(() => console.log("Connected to db"));
+} catch (err) {
+ console.error(err);
+}
+
+// Set up access logging
+if (NODE_ENV === "development") {
+ app.use(morgan("dev"));
+} else if (NODE_ENV === "production") {
+ app.use(morgan("common", {
+ skip: (req, res) => res.statusCode < 400,
+ stream: fs.createWriteStream(path.join(__dirname, "access.log"), {flags: "a"})
+ }));
+}
+
+// Middleware processing
+app.use(helmet({
+ contentSecurityPolicy: false
+}));
+app.use(compression());
+app.use(express.json());
+app.use(methodOverride());
+app.use(session({
+ secret: "itsasecret",
+ resave: false,
+ saveUninitialized: false
+}));
+app.use(passport.initialize());
+app.use(passport.session());
+app.use(express.static(path.join(__dirname, "public")));
+
+// Routing
+app.use("/auth/github", githubAuth.router);
+app.use("/auth/google", googleAuth.router);
+app.use("/api", apiRouter.router);
+
+app.get("/", ensureAuthenticated, (req, res) => {
+ res.sendFile(path.join(__dirname, "public/home.html"));
+});
+app.get("/tasks", ensureAuthenticated, (req, res) => {
+ res.sendFile(path.join(__dirname, "public/tasks.html"));
+});
+app.get("/login", (req, res) => {
+ res.sendFile(path.join(__dirname, "public/login.html"));
+});
+
+app.get("/logout", (req, res) => {
+ req.logout();
+ res.redirect("/login");
+});
+app.get("/account", (req, res) => {
+ if (req.isAuthenticated()) {
+ res.send(`Hello, ${req.user.displayName}!`);
+ } else {
+ res.send("You are not logged in.");
+ }
+});
+
+app.get("*", (req, res) => {
+ res.status(404).send("Error 404. Not found.");
+});
+
+// Web socket handling
+io.on("connection", socket => {
+ console.log("A user has connected");
+
+ socket.on("disconnect", () => {
+ console.log("A user has disconnected");
+ });
+
+ socket.on("chatMessage", (message) => {
+ const {content, sender, date} = message;
+ io.emit("message", {content, sender, date});
+ });
+});
+
+server.listen(PORT, () => console.log(`Listening on port ${PORT}`));