From aea2c1004e8e7526fd7e1b406d4ad897b2f79dcc Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Sat, 3 Dec 2022 15:04:36 -0500 Subject: [PATCH 1/9] api for locations --- backend/schema.js | 133 +++++++++++++++++++++------------- backend/server.js | 181 ++++++++++++++++++++++++++++++---------------- 2 files changed, 202 insertions(+), 112 deletions(-) diff --git a/backend/schema.js b/backend/schema.js index f865536..bf10b1f 100644 --- a/backend/schema.js +++ b/backend/schema.js @@ -9,21 +9,21 @@ const ObjectId = require('mongodb').ObjectID; // } // ) -const userSchema = new mongoose.Schema( +const userSchema = new mongoose.Schema( { - userName:{ - type:String, + userName: { + type: String, equired: true }, - email:{ + email: { type: String, required: true }, pass: { - type: String, + type: String, required: true }, - avatarImage:{ + avatarImage: { // dont know }, // championSig:{ @@ -44,93 +44,126 @@ const userSchema = new mongoose.Schema( 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 + likeNUmber: { + type: Number, + required: true }, - timeStamp:{ - type:Date, - required:true + timeStamp: { + 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 + miles: { + type: Number, + required: true }, - timeStamp:{ - type:Date, - required:true + timeStamp: { + type: Date, + required: true } } ) 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', + } + } + +); + +module.exports = { + userSchema: userSchema, + locationSchema: locationSchema +} diff --git a/backend/server.js b/backend/server.js index fef5801..31726ed 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,6 +4,7 @@ const express = require('express'); const mongoose = require('mongoose'); const session = require('express-session') const bodyParser = require('body-parser') +const cors = require('cors'); const connectionString = process.env.MONGODB_URI; let mongoDB = connectionString; @@ -16,6 +17,7 @@ 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 backend = express(); @@ -33,84 +35,139 @@ backend.use(session({ saveUninitialized: true })) +backend.use(cors()); + +// Authedication APIs +{ + + /** + * 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 }) + } + else { + console.log("Not logged in.") + return res.status(404).json({ "status": 404, "user": undefined }) + } + } + catch { + return res.status(500).json({ "status": 500, "message": "Try again later." }) + } + }) -/** - * 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.") - if (req.session.loggedin) { - return res.status(200).json({ "status": 200, "user": req.session.user }) - } - else { - return res.status(200).json({ "status": 200, "user": undefined }) - } -}) - + /** + * 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 { -/** - * 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 (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 }); + } - 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 (password !== user.pass) { - return res.status(400).json({ 'status': 400, 'message': `The credentials don't match.` }); + 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 { - // The credentials match. - req.session.loggedin = true - req.session.user = { - username: username, - _id: user._id - } - return res.status(200).json({ 'status': 200, 'message': `Logged in.` }) + 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.' }) - } -}) + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } + }) +} + + +// Map related APIs. +{ + /** + * POST /location + * Login Add a location record. + */ + backend.post('/location', async (req, res) => { + try { + + // TODO: check if the userId in the session is the same as what's given in the body. + // TODO: check if the userId exists. + let userId = req.body.userId; + let coordinates = req.body.coordinates; + + let point = { type: 'Point', coordinates: coordinates }; + let newLocation = await Location.create({ + userId: userId, + location: point + }); + if(newLocation) { + return res.status(200).json({ 'status': 200, 'message': 'The location is recorded successfully.' }) + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } + }) +} + + + /** - * DELETE /logout - * Logout - * Will delete the session + * Test if the server is on or not. */ - 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.' }) - } -}) +backend.get('/hello', async (req, res) => { + return res.status(200).json({ 'status': 200, 'message': 'The server has started.' }) +}) From d1952f8a90fe57a7db1a3e1cd82035f74214bedc Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Sat, 3 Dec 2022 15:06:55 -0500 Subject: [PATCH 2/9] api for locations --- backend/schema.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/schema.js b/backend/schema.js index bf10b1f..e932a89 100644 --- a/backend/schema.js +++ b/backend/schema.js @@ -158,6 +158,7 @@ const locationSchema = new mongoose.Schema({ { timestamps: { createdAt: 'created_at', + updatedAt: 'updatedAt' } } From b29632400d98b5ebc0d058fe20f34c01e26e3412 Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Thu, 8 Dec 2022 19:28:07 -0500 Subject: [PATCH 3/9] some update from server.js --- backend/server.js | 188 ++++++++++++++++++---------------------------- 1 file changed, 72 insertions(+), 116 deletions(-) diff --git a/backend/server.js b/backend/server.js index 31726ed..b069840 100644 --- a/backend/server.js +++ b/backend/server.js @@ -17,7 +17,6 @@ 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 backend = express(); @@ -35,144 +34,101 @@ backend.use(session({ saveUninitialized: true })) -backend.use(cors()); - -// Authedication APIs -{ - - /** - * 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 }) - } - else { - console.log("Not logged in.") - return res.status(404).json({ "status": 404, "user": undefined }) - } +//backend.use(cors()); +backend.use(cors({ credentials: true, origin: "http://localhost:19006" })); + + + +/** + * 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 }) } - catch { - return res.status(500).json({ "status": 500, "message": "Try again later." }) + else { + console.log("Not logged in.") + return res.status(404).json({ "status": 404, "user": undefined }) } - }) + } + catch { + return res.status(500).json({ "status": 500, "message": "Try again later." }) + } +}) - /** - * 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 { +/** + * 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.` }); + 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 (password !== user.pass) { + return res.status(400).json({ 'status': 400, 'message': `The credentials don't match.` }); } else { - if (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 }); + // 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.' }) - } - }) -} - - -// Map related APIs. -{ - /** - * POST /location - * Login Add a location record. - */ - backend.post('/location', async (req, res) => { - try { - - // TODO: check if the userId in the session is the same as what's given in the body. - // TODO: check if the userId exists. - let userId = req.body.userId; - let coordinates = req.body.coordinates; - - let point = { type: 'Point', coordinates: coordinates }; - let newLocation = await Location.create({ - userId: userId, - location: point - }); - if(newLocation) { - return res.status(200).json({ 'status': 200, 'message': 'The location is recorded successfully.' }) - } - } - catch (error) { - console.log(error) - return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) - } - }) -} - - + } + } + catch (error) { + console.log(error) + return res.status(500).json({ 'status': 500, 'message': 'The server is down.' }) + } +}) /** - * Test if the server is on or not. + * DELETE /logout + * Logout + * Will delete the session */ - -backend.get('/hello', async (req, res) => { - return res.status(200).json({ 'status': 200, 'message': 'The server has started.' }) +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 port = process.env.PORT || 43030 backend.listen(port, () => { log(`Listening on port ${port}...`) From bc629c67955059814d7d1a592eb9456aea8b5e13 Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Thu, 8 Dec 2022 20:15:15 -0500 Subject: [PATCH 4/9] socket on server --- backend/package.json | 3 ++- backend/server.js | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/backend/package.json b/backend/package.json index aa6b205..30cea10 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,6 +14,7 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-session": "^1.17.3", - "mongoose": "^6.7.3" + "mongoose": "^6.7.3", + "socket.io": "^4.5.4" } } diff --git a/backend/server.js b/backend/server.js index b069840..5df11e6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -35,9 +35,38 @@ backend.use(session({ })) //backend.use(cors()); -backend.use(cors({ credentials: true, origin: "http://localhost:19006" })); +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) => { + + console.log(`${socket.id}`); + + setInterval(function () { + // The Trip Stopped + console.log(Date.now()) + }, 1000); + + socket.on("updateLocation", (location) => { + console.log(`${location}`) + //socket.emit("roomsList", chatRooms); + }); + socket.on('disconnect', () => { + socket.disconnect() + }); +}); + /** * Get /user @@ -56,7 +85,7 @@ backend.get("/user", (req, res) => { } } catch { - return res.status(500).json({ "status": 500, "message": "Try again later." }) + return res.status(500).json({ "status": 500, "message": "Try again later." }) } }) @@ -91,7 +120,7 @@ backend.post('/auth', async (req, res) => { } console.log("password correct") - return res.status(200).json({ 'status': 200, 'message': `Logged in.`, 'user': req.session.user}); + return res.status(200).json({ 'status': 200, 'message': `Logged in.`, 'user': req.session.user }); } } From dbce30187892b2ed09d5745a6e7229d57b754e81 Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Thu, 8 Dec 2022 20:34:54 -0500 Subject: [PATCH 5/9] socket server --- backend/server.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/server.js b/backend/server.js index 5df11e6..24654cd 100644 --- a/backend/server.js +++ b/backend/server.js @@ -51,16 +51,17 @@ const socketIO = require('socket.io')(http, { socketIO.on('connection', (socket) => { - console.log(`${socket.id}`); + console.log(`${socket.id}: ${socket.handshake.query.user_id}`); setInterval(function () { // The Trip Stopped console.log(Date.now()) - }, 1000); + socket.emit("tripEnded", "Trip Ended"); + // Stopped! + }, 30000); socket.on("updateLocation", (location) => { - console.log(`${location}`) - //socket.emit("roomsList", chatRooms); + console.log(`${socket.handshake.query.user_id} is in ${location}.`) }); socket.on('disconnect', () => { socket.disconnect() @@ -159,7 +160,7 @@ backend.delete('/logout', async (req, res) => { const port = process.env.PORT || 43030 -backend.listen(port, () => { +http.listen(port, () => { log(`Listening on port ${port}...`) }) From 91a2e167ca237a93879299edb04f78dff37ca5c7 Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Sat, 10 Dec 2022 15:45:22 -0500 Subject: [PATCH 6/9] some update --- backend/schema.js | 6 +++++- backend/server.js | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/backend/schema.js b/backend/schema.js index e932a89..73abde7 100644 --- a/backend/schema.js +++ b/backend/schema.js @@ -166,5 +166,9 @@ const locationSchema = new mongoose.Schema({ module.exports = { userSchema: userSchema, - locationSchema: locationSchema + locationSchema: locationSchema, + rankSchema: rankSchema, + mileSchema: mileSchema, + transportationSchema: transportationSchema, + questionnaireSchema: questionnaireSchema } diff --git a/backend/server.js b/backend/server.js index 24654cd..9d5a67e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -17,6 +17,20 @@ 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 tables = { + users: User, + locations: Location, + ranks: Rank, + miles: Mile, + transportations: Transportation, + questionnaires: Questionnnaire +} const backend = express(); @@ -155,6 +169,40 @@ backend.delete('/logout', async (req, res) => { }) +/** + * 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.' }) + } +}) From 38ee3977ebf4ba7cd00f4c011f27c94114081312 Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Fri, 6 Jan 2023 09:32:51 -0500 Subject: [PATCH 7/9] big backend update --- backend/.env-sample | 3 +- backend/package.json | 2 + backend/schema.js | 81 ++++-- backend/server.js | 622 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 673 insertions(+), 35 deletions(-) diff --git a/backend/.env-sample b/backend/.env-sample index 9dead41..38dca13 100644 --- a/backend/.env-sample +++ b/backend/.env-sample @@ -1 +1,2 @@ -MONGODB_URI= \ No newline at end of file +MONGODB_URI= +SIGNATURE_CHAR_LIMIT= \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 30cea10..d6c2f03 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,7 +14,9 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "express-session": "^1.17.3", + "moment": "^2.29.4", "mongoose": "^6.7.3", + "node-schedule": "^2.1.0", "socket.io": "^4.5.4" } } diff --git a/backend/schema.js b/backend/schema.js index 73abde7..2761636 100644 --- a/backend/schema.js +++ b/backend/schema.js @@ -13,7 +13,8 @@ const userSchema = new mongoose.Schema( { userName: { type: String, - equired: true + required: true, + unique: true }, email: { type: String, @@ -26,19 +27,49 @@ const userSchema = new mongoose.Schema( 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 + } } ) @@ -53,11 +84,7 @@ const rankSchema = new mongoose.Schema( type: Number, required: true }, - likeNUmber: { - type: Number, - required: true - }, - timeStamp: { + updatedAt: { type: Date, required: true } @@ -74,10 +101,12 @@ const mileSchema = new mongoose.Schema( miles: { type: Number, required: true - }, - timeStamp: { - type: Date, - required: true + } + }, + { + timestamps: { + createdAt: 'created_at', + updatedAt: 'updatedAt' } } ) @@ -166,9 +195,9 @@ const locationSchema = new mongoose.Schema({ module.exports = { userSchema: userSchema, - locationSchema: locationSchema, + locationSchema: locationSchema, rankSchema: rankSchema, - mileSchema: mileSchema, - transportationSchema: transportationSchema, + mileSchema: mileSchema, + transportationSchema: transportationSchema, questionnaireSchema: questionnaireSchema } diff --git a/backend/server.js b/backend/server.js index 9d5a67e..79188b6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,12 +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; @@ -23,12 +27,17 @@ 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, + ranks: Rank, miles: Mile, - transportations: Transportation, + transportations: Transportation, questionnaires: Questionnnaire } @@ -65,17 +74,14 @@ const socketIO = require('socket.io')(http, { socketIO.on('connection', (socket) => { - console.log(`${socket.id}: ${socket.handshake.query.user_id}`); setInterval(function () { // The Trip Stopped - console.log(Date.now()) socket.emit("tripEnded", "Trip Ended"); // Stopped! }, 30000); socket.on("updateLocation", (location) => { - console.log(`${socket.handshake.query.user_id} is in ${location}.`) }); socket.on('disconnect', () => { socket.disconnect() @@ -92,15 +98,74 @@ backend.get("/user", (req, res) => { try { if (req.session.loggedin) { console.log("Logged in.") - return res.status(200).json({ "status": 200, "user": req.session.user }) + 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 }) + 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": "Try again later." }) + 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." }) } }) @@ -168,6 +233,140 @@ backend.delete('/logout', 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 @@ -202,10 +401,417 @@ backend.post('/collection', async (req, res) => { 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 http.listen(port, () => { From 842c9372db5ebf88ce0aa6450674b2ec5ebb5da2 Mon Sep 17 00:00:00 2001 From: ChenPanXYZ Date: Thu, 12 Jan 2023 21:50:50 -0500 Subject: [PATCH 8/9] backend update --- backend/.env-sample | 5 +- backend/package.json | 5 + backend/readme.md | 821 +++++++++++++++++++++++++++++++++ backend/schema.js | 5 + backend/unitify-server.js | 945 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 1780 insertions(+), 1 deletion(-) create mode 100644 backend/readme.md create mode 100644 backend/unitify-server.js diff --git a/backend/.env-sample b/backend/.env-sample index 38dca13..d0f321e 100644 --- a/backend/.env-sample +++ b/backend/.env-sample @@ -1,2 +1,5 @@ MONGODB_URI= -SIGNATURE_CHAR_LIMIT= \ No newline at end of file +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 d6c2f03..569d211 100644 --- a/backend/package.json +++ b/backend/package.json @@ -10,13 +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", "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..5e51822 --- /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. +`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 2761636..c5e9d88 100644 --- a/backend/schema.js +++ b/backend/schema.js @@ -70,6 +70,11 @@ const userSchema = new mongoose.Schema( notificationToken: { type: String } + }, + { + timestamps: { + createdAt: 'created_at' + } } ) 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 From 2507b49a878ff1dc6ea76f076e6fe9e97d5f9416 Mon Sep 17 00:00:00 2001 From: Pan Chen <35253672+ChenPanXYZ@users.noreply.github.com> Date: Thu, 12 Jan 2023 23:27:49 -0500 Subject: [PATCH 9/9] Update readme.md --- backend/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/readme.md b/backend/readme.md index 5e51822..6a2d306 100644 --- a/backend/readme.md +++ b/backend/readme.md @@ -11,7 +11,7 @@ Clone the repo. `git clone https://github.com/VictorS67/unitify.git` -Switch to the backend branch. +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.