diff --git a/backend/.env-sample b/backend/.env-sample index 9dead41..d0f321e 100644 --- a/backend/.env-sample +++ b/backend/.env-sample @@ -1 +1,5 @@ -MONGODB_URI= \ No newline at end of file +MONGODB_URI= +SIGNATURE_CHAR_LIMIT= +UPDATE_RANKS_CRON= +CHECK_CHAMPION_CRON= +PORT= \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index aa6b205..569d211 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,10 +10,18 @@ "author": "", "license": "ISC", "dependencies": { + "axios": "^1.2.2", + "bcrypt": "^5.1.0", + "bcryptjs": "^2.4.3", "body-parser": "^1.20.1", + "connect-redis": "^6.1.3", "dotenv": "^16.0.3", "express": "^4.18.2", "express-session": "^1.17.3", - "mongoose": "^6.7.3" + "moment": "^2.29.4", + "mongoose": "^6.7.3", + "node-schedule": "^2.1.0", + "redis": "^4.5.1", + "socket.io": "^4.5.4" } } diff --git a/backend/readme.md b/backend/readme.md new file mode 100644 index 0000000..6a2d306 --- /dev/null +++ b/backend/readme.md @@ -0,0 +1,821 @@ +# Unitify Backend + +## Environment +- Ubuntu 20.04 +- MongoDB 5.0.9 +- Nginx 1.18.0 +- Redis 5.0.7 + +## Installation + +Clone the repo. +`git clone https://github.com/VictorS67/unitify.git` + +Switch to the backend branch (for the latest version of backend, or you can get the stable version in the main branch). +`git checkout backend` + +Install all the necsessary node libraries and dependencies. +`npm install` + +Now, you need to set up the necessary environments: + +Run this code first to copy from the .env template: +`cp .env-sample .env` + +``` +MONGODB_URI=YOUR_MONGODB_CONNECTION_STRING +SIGNATURE_CHAR_LIMIT=THE_CHAMPION_SIGNATURE_LENGTH_LIMIT +CHECK_CHAMPION_CRON=CRON_FREQUENCY_OF_CHECKING_CHAMPION +UPDATE_RANKS_CRON=CRON_FREQUENCY_OF_UPDATING_LEARERBOARDT +PORT=PORT_THE_BACKEND_IS_RUNNING +``` + +Now, after the above steps, you can run the backend: +`node unitify-server.js` + +And you can verify by running: +`localhost:PORT/hello-world` + +You should be able to see the following output in your browser: +`Congratulations, the Unitify backend is now running.` + +If you want to run the app in the backend (production), you can run the following command, but make sure that you have PM2 installed in your ubuntu machine (you can use this tutorial: https://www.digitalocean.com/community/tutorials/how-to-use-pm2-to-setup-a-node-js-production-environment-on-an-ubuntu-vps): + +`pm2 start unitify-server.js` + +## Usage + +### GET /user: to get a user object if logged in +#### Parameter +None +#### Responses +200 +When a user has logged in before and has not logged out yet. +``` +{ + "status": 200, + "user": UserObject, + "message": "Returned user object in session." +} +``` +The returned UserObject is a JSON object itself: +``` +{ + "_id": userId, + "username": "username" +} +``` + +404 +When the user has not logged in yet. +``` +{ + "status": 400, + "user": undefined, + "message": "The user has not logged in." +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "user": undefined, + "message": "The Server is down." +} +``` + + +### POST /auth: to login - update the user object in session +#### Request Body +``` +{ + "username": username, + "password": password +} +``` + +#### Responses +200 +When the username exists and the username and password match. +This API will create a userObject in the session, and make `session.loggedin = true`: +``` +req.session.loggedin = true +req.session.user = { +username: username, + _id: user._id +} +Response as follow: +{ + "status": 200, + "user": UserObject, + "message": "Logged in." +} +``` + +#### 404 +When the username doesn’t exist in the database. +``` +{ + "status": 404, + "message": "The user doesn't exist." +} +``` + +#### 400 +When the username exists, but the password is not correct. +``` +{ + "status": 400, + "user": undefined, + "message": "The credentials don't match." +} +``` + +#### 500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### POST /signup: to signup - create a new user +#### Request Body +``` +{ + "username": username, + "email": emailAddress, + "password": password, + "confirmedPassword": confirmedPassword +} +``` +#### Responses +200 +The username doesn’t exist and every input is valid. +``` +{ + "status": 200, + "message": "Sign up successfully." +} +``` +400 +When the username already exists or any input is not valid. +``` +{ + "status": 400, + "message": someErrorMessage +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### DELETE /logout: to logout - delete the session + +#### Request Body +None +Responses +200 +The session will be destroyed (empty). + +Response as follow: +``` +{ + "status": 200, + "message": "Signed out." +} +``` + +400 +When the user is not logged in (the session is empty already). +``` +{ + "status": 400, + "message": "Not logged in." +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### POST /location: to add a new location record in location table + +#### Request Body +``` +{ + "userId": userId, # the userId of the user we are saving location for + "location": LIST(43.6532, -79.3832) # a list of length 2 +} +``` +#### Responses +200 +The userid and location are both valid, and the location is saved to the database. +Response as follow: +``` +{ + "status": 200, + "message": "The location is saved to the database." +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` +400 +The location is not in the right format. +``` +{ + "status": 400, + "message": "Please give the location and make sure it is in the right format." +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### POST /questionnaire: to add a new response to a questionnaire + +#### Request Body +``` +{ + "userId": userId, # the userId of the user we are saving location for + "question": Question Content, + "answer": response to the question +} +``` +#### Responses +200 +The userid is valid, and question and answer are both given as required. +Response as follow: +``` +{ + "status": 200, + "message": "The response to the questionnaire is saved to the database." +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` + +400 +question and answer are not given. +``` +{ + "status": 400, + "message": "Please give the location and make sure it is in the right format." +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### PUT /visitedNumber: to increment the like number of a user being visited +To increase the visitedNumber of a user by their user id. + +#### Request Body +``` +{ + "userId": userId, # the userId of the user + "visitedUserId": the user id which is being visited +} +``` +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "message": "The visited number has been incremented." +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### GET /likeNumber/:userId: get the like number of the current user + +#### Responses +200 +``` +{ + "status": 200, + "data": #LikeNumber +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### PUT /miles: to increase a user’s miles record +It will increase the monthlyMiles and totalMiles of a user. It won’t change the rank in real time. +Cron job to update users ranking based on monthly miles. Define the frequency in .env. +Cron job to clear month miles at the end of the month. + +#### Request Body +``` +{ + "userId": userId, # the userId of the user + "miles": the miles to add +} +``` +#### Responses +200 +The userid is valid, and question and answer are both given as required. +Response as follow: +``` +{ + "status": 200, + "message": "The miles have been added." +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` +### GET /dailyLeaderboard?startIndex=&endIndex=: get leaderboard in a range +It will return a slice of the leaderboard in the memory, given the startIndex and endIndex. +Note that it won’t always return all the ties. +leaderboard.slice(startIndex, endIndex) +By default startIndex = 0, endIndex = 10 +This is the live-time leaderboard. + +#### Request Paramters +``` +{ + "startIndex": the start index of the leaderboard + "endIndex": the end index of the leaderboard +} +``` +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "data": an array of the people in the leaderboard +} +``` +501 +The leaderboard is empty (not available or something is wrong in updating the leaderboard) +``` +{ + "status": 501, + "message": "The leaderboard is not available yet." +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### GET /monthlyLeaderboardmonthlyLeader?startIndex=&endIndex=: get leaderboard in a range +It will return a slice of the leaderboard in the memory, given the startIndex and endIndex. +Note that it won’t always return all the ties. +leaderboard.slice(startIndex, endIndex) +By default startIndex = 0, endIndex = 10 +#### Request Paramters +``` +{ + "startIndex": the start index of the leaderboard + "endIndex": the end index of the leaderboard +} +``` +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "data": an array of the people in the leaderboard +} +``` +501 +The leaderboard is empty (not available or something is wrong in updating the leaderboard) +``` +{ + "status": 501, + "message": "The leaderboard is not available yet." +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### PUT /championSignature: to update a user’s champion signature +#### Request Body +``` +{ + "userId": userId, # the userId of the user + "championSignature": the new champion signature +} +``` +#### Responses +200 +The userid is valid, the champion signature has length between 0 and 100. +Response as follow: +``` +{ + "status": 200, + "message": "The chapion signature has been updated." +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` +400 +The champion signature has length > ENV.SIGNATURE_CHAR_LIMIT or the champion signature is missing. +``` +{ + "status": 400, + "message": "Please don’t write more than ENV.SIGNATURE_CHAR_LIMIT characters." +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### GET /getLastestUserStatus/:userId: get the whole user object +#### Responses +200 +The userid is valid, and question and answer are both given as required. +Response as follow: +``` +{ + "status": 200, + "data": userObject +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + + +### PUT /updateEmail: to update a user’s email +``` +{ + "userId": userId, # the userId of the user + "newEmail": the new email address +} +``` +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "message": "Email updated successfully." +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` +400 +New email is the same as the current one. +Email address is not valid. +``` +{ + "status": 400, + "message": "Corresponding error message” +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### PUT /changePassword: to update a user’s password +#### Request Body +``` +{ + "userId": userId, # the userId of the user + "oldPassword": the current password, + "newPassword": new password, + "confirmedNewPassword": confirmed new password +} +``` +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "message": "Password updated successfully." +} +``` + +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` + +400 +new password is different from the confirmed new password +New password is the same as the old one. +Password doesn’t satisfy the requirement. +Old password is not correct. +``` +{ + "status": 400, + "message": "Corresponding error message” +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### GET /historyMiles/:userId:get the history monthly miles +Optional query values: startMonth, endMonth. Format: yyyy-mm +https://unitify-api.chenpan.ca/historyMiles/639de75e4f6f2dd7793c6f17?startMonth=2022-12 + +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "data": aDataFrame(list) of object +} +``` + +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### PUT /like: to like a user +#### Request Body +``` +{ + "userId": userId, # the userId of the user + "likedUserId": the userid of who is being liked +} +``` + +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "message": "The like is recorded." +} +``` + +400 +If the user has already been liked by the user (which should have been disallowed by the front end) +``` +{ + "status": 400, + "message": "Don't like the same person twice!” +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### PUT /unlike: to unlike a user +#### Request Body +``` +{ + "userId": userId, # the userId of the user + "unlikedUserId": the userid of who is being unliked +} +``` + +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "message": "The unlike is recorded." +} +``` + +400 +If the user is not liked by the current user. +``` +{ + "status": 400, + "message": "You can't unlike a person you are not liking.” +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + +### PUT /notificationToken: to update the notificationToken +#### Request Body +``` +{ + "userId": userId, # the userId of the user + "notificationToken": the new notificationToken +} +``` + +#### Responses +200 +Response as follow: +``` +{ + "status": 200, + "message": "The notificationToken has been updated." +} +``` + +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + + +### GET /getMyStatus/:userId: get the whole user object of the current user +#### Responses: +200 +The userid is valid, and question and answer are both given as required. +Response as follow: +``` +{ + "status": 200, + "data": userObject +} +``` +403 +The userid doesn’t match what’s in the session (or the user is logged in) +``` +{ + "status": 403, + "message": "You don’t have the permission to perform this action." +} +``` +500 +The server runs into some issue. +``` +{ + "status": 500, + "message": "The Server is down." +} +``` + + + + + + + + + diff --git a/backend/schema.js b/backend/schema.js index f865536..c5e9d88 100644 --- a/backend/schema.js +++ b/backend/schema.js @@ -9,128 +9,200 @@ const ObjectId = require('mongodb').ObjectID; // } // ) -const userSchema = new mongoose.Schema( +const userSchema = new mongoose.Schema( { - userName:{ - type:String, - equired: true + userName: { + type: String, + required: true, + unique: true }, - email:{ + email: { type: String, required: true }, pass: { - type: String, + type: String, required: true }, - avatarImage:{ + avatarImage: { // dont know }, - // championSig:{ - // type:String, - // required: true - // }, - // backgroundImage:{ - // //type unknown - // required:false - // }, - // fontSize:{ - // type:Number, - // required:false - // }, - //Palette etcs unkown + dailyMiles: { + type: Number, + default: 0 + }, + monthlyMiles: { + type: Number, + default: 0 + }, + totalMiles: { + type: Number, + default: 0 + }, + likeNumber: { + type: Number, + default: 0 + }, + currentRank: { + type: Number, + default: 0 + }, + championSignature:{ + type:String, + default: "" + }, + championTimes: { + type: Number, + default: 0 + }, + visitedTimes: { + type: Number, + default: 0 + }, + whoLikedMe: { + type: [String], + default: [] + }, + whoILiked: { + type: [String], + default: [] + }, + notificationToken: { + type: String + } + }, + { + timestamps: { + createdAt: 'created_at' + } } ) const rankSchema = new mongoose.Schema( { - userId:{ - type:ObjectId, + userId: { + type: ObjectId, required: true }, //If username unique, we can also save username here - rankNumber:{ - type:Number, + rankNumber: { + type: Number, required: true }, - likeNUmber:{ - type:Number, - required:true - }, - timeStamp:{ - type:Date, - required:true + updatedAt: { + type: Date, + required: true } } ) const mileSchema = new mongoose.Schema( { - userId:{ - type:ObjectId, + userId: { + type: ObjectId, required: true }, //If username unique, we can also save username here - miles:{ - type:Number, - required:true - }, - timeStamp:{ - type:Date, - required:true + miles: { + type: Number, + required: true + } + }, + { + timestamps: { + createdAt: 'created_at', + updatedAt: 'updatedAt' } } ) const transportationSchema = new mongoose.Schema( { - userId:{ - type:ObjectId, + userId: { + type: ObjectId, required: true }, //If username unique, we can also save username here - transportationKind:{ + transportationKind: { type: String, - required:true + required: true }, - GPS:{ - type:String, - required:true + GPS: { + type: String, + required: true }, - speed:{ + speed: { type: Number, required: true }, - timeStamp:{ - type:Date, - required:true - } + timeStamp: { + type: Date, + required: true + } } ) const questionnaireSchema = new mongoose.Schema( { - userId:{ - type:ObjectId, + userId: { + type: ObjectId, required: true }, //If username unique, we can also save username here - question:{ - type:String, - required:true + question: { + type: String, + required: true }, - answer:{ - type:String, - required:true + answer: { + type: String, + required: true }, - timeStamp:{ - type:Date, - required:true + timeStamp: { + type: Date, + required: true } } ) -module.exports = { - userSchema: userSchema + + +// https://mongoosejs.com/docs/geojson.html +const pointSchema = new mongoose.Schema({ + type: { + type: String, + enum: ['Point'], + required: true + }, + coordinates: { + type: [Number], + required: true } - \ No newline at end of file +}); + +const locationSchema = new mongoose.Schema({ + userId: { + type: ObjectId, + required: true + }, + location: { + type: pointSchema, + required: true + } +}, + { + timestamps: { + createdAt: 'created_at', + updatedAt: 'updatedAt' + } + } + +); + +module.exports = { + userSchema: userSchema, + locationSchema: locationSchema, + rankSchema: rankSchema, + mileSchema: mileSchema, + transportationSchema: transportationSchema, + questionnaireSchema: questionnaireSchema +} diff --git a/backend/server.js b/backend/server.js index fef5801..79188b6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,11 +1,16 @@ log = console.log; +const moment = require('moment'); require('dotenv').config(); const express = require('express'); const mongoose = require('mongoose'); const session = require('express-session') const bodyParser = require('body-parser') +const cors = require('cors'); +const schedule = require('node-schedule'); +const ObjectId = require('mongodb').ObjectID; const connectionString = process.env.MONGODB_URI; +const SIGNATURE_CHAR_LIMIT = parseInt(process.env.SIGNATURE_CHAR_LIMIT); let mongoDB = connectionString; mongoose.connect(mongoDB, { useNewUrlParser: true, useUnifiedTopology: true }); let db = mongoose.connection; @@ -16,6 +21,25 @@ const schemas = require('./schema.js'); const e = require('express'); const User = mongoose.model('users', schemas.userSchema, 'users'); +const Location = mongoose.model('locations', schemas.locationSchema, 'locations'); +const Rank = mongoose.model('ranks', schemas.rankSchema, 'ranks'); +const Mile = mongoose.model('miles', schemas.mileSchema, 'miles'); +const Transportation = mongoose.model('transportations', schemas.transportationSchema, 'transportations'); +const Questionnnaire = mongoose.model('questionnaires', schemas.questionnaireSchema, 'questionnaires'); + + +let leaderboard = []; + +let monthlyLeaderboard = []; + +const tables = { + users: User, + locations: Location, + ranks: Rank, + miles: Mile, + transportations: Transportation, + questionnaires: Questionnnaire +} const backend = express(); @@ -33,6 +57,36 @@ backend.use(session({ saveUninitialized: true })) +//backend.use(cors()); +backend.use(cors({ credentials: true, origin: "*" })); + + +/** + * SOCKET PART + */ + +const http = require("http").Server(backend); +const socketIO = require('socket.io')(http, { + cors: { + origin: "*" + } +}); + +socketIO.on('connection', (socket) => { + + + setInterval(function () { + // The Trip Stopped + socket.emit("tripEnded", "Trip Ended"); + // Stopped! + }, 30000); + + socket.on("updateLocation", (location) => { + }); + socket.on('disconnect', () => { + socket.disconnect() + }); +}); /** @@ -41,11 +95,77 @@ backend.use(session({ */ backend.get("/user", (req, res) => { console.log("Received a request to check if logged in.") - if (req.session.loggedin) { - return res.status(200).json({ "status": 200, "user": req.session.user }) + try { + if (req.session.loggedin) { + console.log("Logged in.") + return res.status(200).json({ "status": 200, "user": req.session.user, "message": "Returned user object in session." }) + } + else { + console.log("Not logged in.") + return res.status(404).json({ "status": 404, "user": undefined, "message": "The user has not logged in." }) + } } - else { - return res.status(200).json({ "status": 200, "user": undefined }) + catch { + return res.status(500).json({ "status": 500, "message": "The Server is down." }) + } +}) + + +/** + * Post /signup + * Return a message for the result (sucessful signed up or error messages). + */ +backend.post("/signup", async (req, res) => { + const isValidUsername = (username) => { + return true; + } + + const isUsernameAlreadyIn = async (username) => { + let user = await User.findOne({ userName: username }); + if (user) return false; + else return true; + } + + const isValidEmail = async (email) => { + let re = /\S+@\S+\.\S+/; + return re.test(email); + } + + const isValidPassword = async (email) => { + return /^[A-Za-z]\w{7,14}$/.test(email) + } + + try { + if (!req.body.username || !req.body.email || !req.body.password || !req.body.confirmedPassword) { + return res.status(400).json({ "status": 400, "message": "Please make sure that you give all of the following: username, email, password, confirmed password." }); + } + else if (req.body.password !== req.body.confirmedPassword) { + return res.status(400).json({ "status": 400, "message": "The password and the confirmed password don't match." }); + } + else if (!isValidUsername(req.body.username)) { + return res.status(400).json({ "status": 400, "message": "Please give a valid username (currently no requirements)" }); + } + else if (!(await isUsernameAlreadyIn(req.body.username))) { + return res.status(400).json({ "status": 400, "message": "The username already exists." }); + } + else if (!(await isValidEmail(req.body.email))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid email address." }); + } + else if (!(await isValidPassword(req.body.password))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid password." }); + } + else { + let response = await User.create({ + "userName": req.body.username, + "pass": req.body.password, + "email": req.body.email + }); + return res.status(200).json({ "status": 200, "message": "Sign up successfully." }) + } + } + catch (e) { + console.log(e) + return res.status(500).json({ "status": 500, "message": "The Server is down." }) } }) @@ -78,7 +198,9 @@ backend.post('/auth', async (req, res) => { username: username, _id: user._id } - return res.status(200).json({ 'status': 200, 'message': `Logged in.` }) + + console.log("password correct") + return res.status(200).json({ 'status': 200, 'message': `Logged in.`, 'user': req.session.user }); } } @@ -95,9 +217,9 @@ backend.post('/auth', async (req, res) => { * Logout * Will delete the session */ - backend.delete('/logout', async (req, res) => { +backend.delete('/logout', async (req, res) => { try { - if(req.session.user) { + if (req.session.user) { req.session.destroy(); return res.status(200).json({ 'status': 200, 'message': 'Signed out.' }) } @@ -111,13 +233,588 @@ backend.post('/auth', async (req, res) => { } }) +const getAuth = function (req, res, next) { + let userId = undefined; + if (req.body.userId) userId = req.body.userId; + else userId = req.params.userId; + if (req.session.loggedin && req.session.user && req.session.user._id == userId) { + next(); + } + else { + return res.status(403).json({ 'status': 403, 'message': `You don't have the permission to perform this action.` }) + } +} + +backend.post('/location', getAuth, async (req, res) => { + try { + if (!req.body.location) { + res.status(400).json({ 'status': 400, 'message': "Please give the location and make sure it is in the right format." }); + } + else { + let response = await Location.create({ + "userId": req.body.userId, + "location": location + }); + return res.status(200).json({ 'status': 200, 'data': response }); + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + +backend.post('/questionnaire', getAuth, async (req, res) => { + try { + if (!req.body.question || !req.body.answer) { + res.status(400).json({ 'status': 400, 'message': "Please give both the question and the answer." }); + } + else { + let response = await Questionnnaire.create({ + "userId": req.body.userId, + "question": req.body.question, + "answer": req.body.answer + }); + return res.status(200).json({ 'status': 200, 'message': "The response to the questionnaire is saved to the database." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + +backend.put('/likeNumber', getAuth, async (req, res) => { + try { + let response = await User.findOneAndUpdate({ _id: req.body.likedUserId }, + { $inc: { 'likeNumber': 1 } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The like number has been incremented." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.put('/visitedNumber', getAuth, async (req, res) => { + try { + let response = await User.findOneAndUpdate({ _id: req.body.visitedUserId }, + { $inc: { 'visitedNumber': 1 } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The visited number has been incremented." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.get('/likeNumber/:userId', getAuth, async (req, res) => { + try { + let response = await User.findOne({ _id: req.params.userId }); + return res.status(200).json({ 'status': 200, data: response.likeNumber }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.get('/getLastestUserStatus/:userId', async (req, res) => { + try { + let response = await User.findOne({ _id: req.params.userId }).select(["-pass"]); + response.likeNumber = response.whoLikedMe ? response.whoLikedMe.length: 0; + return res.status(200).json({ 'status': 200, data: response }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.post('/getLatestUsersStatus', async (req, res) => { + try { + let response = await User.find({ _id: {$in: req.body.userIds} }).select(["-pass"]); + response.forEach(function(obj){ + obj.likeNumber = obj.whoLikedMe ? obj.whoLikedMe.length: 0; + }); + return res.status(200).json({ 'status': 200, data: response }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.get('/getMyStatus/:userId', getAuth, async (req, res) => { + try { + let response = await User.findOne({ _id: {$in: req.params.userId} }).select(["-pass"]); + return res.status(200).json({ 'status': 200, data: response }); + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +/** + * Get /collection + * Get Collection + * Will return the whole document based on the filter + */ + +// TODO: all need proper error handling, permission check, e.t.c. +backend.get('/collection', async (req, res) => { + try { + let tableName = req.body.tableName; + let Table = tables[tableName]; + let response = await Table.findOne(req.body.filter); + return res.status(200).json({ 'status': 200, 'data': response }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + + +backend.post('/collection', async (req, res) => { + try { + let tableName = req.body.tableName; + let Table = tables[tableName]; + let response = await Table.create(req.body.newDoc); + return res.status(200).json({ 'status': 200, 'data': response }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + +backend.put('/miles', getAuth, async (req, res) => { + try { + if (!req.body.miles) { + res.status(400).json({ 'status': 400, 'message': "Please give the miles to add." }); + } + else { + let response = await User.findOneAndUpdate({ _id: req.body.userId }, + { $inc: { 'monthlyMiles': req.body.miles, 'totalMiles': req.body.miles, 'dailyMiles': req.body.miles } }, + { + new: true + }); + + await Mile.create({ + userId: req.body.userId, + miles: req.body.miles + }) + return res.status(200).json({ 'status': 200, 'message': "The miles have been added." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.get('/dailyLeaderboard', async (req, res) => { + try { + if (leaderboard.length === 0) { + res.status(500).json({ 'status': 501, 'message': "The leaderboard is not available yet." }); + } + else { + let startIndex = req.query.startIndex ? req.query.startIndex : 0; + let endIndex = req.query.endIndex ? req.query.endIndex : 10; + + let slicedLeaderboard = leaderboard.slice(startIndex, endIndex); + let champions = slicedLeaderboard.filter(function (el) { + return el.rankNumber === 1; + }) + let nonchampions = slicedLeaderboard.filter(function (el) { + return el.rankNumber !== 1; + }); + return res.status(200).json({ 'status': 200, 'data': leaderboard.slice(startIndex, endIndex), 'champions': champions, 'nonchampions': nonchampions }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.get('/monthlyLeaderboard', async (req, res) => { + try { + if (monthlyLeaderboard.length === 0) { + res.status(500).json({ 'status': 501, 'message': "The monthly leaderboard is not available yet." }); + } + else { + let startIndex = req.query.startIndex ? req.query.startIndex : 0; + let endIndex = req.query.endIndex ? req.query.endIndex : 10; + + //console.log(users) + let slicedLeaderboard = monthlyLeaderboard.slice(startIndex, endIndex); + let champions = slicedLeaderboard.filter(function (el) { + return el.rankNumber === 1; + }) + let nonchampions = slicedLeaderboard.filter(function (el) { + return el.rankNumber !== 1; + }); + return res.status(200).json({ 'status': 200, 'data': monthlyLeaderboard.slice(startIndex, endIndex), 'champions': champions, 'nonchampions': nonchampions }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + + +backend.put('/championSignature', getAuth, async (req, res) => { + try { + if (!req.body.championSignature || req.body.championSignature.length > SIGNATURE_CHAR_LIMIT) { + res.status(400).json({ 'status': 400, 'message': `Please give the champion signature, and make sure there are no more than ${SIGNATURE_CHAR_LIMIT} characters.` }); + } + else { + let response = await User.findOneAndUpdate({ _id: req.body.userId }, + { $set: { 'championSignature': req.body.championSignature } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The champion signature has been updated." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + +// To update user's information. +backend.put("/changePassword", getAuth, async (req, res) => { + const oldPasswordCorrect = async (userId, oldPassword) => { + let user = await User.findOne({ + _id: userId, + pass: oldPassword + }); + if (user !== null) return true; + else return false; + } + + const isValidPassword = async (email) => { + return /^[A-Za-z]\w{7,14}$/.test(email) + } + + try { + if (!req.body.oldPassword || !req.body.newPassword || !req.body.confirmedNewPassword) { + return res.status(400).json({ "status": 400, "message": "Please make sure that you give all of the following: oldPassword, and confirmedNewPassword." }); + } + else if (req.body.newPassword !== req.body.confirmedNewPassword) { + return res.status(400).json({ "status": 400, "message": "The new password and the confirmed new password don't match." }); + } + else if (!(await oldPasswordCorrect(req.body.userId, req.body.oldPassword))) { + return res.status(400).json({ "status": 400, "message": "Please double check if your old password is correct or not." }); + } + else if (!(await isValidPassword(req.body.newPassword))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid password." }); + } + else if (req.body.newPassword === req.body.oldPassword) { + return res.status(400).json({ "status": 400, "message": "Your new password is the same as the current one." }); + } + else { + let response = await User.findOneAndUpdate({ + _id: req.body.userId + }, { + pass: req.body.newPassword + }); + return res.status(200).json({ "status": 200, "message": "Password updated successfully." }) + } + } + catch { + return res.status(500).json({ "status": 500, "message": "The Server is down." }) + } +}) + + + +// To update user's email. +backend.put("/updateEmail", getAuth, async (req, res) => { + const isValidEmail = async (email) => { + let re = /\S+@\S+\.\S+/; + return re.test(email); + } + const isEmailAlreadyExistForTheUser = async (userId, email) => { + let user = await User.findOne({ + _id: userId, + email: email + }); + + if (user !== null) return true; + else return false; + } + + try { + if (!req.body.newEmail) { + return res.status(400).json({ "status": 400, "message": "Please make sure that you give all of the following: newEmail." }); + } + else if (!(await isValidEmail(req.body.newEmail))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid email address." }); + } + else if ((await isEmailAlreadyExistForTheUser(req.body.userId, req.body.newEmail))) { + return res.status(400).json({ "status": 400, "message": "The email is same as the current one." }); + } + else { + let response = await User.findOneAndUpdate({ + _id: req.body.userId + }, { + email: req.body.newEmail + }); + return res.status(200).json({ "status": 200, "message": "Email updated successfully." }) + } + } + catch { + return res.status(500).json({ "status": 500, "message": "The Server is down." }) + } +}) + + + + +backend.get('/historyMiles/:userId', getAuth, async (req, res) => { + // Date needs to be yyyy-mm-dd format. + try { + let userId = req.params.userId; + let miles = [] + if (req.query.startMonth && req.query.endMonth) { + + let startMonthInUTC = moment.utc(moment(req.query.startMonth)).format() + let endMonthInUTC = moment.utc(moment(req.query.endMonth)).format() + miles = await Mile.find({ + userId: userId, + created_at: { + $gte: startMonthInUTC, + $lte: endMonthInUTC + } + }) + } + else if (req.query.startMonth) { + let startMonthInUTC = moment.utc(moment(req.query.startMonth)).format() + miles = await Mile.find({ + userId: userId, + created_at: { + $gte: startMonthInUTC + } + }) + } + else if (req.query.endMonth) { + let endMonthInUTC = moment.utc(moment(req.query.endMonth)).format() + miles = await Mile.find({ + userId: userId, + created_at: { + $lte: endMonthInUTC + } + }) + } + else { + miles = await Mile.find({ + userId: userId + }) + } + + let cleanedMiles = [] + for (let mile of miles) { + cleanedMiles.push({ + value: mile.miles, + month: moment(mile.created_at).local().format('YYYYMM') + }) + } + + let groupedSums = {} + for (let mile of cleanedMiles) { + if (groupedSums[mile.month] === undefined) groupedSums[mile.month] = 0 + groupedSums[mile.month] += mile.value + } + + let result = [] + for (const [key, value] of Object.entries(groupedSums)) { + result.push({ + "month": key, + "miles": value + }) + } + + + return res.status(200).json({ "status": 200, "message": "The requested history monthly miles returned.", "data": result }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + + + +backend.put('/like', getAuth, async (req, res) => { + try { + let currentUser = await User.findOne({ + "_id": req.body.userId + }); + if(currentUser.whoILiked.includes(req.body.likedUserId)) { + return res.status(400).json({ 'status': 400, 'message': "Don't like the same person twice!" }) + } + else { + await User.findOneAndUpdate({ _id: req.body.userId }, + { $push: { whoILiked: req.body.likedUserId } }, + { + new: true + }); + + await User.findOneAndUpdate({ _id: req.body.likedUserId }, + { $push: { whoLikedMe: req.body.userId } }, + { + new: true + }); + } + + return res.status(200).json({ 'status': 200, 'message': "The like is recorded." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + +backend.put('/unlike', getAuth, async (req, res) => { + try { + let currentUser = await User.findOne({ + "_id": req.body.userId + }); + if(!currentUser.whoILiked.includes(req.body.unlikedUserId)) { + return res.status(400).json({ 'status': 400, 'message': "You can't unlike a person you are not liking." }) + } + else { + await User.findOneAndUpdate({ _id: req.body.userId }, + { $pull: { whoILiked: req.body.unlikedUserId } }, + { + new: true + }); + + await User.findOneAndUpdate({ _id: req.body.unlikedUserId }, + { $pull: { whoLikedMe: req.body.userId } }, + { + new: true + }); + } + + return res.status(200).json({ 'status': 200, 'message': "The unlike is recorded." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.put('/notificationToken', getAuth, async (req, res) => { + try { + if (!req.body.notificationToken) { + res.status(400).json({ 'status': 400, 'message': `Please give the notificationToken.` }); + } + else { + let response = await User.findOneAndUpdate({ _id: req.body.userId }, + { $set: { 'notificationToken': req.body.notificationToken } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The notificationToken has been updated." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +const updateLeaderboard = async () => { + let updatedAt = new Date(Date.now()).toISOString(); + const usersWithRanks = await User.aggregate([ + { + $setWindowFields: { + partitionBy: "$state", + sortBy: { monthlyMiles: -1 }, + output: { + rankNumber: { + $rank: {} + } + } + } + }, + { $project: { _id: 0, userId: "$_id", rankNumber: 1, updatedAt: updatedAt, userName: "$userName", monthlyMiles: 1, dailyMiles: 1 } }, // TODO: add the other required attributes + ]) + try { + let response = await Rank.insertMany(usersWithRanks); + + for (let i = 0; i < usersWithRanks.length; i++) { + if (usersWithRanks[i].rankNumber === 1) { + let user = await User.findOne({ _id: usersWithRanks[i].userId }); + usersWithRanks[i].championSignature = user.championSignature; + } + } + leaderboard = usersWithRanks; + // TODO: remove daily miles once pushed to the leaderboard. + } + catch (error) { + console.log(error) + } +} + + +const checkChampions = async () => { + monthlyLeaderboard = leaderboard; + let champions = leaderboard.filter(function (el) { + return el.rankNumber === 1 + }); + + let championIds = champions.map(function (el) { + return el.userId + }) + + await User.updateMany({ _id: championIds }, + { $inc: { 'championTimes': 1 } }, + { + new: true + }); + // TODO: remove daily miles once pushed to the leaderboard. +} +const job = schedule.scheduleJob(process.env.UPDATE_RANKS_CRON, function () { + updateLeaderboard(); +}); +const job2 = schedule.scheduleJob(process.env.CHECK_CHAMPION_CRON, function () { + checkChampions(); +}); +updateLeaderboard(); const port = process.env.PORT || 43030 -backend.listen(port, () => { +http.listen(port, () => { log(`Listening on port ${port}...`) }) diff --git a/backend/unitify-server.js b/backend/unitify-server.js new file mode 100644 index 0000000..b90a61c --- /dev/null +++ b/backend/unitify-server.js @@ -0,0 +1,945 @@ +log = console.log; +const moment = require('moment'); +require('dotenv').config(); +const express = require('express'); +const mongoose = require('mongoose'); +const session = require('express-session') +const bodyParser = require('body-parser') +const cors = require('cors'); +const schedule = require('node-schedule'); +const ObjectId = require('mongodb').ObjectID; +const Bcrypt = require("bcryptjs") + +var axios = require('axios'); +const connectionString = process.env.MONGODB_URI; +const SIGNATURE_CHAR_LIMIT = parseInt(process.env.SIGNATURE_CHAR_LIMIT); +let mongoDB = connectionString; +mongoose.connect(mongoDB, { useNewUrlParser: true, useUnifiedTopology: true }); +let db = mongoose.connection; +db.on('error', console.error.bind(console, 'MongoDB connection error:')); + + +const schemas = require('./schema.js'); +const e = require('express'); + +const User = mongoose.model('users', schemas.userSchema, 'users'); +const Location = mongoose.model('locations', schemas.locationSchema, 'locations'); +const Rank = mongoose.model('ranks', schemas.rankSchema, 'ranks'); +const Mile = mongoose.model('miles', schemas.mileSchema, 'miles'); +const Transportation = mongoose.model('transportations', schemas.transportationSchema, 'transportations'); +const Questionnnaire = mongoose.model('questionnaires', schemas.questionnaireSchema, 'questionnaires'); +const fs = require('fs'); + + +let leaderboard = []; + +fs.readFile('./leaderboard.json', 'utf8', (error, data) => { + if(error){ + console.log(error); + return; + } + leaderboard = JSON.parse(data); +}) + +let monthlyLeaderboard = []; + +const tables = { + users: User, + locations: Location, + ranks: Rank, + miles: Mile, + transportations: Transportation, + questionnaires: Questionnnaire +} + +const backend = express(); + + +backend.use(bodyParser.urlencoded({ + extended: true +})) + + +backend.use(bodyParser.json()) + +// backend.use(session({ +// secret: 'secret', +// resave: true, +// saveUninitialized: true +// })) + + +let RedisStore = require("connect-redis")(session) + +// redis@v4 +const { createClient } = require("redis") +let redisClient = createClient({ legacyMode: true }) +redisClient.connect().catch(console.error) + +backend.use( + session({ + store: new RedisStore({ client: redisClient }), + saveUninitialized: false, + secret: "keyboard cat", + resave: false, + }) +) + +//backend.use(cors()); +backend.use(cors({ credentials: true, origin: "*" })); + + +/** + * SOCKET PART + */ + +const http = require("http").Server(backend); +const socketIO = require('socket.io')(http, { + cors: { + origin: "*" + } +}); + +socketIO.on('connection', (socket) => { + + + setInterval(function () { + // The Trip Stopped + socket.emit("tripEnded", "Trip Ended"); + // Stopped! + }, 30000); + + socket.on("updateLocation", (location) => { + }); + socket.on('disconnect', () => { + socket.disconnect() + }); +}); + + +/** + * Get /user + * Return the user object if the user logins in. Otherwise return undefined. + */ +backend.get("/user", (req, res) => { + console.log("Received a request to check if logged in.") + try { + if (req.session.loggedin) { + console.log("Logged in.") + return res.status(200).json({ "status": 200, "user": req.session.user, "message": "Returned user object in session." }) + } + else { + console.log("Not logged in.") + return res.status(404).json({ "status": 404, "user": undefined, "message": "The user has not logged in." }) + } + } + catch { + return res.status(500).json({ "status": 500, "message": "The Server is down." }) + } +}) + + +/** + * Post /signup + * Return a message for the result (sucessful signed up or error messages). + */ +backend.post("/signup", async (req, res) => { + const isValidUsername = (username) => { + return true; + } + + const isUsernameAlreadyIn = async (username) => { + let user = await User.findOne({ userName: username }); + if (user) return false; + else return true; + } + + const isValidEmail = async (email) => { + let re = /\S+@\S+\.\S+/; + return re.test(email); + } + + const isValidPassword = async (email) => { + return /^[A-Za-z]\w{7,14}$/.test(email) + } + + try { + if (!req.body.username || !req.body.email || !req.body.password || !req.body.confirmedPassword) { + return res.status(400).json({ "status": 400, "message": "Please make sure that you give all of the following: username, email, password, confirmed password." }); + } + else if (req.body.password !== req.body.confirmedPassword) { + return res.status(400).json({ "status": 400, "message": "The password and the confirmed password don't match." }); + } + else if (!isValidUsername(req.body.username)) { + return res.status(400).json({ "status": 400, "message": "Please give a valid username (currently no requirements)" }); + } + else if (!(await isUsernameAlreadyIn(req.body.username))) { + return res.status(400).json({ "status": 400, "message": "The username already exists." }); + } + else if (!(await isValidEmail(req.body.email))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid email address." }); + } + else if (!(await isValidPassword(req.body.password))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid password." }); + } + else { + let response = await User.create({ + "userName": req.body.username, + "pass": Bcrypt.hashSync(req.body.password, 10), + "email": req.body.email + }); + return res.status(200).json({ "status": 200, "message": "Sign up successfully." }) + } + } + catch (e) { + console.log(e) + return res.status(500).json({ "status": 500, "message": "The Server is down." }) + } +}) + + + +/** + * POST /auth + * Login Authedication. + * Will set up user object in the session if the user exists and the credentials match. + * The Request Body includes username & password. + */ +backend.post('/auth', async (req, res) => { + try { + + + const username = req.body.username; + const password = req.body.password; + let user = await User.findOne({ userName: username }); + if (user === null) { + return res.status(404).json({ 'status': 404, 'message': `The user doesn't exist.` }); + } + else { + if (!Bcrypt.compareSync(password, user.pass)) { + return res.status(400).json({ 'status': 400, 'message': `The credentials don't match.` }); + } + else { + // The credentials match. + req.session.loggedin = true + req.session.user = { + username: username, + _id: user._id + } + + console.log("password correct") + return res.status(200).json({ 'status': 200, 'message': `Logged in.`, 'user': req.session.user }); + } + + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +/** + * DELETE /logout + * Logout + * Will delete the session + */ +backend.delete('/logout', async (req, res) => { + try { + if (req.session.user) { + req.session.destroy(); + return res.status(200).json({ 'status': 200, 'message': 'Signed out.' }) + } + else { + return res.status(400).json({ 'status': 400, 'message': 'Not logged in.' }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + +const getAuth = function (req, res, next) { + let userId = undefined; + if (req.body.userId) userId = req.body.userId; + else userId = req.params.userId; + if (req.session.loggedin && req.session.user && req.session.user._id == userId) { + next(); + } + else { + return res.status(403).json({ 'status': 403, 'message': `You don't have the permission to perform this action.` }) + } +} + +backend.post('/location', getAuth, async (req, res) => { + try { + if (!req.body.location) { + res.status(400).json({ 'status': 400, 'message': "Please give the location and make sure it is in the right format." }); + } + else { + const location = { type: 'Point', coordinates: req.body.location }; + let response = await Location.create({ + "userId": req.body.userId, + "location":location + }); + return res.status(200).json({ 'status': 200, 'data': response }); + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + +backend.post('/questionnaire', getAuth, async (req, res) => { + try { + if (!req.body.question || !req.body.answer) { + res.status(400).json({ 'status': 400, 'message': "Please give both the question and the answer." }); + } + else { + let response = await Questionnnaire.create({ + "userId": req.body.userId, + "question": req.body.question, + "answer": req.body.answer + }); + return res.status(200).json({ 'status': 200, 'message': "The response to the questionnaire is saved to the database." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + +backend.put('/likeNumber', getAuth, async (req, res) => { + try { + let response = await User.findOneAndUpdate({ _id: req.body.likedUserId }, + { $inc: { 'likeNumber': 1 } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The like number has been incremented." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.put('/visitedNumber', getAuth, async (req, res) => { + try { + let response = await User.findOneAndUpdate({ _id: req.body.visitedUserId }, + { $inc: { 'visitedNumber': 1 } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The visited number has been incremented." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.get('/likeNumber/:userId', getAuth, async (req, res) => { + try { + let response = await User.findOne({ _id: req.params.userId }); + return res.status(200).json({ 'status': 200, data: response.likeNumber }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.get('/getLastestUserStatus/:userId', async (req, res) => { + try { + let response = await User.findOne({ _id: req.params.userId }).select(["-pass"]); + response.likeNumber = response.whoLikedMe ? response.whoLikedMe.length : 0; + return res.status(200).json({ 'status': 200, data: response }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.post('/getLatestUsersStatus', async (req, res) => { + try { + let response = await User.find({ _id: { $in: req.body.userIds } }).select(["-pass"]); + response.forEach(function (obj) { + obj.likeNumber = obj.whoLikedMe ? obj.whoLikedMe.length : 0; + }); + + let result = [] + for(let userId of req.body.userIds) { + let object = response.find(user => user._id == userId); + result.push(object); + } + return res.status(200).json({ 'status': 200, data: result }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.get('/getMyStatus/:userId', getAuth, async (req, res) => { + try { + let response = await User.findOne({ _id: { $in: req.params.userId } }).select(["-pass"]); + return res.status(200).json({ 'status': 200, data: response }); + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +/** + * Get /collection + * Get Collection + * Will return the whole document based on the filter + */ + +// TODO: all need proper error handling, permission check, e.t.c. +backend.get('/collection', async (req, res) => { + try { + let tableName = req.body.tableName; + let Table = tables[tableName]; + let response = await Table.findOne(req.body.filter); + return res.status(200).json({ 'status': 200, 'data': response }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + + +backend.post('/collection', async (req, res) => { + try { + let tableName = req.body.tableName; + let Table = tables[tableName]; + let response = await Table.create(req.body.newDoc); + return res.status(200).json({ 'status': 200, 'data': response }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + +backend.put('/miles', getAuth, async (req, res) => { + try { + if (!req.body.miles) { + res.status(400).json({ 'status': 400, 'message': "Please give the miles to add." }); + } + else { + let response = await User.findOneAndUpdate({ _id: req.body.userId }, + { $inc: { 'monthlyMiles': Math.round(req.body.miles), 'totalMiles': Math.round(req.body.miles), 'dailyMiles': Math.round(req.body.miles) } }, + { + new: true + }); + + await Mile.create({ + userId: req.body.userId, + miles: Math.round(req.body.miles) + }) + return res.status(200).json({ 'status': 200, 'message': "The miles have been added." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.get('/dailyLeaderboard', async (req, res) => { + try { + if (leaderboard.length === 0) { + res.status(500).json({ 'status': 501, 'message': "The leaderboard is not available yet." }); + } + else { + let startIndex = req.query.startIndex ? req.query.startIndex : 0; + let endIndex = req.query.endIndex ? req.query.endIndex : 10; + + let slicedLeaderboard = leaderboard.slice(startIndex, endIndex); + let champions = slicedLeaderboard.filter(function (el) { + return el.rankNumber === 1; + }) + let nonchampions = slicedLeaderboard.filter(function (el) { + return el.rankNumber !== 1; + }); + return res.status(200).json({ 'status': 200, 'data': leaderboard.slice(startIndex, endIndex), 'champions': champions, 'nonchampions': nonchampions }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + +backend.get('/monthlyLeaderboard', async (req, res) => { + try { + if (monthlyLeaderboard.length === 0) { + res.status(500).json({ 'status': 501, 'message': "The monthly leaderboard is not available yet." }); + } + else { + let startIndex = req.query.startIndex ? req.query.startIndex : 0; + let endIndex = req.query.endIndex ? req.query.endIndex : 10; + + //console.log(users) + let slicedLeaderboard = monthlyLeaderboard.slice(startIndex, endIndex); + let champions = slicedLeaderboard.filter(function (el) { + return el.rankNumber === 1; + }) + let nonchampions = slicedLeaderboard.filter(function (el) { + return el.rankNumber !== 1; + }); + return res.status(200).json({ 'status': 200, 'data': monthlyLeaderboard.slice(startIndex, endIndex), 'champions': champions, 'nonchampions': nonchampions }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) + + + +backend.put('/championSignature', getAuth, async (req, res) => { + try { + if (!req.body.championSignature || req.body.championSignature.length > SIGNATURE_CHAR_LIMIT) { + res.status(400).json({ 'status': 400, 'message': `Please give the champion signature, and make sure there are no more than ${SIGNATURE_CHAR_LIMIT} characters.` }); + } + else { + let response = await User.findOneAndUpdate({ _id: req.body.userId }, + { $set: { 'championSignature': req.body.championSignature } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The champion signature has been updated." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + +// To update user's information. +backend.put("/changePassword", getAuth, async (req, res) => { + const oldPasswordCorrect = async (userId, oldPassword) => { + let user = await User.findOne({ + _id: userId + }); + if (user !== null) { + return Bcrypt.compareSync(user.pass, oldPassword); + } + else { + return false + }; + } + + const isValidPassword = async (email) => { + return /^[A-Za-z]\w{7,14}$/.test(email) + } + + try { + if (!req.body.oldPassword || !req.body.newPassword || !req.body.confirmedNewPassword) { + return res.status(400).json({ "status": 400, "message": "Please make sure that you give all of the following: oldPassword, and confirmedNewPassword." }); + } + else if (req.body.newPassword !== req.body.confirmedNewPassword) { + return res.status(400).json({ "status": 400, "message": "The new password and the confirmed new password don't match." }); + } + else if (!(await oldPasswordCorrect(req.body.userId, req.body.oldPassword))) { + return res.status(400).json({ "status": 400, "message": "Please double check if your old password is correct or not." }); + } + else if (!(await isValidPassword(req.body.newPassword))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid password." }); + } + else if (req.body.newPassword === req.body.oldPassword) { + return res.status(400).json({ "status": 400, "message": "Your new password is the same as the current one." }); + } + else { + let response = await User.findOneAndUpdate({ + _id: req.body.userId + }, { + pass: Bcrypt.hashSync(req.body.newPassword) + }); + return res.status(200).json({ "status": 200, "message": "Password updated successfully." }) + } + } + catch { + return res.status(500).json({ "status": 500, "message": "The Server is down." }) + } +}) + + + +// To update user's email. +backend.put("/updateEmail", getAuth, async (req, res) => { + const isValidEmail = async (email) => { + let re = /\S+@\S+\.\S+/; + return re.test(email); + } + const isEmailAlreadyExistForTheUser = async (userId, email) => { + let user = await User.findOne({ + _id: userId, + email: email + }); + + if (user !== null) return true; + else return false; + } + + try { + if (!req.body.newEmail) { + return res.status(400).json({ "status": 400, "message": "Please make sure that you give all of the following: newEmail." }); + } + else if (!(await isValidEmail(req.body.newEmail))) { + return res.status(400).json({ "status": 400, "message": "Please give a valid email address." }); + } + else if ((await isEmailAlreadyExistForTheUser(req.body.userId, req.body.newEmail))) { + return res.status(400).json({ "status": 400, "message": "The email is same as the current one." }); + } + else { + let response = await User.findOneAndUpdate({ + _id: req.body.userId + }, { + email: req.body.newEmail + }); + return res.status(200).json({ "status": 200, "message": "Email updated successfully." }) + } + } + catch { + return res.status(500).json({ "status": 500, "message": "The Server is down." }) + } +}) + + + + +backend.get('/historyMiles/:userId', getAuth, async (req, res) => { + // Date needs to be yyyy-mm-dd format. + try { + let userId = req.params.userId; + let miles = [] + if (req.query.startMonth && req.query.endMonth) { + + let startMonthInUTC = moment.utc(moment(req.query.startMonth)).format() + let endMonthInUTC = moment.utc(moment(req.query.endMonth)).format() + miles = await Mile.find({ + userId: userId, + created_at: { + $gte: startMonthInUTC, + $lte: endMonthInUTC + } + }) + } + else if (req.query.startMonth) { + let startMonthInUTC = moment.utc(moment(req.query.startMonth)).format() + miles = await Mile.find({ + userId: userId, + created_at: { + $gte: startMonthInUTC + } + }) + } + else if (req.query.endMonth) { + let endMonthInUTC = moment.utc(moment(req.query.endMonth)).format() + miles = await Mile.find({ + userId: userId, + created_at: { + $lte: endMonthInUTC + } + }) + } + else { + miles = await Mile.find({ + userId: userId + }) + } + + let cleanedMiles = [] + for (let mile of miles) { + cleanedMiles.push({ + value: mile.miles, + month: moment(mile.created_at).local().format('YYYYMM') + }) + } + + let groupedSums = {} + for (let mile of cleanedMiles) { + if (groupedSums[mile.month] === undefined) groupedSums[mile.month] = 0 + groupedSums[mile.month] += mile.value + } + + let result = [] + for (const [key, value] of Object.entries(groupedSums)) { + result.push({ + "month": key, + "miles": value + }) + } + + + return res.status(200).json({ "status": 200, "message": "The requested history monthly miles returned.", "data": result }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + + + +backend.put('/like', getAuth, async (req, res) => { + try { + let currentUser = await User.findOne({ + "_id": req.body.userId + }); + if (currentUser.whoILiked.includes(req.body.likedUserId)) { + return res.status(400).json({ 'status': 400, 'message': "Don't like the same person twice!" }) + } + else { + await User.findOneAndUpdate({ _id: req.body.userId }, + { $push: { whoILiked: req.body.likedUserId } }, + { + new: true + }); + + await User.findOneAndUpdate({ _id: req.body.likedUserId }, + { $push: { whoLikedMe: req.body.userId } }, + { + new: true + }); + } + + return res.status(200).json({ 'status': 200, 'message': "The like is recorded." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + +backend.put('/unlike', getAuth, async (req, res) => { + try { + let currentUser = await User.findOne({ + "_id": req.body.userId + }); + if (!currentUser.whoILiked.includes(req.body.unlikedUserId)) { + return res.status(400).json({ 'status': 400, 'message': "You can't unlike a person you are not liking." }) + } + else { + await User.findOneAndUpdate({ _id: req.body.userId }, + { $pull: { whoILiked: req.body.unlikedUserId } }, + { + new: true + }); + + await User.findOneAndUpdate({ _id: req.body.unlikedUserId }, + { $pull: { whoLikedMe: req.body.userId } }, + { + new: true + }); + } + + return res.status(200).json({ 'status': 200, 'message': "The unlike is recorded." }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + +backend.put('/notificationToken', getAuth, async (req, res) => { + try { + if (!req.body.notificationToken) { + res.status(400).json({ 'status': 400, 'message': `Please give the notificationToken.` }); + } + else { + console.log("Token from frontend received"); + console.log(req.body.notificationToken) + let response = await User.findOneAndUpdate({ _id: req.body.userId }, + { $set: { 'notificationToken': req.body.notificationToken } }, + { + new: true + }); + return res.status(200).json({ 'status': 200, 'message': "The notificationToken has been updated." }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + + + + +backend.get('/spamTony', async (req, res) => { + // Date needs to be yyyy-mm-dd format. + try { + let user = await User.findOne({ + "userName": "Tony" + }); + if (user.notificationToken === undefined) return res.status(200).json({ 'status': 200, 'message': 'Token not given.' }); + console.log(user.notificationToken) + var data = JSON.stringify({ + "to": user.notificationToken, + "title": "Welcome to Unitify 🚲", + "sound": "default", + "body": `Welcome, Tony! ♻️` + }); + + var config = { + method: 'post', + url: 'https://exp.host/--/api/v2/push/send', + headers: { + 'Content-Type': 'application/json' + }, + data: data + }; + + let response = await axios(config); + console.log(response) + + return res.status(200).json({ 'status': 200, 'message': 'sent a message' }) + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}); + + + +const updateLeaderboard = async () => { + let updatedAt = new Date(Date.now()).toISOString(); + const usersWithRanks = await User.aggregate([ + { + $setWindowFields: { + partitionBy: "$state", + sortBy: { monthlyMiles: -1 }, + output: { + rankNumber: { + $rank: {} + } + } + } + }, + { $project: { _id: 0, userId: "$_id", rankNumber: 1, updatedAt: updatedAt, userName: "$userName", monthlyMiles: 1, dailyMiles: 1 } }, // TODO: add the other required attributes + ]) + try { + let response = await Rank.insertMany(usersWithRanks); + + for (let i = 0; i < usersWithRanks.length; i++) { + if (usersWithRanks[i].rankNumber === 1) { + let user = await User.findOne({ _id: usersWithRanks[i].userId }); + usersWithRanks[i].championSignature = user.championSignature; + } + } + leaderboard = usersWithRanks; + // TODO: remove daily miles once pushed to the leaderboard. + // await User.updateMany({}, { "$set": { "dailyMiles": 0 } }); + + await User.bulkWrite(usersWithRanks.map( function(p) { + return { updateOne:{ + filter: {_id: p.userId}, + update: {$set: {currentRank: p.rankNumber, dailyMiles: 0}} + }} + })) + + let json = JSON.stringify(leaderboard); + fs.writeFile ("leaderboard.json", json, function(err) { + if (err) throw err; + console.log('complete'); + } + ); + } + catch (error) { + console.log(error) + } +} + + +const checkChampions = async () => { + monthlyLeaderboard = leaderboard; + let champions = leaderboard.filter(function (el) { + return el.rankNumber === 1 + }); + + let championIds = champions.map(function (el) { + return el.userId + }) + + let users = await User.find({}); + for (let user of users) { + if (user.notificationToken === undefined) continue; + var data = JSON.stringify({ + "to": user.notificationToken, + "title": "Hi", + "sound": "default", + "body": `Hello world🥂` + }); + + var config = { + method: 'post', + url: 'https://exp.host/--/api/v2/push/send', + headers: { + 'Content-Type': 'application/json' + }, + data: data + }; + + await axios(config); + } + + await User.updateMany({ _id: championIds }, + { $inc: { 'championTimes': 1 } }, + { + new: true + }); +} + + +const job = schedule.scheduleJob(process.env.UPDATE_RANKS_CRON, function () { + updateLeaderboard(); +}); + +const job2 = schedule.scheduleJob(process.env.CHECK_CHAMPION_CRON, function () { + checkChampions(); +}); + + +const port = process.env.PORT || 43030 +http.listen(port, () => { + log(`Listening on port ${port}...`) +}) + +if (process.env.NODE_ENV === 'production') { + // Exprees will serve up production assets + backend.use(express.static('client/build')); + + // Express serve up index.html file if it doesn't recognize route + const path = require('path'); + backend.get('*', (req, res) => { + res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html')); + }); +} \ No newline at end of file