Add files via upload
This commit is contained in:
72
api/README.md
Normal file
72
api/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# SENG 513 - UI / API
|
||||
|
||||
The following System Requirements and Installation steps are required for running the project locally. <br>
|
||||
If you are accessing the project remotely ([vivideradicator.ca](http://www.vivideradicator.ca)), you may skip to the Usage section of this document. <br>
|
||||
|
||||
## System Requirements
|
||||
Ensure that the following are already installed on your operating system of choice. <br>
|
||||
> Node JS <br>
|
||||
> NPM <br>
|
||||
> MongoDB
|
||||
|
||||
## Installation
|
||||
User Interface
|
||||
1. Clone or download the code found within the GitHub SENG513-UI repository, to a path/of/your/choosing <br>
|
||||
1b. This can be done from within VS Code, via `git clone https://github.com/Rafael1321/SENG513-UI.git` <br>
|
||||
2. Using the command prompt, `cd ~path/of/your/choosing/SENG513-UI` will navigate you to the proper directory <br>
|
||||
3. Install all required project dependencies using `npm i` <br>
|
||||
4. Run the application with `npm start` <br>
|
||||
</br>
|
||||
|
||||
API <br>
|
||||
Note that you must be running MongoDB before beginning this step. <br>
|
||||
Instructions can be found at https://www.mongodb.com/docs/manual/installation/ <br>
|
||||
1. Clone or download the code found within the GitHub SENG513-API repository, to a path/of/your/choosing <br>
|
||||
1b. This can be done from within VS Code, via `git clone https://github.com/Rafael1321/SENG513-API.git` <br>
|
||||
2. Using the command prompt, `cd ~path/of/your/choosing/SENG513-API` will navigate you to the proper directory <br>
|
||||
3. Install all required project dependencies using `npm i` <br>
|
||||
4. Run the application with `npm start` <br>
|
||||
|
||||
## Usage
|
||||
The following steps can be used to experience the full functionality of the application. </br>
|
||||
|
||||
1. You will be greeted by the login/account registration page <br>
|
||||
1b. If you do not yet have an account on DuoFinder, feel free to create one now. The 2 starter accounts below may also be used <br>
|
||||
DO NOT use real password information here, it is not yet encrypted <br>
|
||||
1c. Profile 1: email = vivideradicator@gmail.com password = seng513grading <br>
|
||||
1d. Profile 2: email = me@vivideradicator.ca password = seng513grading <br>
|
||||
2. Enter your email address and passwords into their respective fields <br>
|
||||
3. Once logged in, the profile card can be seen. Clicking the edit button in the top right will enable customizing of your profile <br>
|
||||
3b. You may change your profile picture, bio, gender, and age. Customize your profile as desired, and save the changes by pressing the same edit button again <br>
|
||||
3c. Set your desired filters by pushing the 'Chat Filters' button, which will limit who you can match with based on your wishes <br>
|
||||
4. To connect with another user, press the 'Find Duo' button <br>
|
||||
4b. If you are wishing to test the functionality by yourself, simply log into another account on a seperate window and search for a match on both accounts <br>
|
||||
5. Once connected, you may send messages in real-time to the other user. Pushing the 'Share Contact' button will automatically send your in-game information to the other user <br>
|
||||
5b. Move onto another connection with the "Next" button, or return to your profile page by clicking the 'x' in the top-right corner <br>
|
||||
6. Once back on your profile page, view your past matches with the 'Chat History' button <br>
|
||||
6b. You may re-read past messages and give a rating to anyone you have talked to previously. To give a rating, press the top-right 'Rate Player' button <br>
|
||||
7. Enjoy!
|
||||
|
||||
## Version History
|
||||
*v1.0* <br>
|
||||
Front-end only. Focus on creating a visually appealing and easy-to-use UI. <br>
|
||||
Use-state-based system, where changes were not saved and lost upon refreshing/exiting the app. <br>
|
||||
<br>
|
||||
*v2.0* </br>
|
||||
Introduced save states and a backend. User information now saved. <br>
|
||||
*v2.1* </br>
|
||||
Real-time data share between users made possible with sockets. <br>
|
||||
<br>
|
||||
*v3.0* <br>
|
||||
Polishing of the app, squashing of several major bugs. <br>
|
||||
Small UI adjustments, ensuring all systems work as they should. <br>
|
||||
<br>
|
||||
|
||||
|
||||
## Contributors and Credit
|
||||
Gaganjot Brar <br>
|
||||
Harkamal Randhawa <br>
|
||||
Martha Ibarra <br>
|
||||
Rafael Flores Souza <br>
|
||||
Richard Gingrich <br>
|
||||
Tyler Chen
|
||||
45
api/controllers/chats.js
Normal file
45
api/controllers/chats.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const chats = require("../models/chat");
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports.saveMessage = async (req, res) => {
|
||||
try{
|
||||
const {senderId, receiverId, message} = req.body;
|
||||
|
||||
if(!senderId) return res.type('json').status(400).send("Sender id was not provided.");
|
||||
if(!receiverId) return res.type('json').status(400).send("Receiver id was not provided.");
|
||||
if(!message) return res.type('json').status(400).send("Message was not provided");
|
||||
|
||||
const newChat = new chats({
|
||||
senderId:senderId,
|
||||
receiverId:receiverId,
|
||||
message:message
|
||||
});
|
||||
|
||||
await newChat.save();
|
||||
|
||||
return res.type('json').status(201).send(newChat);
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.retrieveMessages = async (req, res) => {
|
||||
try{
|
||||
const {senderId, receiverId} = req.body;
|
||||
|
||||
if(!senderId) return res.type('json').status(400).send("Sender id was not provided.");
|
||||
if(!receiverId) return res.type('json').status(400).send("Receiver id was not provided.");
|
||||
|
||||
const messages = [];
|
||||
const cursor = chats.find({$or : [{$and:[{senderId:senderId}, {receiverId:receiverId}]}, {$and:[{senderId:receiverId}, {receiverId:senderId}]}]}).sort("timestamp").cursor();
|
||||
for (let message = await cursor.next(); message != null; message = await cursor.next()) {
|
||||
messages.push({senderId:message.senderId, receiverId:message.receiverId, message:message.message});
|
||||
}
|
||||
|
||||
return res.type('json').status(200).send(messages);
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
}
|
||||
45
api/controllers/commendations.js
Normal file
45
api/controllers/commendations.js
Normal file
@@ -0,0 +1,45 @@
|
||||
const commendation = require("../models/commendation");
|
||||
const user = require("../models/user");
|
||||
|
||||
module.exports.saveCommendation = async (req, res) => {
|
||||
try{
|
||||
const {commenderId, commendedId, score} = req.body;
|
||||
|
||||
if(!commenderId) return res.type('json').status(400).send("Commender id was not provided.");
|
||||
if(!commendedId) return res.type('json').status(400).send("Commended id was not provided.");
|
||||
if(!score) return res.type('json').status(400).send("Score was not provided.");
|
||||
if(score <= 0 || score > 10) return res.type('json').status(400).send("Score must be >= 1 and <= 10");
|
||||
|
||||
// Make sure the person is not commending again
|
||||
let foundCommendation = await commendation.findOne({$and: [{commendedId:commendedId}, {commenderId:commenderId}]}).exec();
|
||||
if(foundCommendation) return res.type('json').status(400).send("You have already commended this user.");
|
||||
|
||||
// Compute average reputation of commended person
|
||||
let totalReputation = score, totalNumOfCommends = 1, newReputation = 0;
|
||||
const cursor = commendation.find({commendedId:commendedId}).cursor();
|
||||
for (let commendation = await cursor.next(); commendation != null; commendation = await cursor.next()) {
|
||||
totalReputation += commendation.score;
|
||||
totalNumOfCommends += 1;
|
||||
}
|
||||
newReputation = Math.round(totalReputation/totalNumOfCommends);
|
||||
|
||||
// Save New Commendation
|
||||
await new commendation({
|
||||
commendedId: commendedId,
|
||||
commenderId: commenderId,
|
||||
score: score
|
||||
}).save();
|
||||
|
||||
// Update users reputation
|
||||
const updatedUser = await user.findOneAndUpdate({_id: commendedId},
|
||||
{reputation:newReputation},
|
||||
{returnOriginal: false});
|
||||
|
||||
if(!updatedUser) return res.type('json').status(400).send("Commended user reputation could not be updated.");
|
||||
|
||||
return res.type('json').status(200).send();
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
};
|
||||
61
api/controllers/filters.js
Normal file
61
api/controllers/filters.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const filter = require("../models/filter");
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports.retrieve = async (req, res) => {
|
||||
|
||||
try{
|
||||
const userId = req.params.userId;
|
||||
|
||||
// Basic Validation
|
||||
if(!userId) return res.type('json').status(400).send('The user id is missing');
|
||||
|
||||
// Check if the filters exist
|
||||
var searchedFilters = await filter.findOne({userId:userId}).exec();
|
||||
|
||||
if (!searchedFilters){
|
||||
return res.type('json').status(404).send("Filters for specified user were not found.");
|
||||
}else{
|
||||
return res.type('json').status(200).send(searchedFilters);
|
||||
}
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.upsert = async(req, res) => {
|
||||
|
||||
try{
|
||||
|
||||
const {userId, filters} = req.body;
|
||||
|
||||
// Basic Validation
|
||||
if(!userId) return res.type('json').status(400).send('The user id is missing');
|
||||
|
||||
// Check if the filters exist
|
||||
var searchedFilters = await filter.findOne({userId:userId}).exec();
|
||||
|
||||
if(searchedFilters){ // Update the filters
|
||||
|
||||
if(!filters) return res.type('json').status(400).send('The filters are is missing');
|
||||
|
||||
const updatedFilters = await filter.findOneAndUpdate({userId:userId},
|
||||
{...filters},
|
||||
{returnOriginal: false});
|
||||
return res.type('json').status(200).send(updatedFilters);
|
||||
|
||||
}else{ // Insert new filters [with default values]
|
||||
|
||||
const newFilters = new filter({
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
await newFilters.save();
|
||||
|
||||
return res.type('json').status(201).send(newFilters);
|
||||
}
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
}
|
||||
29
api/controllers/matchings.js
Normal file
29
api/controllers/matchings.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const matching = require("../models/matching");
|
||||
const user = require("../models/user");
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports.retrieveMatchHistory = async (req, res) => {
|
||||
|
||||
try{
|
||||
const userId = req.params.userId;
|
||||
|
||||
// Basic Validation
|
||||
if(!userId) return res.type('json').status(400).send('The user id is missing');
|
||||
|
||||
// Check if the filters exist
|
||||
const userList = [];
|
||||
const cursor = matching.find({ $or:[ {'firstUser': userId}, {'secondUser': userId} ]}).sort('timestamp').cursor();
|
||||
for (let match = await cursor.next(); match != null; match = await cursor.next()) {
|
||||
if(match.firstUser !== userId){
|
||||
userList.push(await user.findOne({_id:match.firstUser}).exec());
|
||||
}else if(match.secondUser !== userId){
|
||||
userList.push(await user.findOne({_id:match.secondUser}).exec());
|
||||
}
|
||||
}
|
||||
|
||||
return res.type('json').status(200).send(userList);
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
}
|
||||
188
api/controllers/users.js
Normal file
188
api/controllers/users.js
Normal file
@@ -0,0 +1,188 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const user = require("../models/user");
|
||||
const _ = require('lodash');
|
||||
const axios = require('axios');
|
||||
|
||||
module.exports.registerUser = async (req, res) => {
|
||||
try{
|
||||
const{displayName, gameName, tagLine, email, password, avatarImage} = req.body;
|
||||
|
||||
// Validation of body variables
|
||||
if(!gameName) return res.type('json').status(404).send('The game name is missing');
|
||||
if(!tagLine) return res.type('json').status(404).send('The tag line is missing');
|
||||
if(!email) return res.type('json').status(404).send('The email is missing');
|
||||
if(!password) return res.type('json').status(404).send('The password is missing');
|
||||
|
||||
let searchedUser = await user.findOne({gameName:gameName, tagLine:tagLine}).exec();
|
||||
if(searchedUser) return res.type('json').status(400).send("Game name and tagline combination already in use.");
|
||||
|
||||
// Getting user's extra info from riot's api
|
||||
const baseUrl1 = process.env.RIOT_BASE_URL1;
|
||||
const fullUrl1 = `${baseUrl1}${gameName}\\${tagLine}`
|
||||
|
||||
let response = await axios.get(fullUrl1);
|
||||
var extraData = response.data.data;
|
||||
|
||||
// Getting user's rank from riot's api
|
||||
const baseUrl2 = process.env.RIOT_BASE_URL2;
|
||||
const fullUrl2 = `${baseUrl2}${extraData.region}\\${gameName}\\${tagLine}`;
|
||||
|
||||
let response2 = await axios.get(fullUrl2);
|
||||
let rank = parseRank(response2.data.data.currenttierpatched);
|
||||
|
||||
const newUser = new user({
|
||||
riotId: extraData.puuid,
|
||||
displayName: !displayName?gameName:displayName,
|
||||
gameName: gameName,
|
||||
tagLine: tagLine,
|
||||
email: email,
|
||||
password: await bcrypt.hash(password, await bcrypt.genSalt(10)),
|
||||
avatarImage: avatarImage,
|
||||
rank: [rank.rankType, rank.rankLevel],
|
||||
accountLevel: extraData.account_level,
|
||||
region: toRegionNo(extraData.region)
|
||||
});
|
||||
|
||||
await newUser.save();
|
||||
|
||||
return res.type('json').status(201).send( _.pick(newUser, ['_id', 'riotId', 'displayName', 'gameName','tagLine', 'email', 'avatarImage', 'rank', 'accountLevel', 'region', 'age', 'gender', 'reputation', 'playerType', 'aboutMe']));
|
||||
|
||||
}catch(err){
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.loginUser = async (req, res) => {
|
||||
try{
|
||||
const {email, password} = req.body;
|
||||
|
||||
if(!email) return res.type('json').status(404).send('The username is missing');
|
||||
if(!password) return res.type('json').status(404).send('The password is missing');
|
||||
|
||||
// Check if the username exists
|
||||
var searchedUser = await user.findOne({email:email}).select("+password").exec();
|
||||
|
||||
if (!searchedUser){
|
||||
return res.type('json').status(404).send("User was not found.");
|
||||
}else{
|
||||
// Check if the passwords match
|
||||
const isPasswordValid = await bcrypt.compare(password, searchedUser.password);
|
||||
if(!isPasswordValid){
|
||||
return res.type('json').status(404).send("The password is incorrect.");
|
||||
}
|
||||
return res.type('json').status(200).send(_.pick(searchedUser, ['_id', 'riotId', 'displayName', 'gameName','tagLine','email', 'avatarImage', 'rank', 'accountLevel', 'region', 'age', 'gender', 'reputation', 'playerType', 'aboutMe']));
|
||||
}
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.updateUser = async (req, res) => {
|
||||
try{
|
||||
const updateDTO = req.body;
|
||||
|
||||
if(!updateDTO.userId) return res.type('json').status(400).send('A user id must be provided');
|
||||
|
||||
// Check if the username exists
|
||||
var searchedUser = await user.findOne({_id:updateDTO.userId}).exec();
|
||||
|
||||
if (!searchedUser){
|
||||
return res.type('json').status(404).send("User to update was not found.");
|
||||
}else{
|
||||
|
||||
let updateParams = _.pick(updateDTO, ['displayName', 'age', 'gender', 'playerType', 'aboutMe', 'avatarImage']);
|
||||
const updatedUser = await user.findOneAndUpdate({_id:updateDTO.userId},
|
||||
{...updateParams},
|
||||
{returnOriginal: false});
|
||||
|
||||
return res.type('json').status(200).send(updatedUser);
|
||||
}
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
};
|
||||
|
||||
// Implement end-point to retrieve a user by ID (we only have login)
|
||||
|
||||
module.exports.findUser = async (req, res) => {
|
||||
try{
|
||||
const userId = req.params.userId;
|
||||
|
||||
if(!userId) return res.type('json').status(400).send('Invalid user id provided.');
|
||||
|
||||
// Find the user
|
||||
var searchedUser = await user.findOne({_id:userId}).exec();
|
||||
|
||||
if (!searchedUser){
|
||||
return res.type('json').status(404).send("User was not found.");
|
||||
}else{
|
||||
return res.type('json').status(200).send(_.pick(searchedUser, ['_id', 'riotId', 'displayName', 'gameName','tagLine','email', 'avatarImage', 'rank', 'accountLevel', 'region', 'age', 'gender', 'reputation', 'playerType', 'aboutMe']));
|
||||
}
|
||||
|
||||
}catch(err) {
|
||||
return res.type('json').status(500).send(err.toString());
|
||||
}
|
||||
};
|
||||
|
||||
/* Helper Functions */
|
||||
|
||||
function toRegionNo(region){
|
||||
let res = -1;
|
||||
switch(region){
|
||||
case 'na':
|
||||
res = 0;
|
||||
break;
|
||||
case 'eu':
|
||||
res = 1;
|
||||
break;
|
||||
case 'ap':
|
||||
res = 2;
|
||||
break;
|
||||
case 'kr':
|
||||
res = 3;
|
||||
break;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function parseRank(rank){
|
||||
|
||||
let rankName = rank.substring(0, rank.indexOf(" "));
|
||||
|
||||
let rankType = -1;
|
||||
let rankLevel = rank.substring(rank.indexOf(" ") + 1) * 1;
|
||||
|
||||
switch(rankName.toLowerCase()){
|
||||
case 'iron':
|
||||
rankType = 1;
|
||||
break;
|
||||
case 'bronze':
|
||||
rankType = 2;
|
||||
break;
|
||||
case 'silver':
|
||||
rankType = 3;
|
||||
break;
|
||||
case 'gold':
|
||||
rankType = 4;
|
||||
break;
|
||||
case 'platinum':
|
||||
rankType = 5;
|
||||
break;
|
||||
case 'diamond':
|
||||
rankType = 6;
|
||||
break;
|
||||
case 'ascendant':
|
||||
rankType = 7;
|
||||
break;
|
||||
case 'immortal':
|
||||
rankType = 8;
|
||||
break;
|
||||
case 'radiant':
|
||||
rankType = 9;
|
||||
break;
|
||||
}
|
||||
|
||||
return {rankType: rankType, rankLevel: rankLevel};
|
||||
}
|
||||
28
api/models/chat.js
Normal file
28
api/models/chat.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const ChatsSchema = new Schema({
|
||||
senderId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false,
|
||||
},
|
||||
receiverId:{
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false,
|
||||
},
|
||||
timestamp : {
|
||||
type: Date,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Chats", ChatsSchema);
|
||||
22
api/models/commendation.js
Normal file
22
api/models/commendation.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const CommendationsSchema = new Schema({
|
||||
commenderId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false,
|
||||
},
|
||||
commendedId:{
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
score: {
|
||||
type: Number,
|
||||
required: true,
|
||||
unique: false,
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Commendations", CommendationsSchema);
|
||||
42
api/models/filter.js
Normal file
42
api/models/filter.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const FiltersSchema = new Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
serverPreference: {
|
||||
type: Number,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: 0 // na
|
||||
},
|
||||
gameMode: {
|
||||
type: Number,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: 1 // casual
|
||||
},
|
||||
rankDisparity:{
|
||||
type: Array,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: [1, 1, 5, 2]
|
||||
},
|
||||
ageRange:{
|
||||
type: Array,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: [18, 25]
|
||||
},
|
||||
genders: {
|
||||
type: Array,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: [true, true, true, true]
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Filters", FiltersSchema);
|
||||
23
api/models/matching.js
Normal file
23
api/models/matching.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const MatchingSchema = new Schema({
|
||||
firstUser: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
secondUser: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
timestamp : {
|
||||
type: Date,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: Date.now
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("Matching", MatchingSchema);
|
||||
17
api/models/socketModel.js
Normal file
17
api/models/socketModel.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const SocketSchema = new Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
socketId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("SocketModel", SocketSchema);
|
||||
89
api/models/user.js
Normal file
89
api/models/user.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const UserSchema = new Schema({
|
||||
riotId: {
|
||||
type: String,
|
||||
required: false,
|
||||
unique: false
|
||||
},
|
||||
displayName: {
|
||||
type: String,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: ""
|
||||
},
|
||||
gameName: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
tagLine:{
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false,
|
||||
select: false
|
||||
},
|
||||
avatarImage: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
rank : {
|
||||
type: Array,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
accountLevel : {
|
||||
type: Number,
|
||||
required: true ,
|
||||
unique: false
|
||||
},
|
||||
region : {
|
||||
type: Number,
|
||||
required: true,
|
||||
unique: false
|
||||
},
|
||||
age : {
|
||||
type: Number,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: 0
|
||||
},
|
||||
gender : {
|
||||
type: Number,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: -1
|
||||
},
|
||||
reputation : {
|
||||
type: Number,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: 5 // In the middle
|
||||
},
|
||||
playerType : {
|
||||
type: Number,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: 1 // Casual
|
||||
},
|
||||
aboutMe : {
|
||||
type: String,
|
||||
required: false,
|
||||
unique: false,
|
||||
default: ""
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model("User", UserSchema);
|
||||
3390
api/package-lock.json
generated
Normal file
3390
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
31
api/package.json
Normal file
31
api/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "seng513-api",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "nodemon server.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Rafael1321/SENG513-API.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Rafael1321/SENG513-API/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Rafael1321/SENG513-API#readme",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"bcrypt": "^5.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.2",
|
||||
"express": "^4.18.1",
|
||||
"lodash": "^4.17.21",
|
||||
"mongoose": "^6.6.1",
|
||||
"nodemon": "^2.0.20",
|
||||
"socket.io": "^4.5.3"
|
||||
}
|
||||
}
|
||||
12
api/routes/chats.js
Normal file
12
api/routes/chats.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const express = require("express");
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
const chats = require("../controllers/chats");
|
||||
|
||||
router.route('/chats/retrieve')
|
||||
.post(chats.retrieveMessages);
|
||||
|
||||
router.route('/chats/save')
|
||||
.post(chats.saveMessage);
|
||||
|
||||
module.exports = router;
|
||||
9
api/routes/commendations.js
Normal file
9
api/routes/commendations.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const express = require("express");
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
const commendations = require("../controllers/commendations");
|
||||
|
||||
router.route('/commendations')
|
||||
.post(commendations.saveCommendation);
|
||||
|
||||
module.exports = router;
|
||||
12
api/routes/filters.js
Normal file
12
api/routes/filters.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const express = require("express");
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
const filters = require("../controllers/filters");
|
||||
|
||||
router.route('/filters/:userId')
|
||||
.get(filters.retrieve);
|
||||
|
||||
router.route('/filters')
|
||||
.post(filters.upsert);
|
||||
|
||||
module.exports = router;
|
||||
9
api/routes/matchings.js
Normal file
9
api/routes/matchings.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const express = require("express");
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
const matchings = require("../controllers/matchings");
|
||||
|
||||
router.route('/matchings/:userId')
|
||||
.get(matchings.retrieveMatchHistory);
|
||||
|
||||
module.exports = router;
|
||||
18
api/routes/users.js
Normal file
18
api/routes/users.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const express = require("express");
|
||||
const router = express.Router({ mergeParams: true });
|
||||
|
||||
const users = require("../controllers/users");
|
||||
|
||||
router.route('/users/:userId')
|
||||
.get(users.findUser);
|
||||
|
||||
router.route('/users/register')
|
||||
.post(users.registerUser);
|
||||
|
||||
router.route('/users/login')
|
||||
.post(users.loginUser);
|
||||
|
||||
router.route('/users/update')
|
||||
.put(users.updateUser);
|
||||
|
||||
module.exports = router;
|
||||
68
api/server.js
Normal file
68
api/server.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const express = require("express");
|
||||
const mongoose = require("mongoose");
|
||||
const dotenv = require('dotenv');
|
||||
const path = require('path');
|
||||
const { Server } = require("socket.io");
|
||||
const { createServer } = require('http');
|
||||
const {broadcasting} = require('./sockets/broadcasting');
|
||||
const cors=require("cors");
|
||||
|
||||
// DotEnv Configuration
|
||||
dotenv.config();
|
||||
|
||||
// Mongo DB Configuration
|
||||
const dbConfig = {
|
||||
database : process.env.MONGO_DB ?? '',
|
||||
hostname : process.env.MONGO_HOST ?? '',
|
||||
port : process.env.MONGO_PORT ?? '',
|
||||
};
|
||||
|
||||
const connectionURI = `mongodb://${dbConfig.hostname}:${dbConfig.port}/${dbConfig.database}`
|
||||
mongoose.connect(connectionURI);
|
||||
const db = mongoose.connection;
|
||||
db.on("error", console.error.bind(console, "connection error:"));
|
||||
db.once("open", () => {
|
||||
console.log("Database connected");
|
||||
});
|
||||
|
||||
// App configuration
|
||||
const appConfig = { port : process.env.APP_PORT ?? '5000' }
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
app.use(cors({
|
||||
origin:'*',
|
||||
credentials:true,
|
||||
optionSuccessStatus:200,
|
||||
}));
|
||||
|
||||
// Route Configuration
|
||||
const userRoutes = require("./routes/users");
|
||||
const filtersRoutes = require("./routes/filters");
|
||||
const matchingRoutes = require("./routes/matchings");
|
||||
const chatRoutes = require("./routes/chats");
|
||||
const commendationRoutes = require("./routes/commendations");
|
||||
|
||||
app.use("/", userRoutes);
|
||||
app.use("/", filtersRoutes);
|
||||
app.use("/", matchingRoutes);
|
||||
app.use("/", chatRoutes);
|
||||
app.use("/", commendationRoutes)
|
||||
|
||||
// Starting the node.js server
|
||||
app.listen(appConfig.port, () => {
|
||||
console.log(`Serving express server on port ${appConfig.port}`);
|
||||
});
|
||||
|
||||
// Socket configuration
|
||||
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer);
|
||||
|
||||
broadcasting(io); // Used for sending and receiving simple real-time messages between users.
|
||||
|
||||
const socketConfig = { port : process.env.SOCKET_PORT ?? '2000'}
|
||||
httpServer.listen(socketConfig.port, () => {
|
||||
console.log(`Serving socket server on port ${socketConfig.port}`);
|
||||
});
|
||||
218
api/sockets/broadcasting.js
Normal file
218
api/sockets/broadcasting.js
Normal file
@@ -0,0 +1,218 @@
|
||||
const user = require("../models/user");
|
||||
const matching = require("../models/matching");
|
||||
const socketModel = require("../models/socketModel");
|
||||
const filter = require("../models/filter");
|
||||
|
||||
// Used to keep track of who is connected and know their socket
|
||||
const connectedUsers = new Map();
|
||||
let matchingQueue = [];
|
||||
|
||||
function broadcasting(io){ // For connection and disconnection
|
||||
|
||||
io.on("connection", async (socket) => {
|
||||
|
||||
const connectedUserId = socket.handshake.query.userId;
|
||||
|
||||
if(!connectedUserId){
|
||||
socket.emit(`error_user_connected`, {msg:`Invalid connected user id ${connectedUserId}.`});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!connectedUsers.has(connectedUserId)){
|
||||
connectedUsers.set(connectedUserId, socket.id);
|
||||
|
||||
// Check if entry exists in the db
|
||||
const socketInfo = await socketModel.findOne({userId:connectedUserId}).exec();
|
||||
if(socketInfo){
|
||||
if(socketInfo.socketId !== socket.socketId){
|
||||
await socketModel.updateOne({userId:connectedUserId}, {socketId:socket.id}).exec();
|
||||
}
|
||||
}else{
|
||||
await new socketModel({
|
||||
userId: connectedUserId,
|
||||
socketId: socket.id
|
||||
}).save();
|
||||
}
|
||||
|
||||
// save in database
|
||||
socket.emit(`success_user_connected`, {msg:`User with id ${connectedUserId} CONNECTED.`});
|
||||
}else{
|
||||
socket.emit(`error_user_connected`, {msg:`User with id ${connectedUserId} is already connected.`});
|
||||
}
|
||||
|
||||
/* MATCHING */
|
||||
|
||||
socket.on('find_matching', async (findMatchDTO) => {
|
||||
|
||||
// In case user id is invalid.
|
||||
if(!findMatchDTO.userId){
|
||||
socket.emit('error_find_matching',{msg:'User id is invalid.'});
|
||||
return;
|
||||
}
|
||||
if(!findMatchDTO.filters){
|
||||
socket.emit('error_find_matching', {msg:"Filters are invalid."});
|
||||
return;
|
||||
}
|
||||
|
||||
// In case user id is not found
|
||||
if(!connectedUsers.has(findMatchDTO.userId)){
|
||||
socket.emit('error_find_matching', {msg:'User id does not exist.'});
|
||||
return;
|
||||
}
|
||||
|
||||
socket.emit('success_find_matching', {msg:`User with id ${findMatchDTO.userId} ONLINE.`});
|
||||
|
||||
// Attempting to find a match
|
||||
let matchFound = -1;
|
||||
const user1 = await user.findOne({_id:findMatchDTO.userId}).exec();
|
||||
const user1Filters = findMatchDTO.filters;
|
||||
for(let i = 0; i < matchingQueue.length && matchFound == -1; i++){
|
||||
var user2Id = matchingQueue[i];
|
||||
matching.countDocuments( {$or: [ { $and:[ {'firstUser': findMatchDTO.userId}, {'secondUser': user2Id} ]}, { $and:[ {'firstUser': user2Id}, {'secondUser': findMatchDTO.userId} ]}]}).exec().then( count => {
|
||||
if(!count){
|
||||
user.findOne({_id:user2Id}).exec().then( user2 => {
|
||||
filter.findOne({userId:user2Id}).exec().then( user2Filters => {
|
||||
|
||||
// Region Matched
|
||||
const regionMatched = (user1.region === user2Filters.serverPreference) && (user2.region === user1Filters.serverPreference);
|
||||
|
||||
// Game Mode Type
|
||||
const playerTypeMatched = (user1.playerType === user2Filters.gameMode) && (user2.playerType === user1Filters.gameMode);
|
||||
|
||||
// Rank Macthed
|
||||
const rankMatchedUser1 = (user1.rank[0] >= user2Filters.rankDisparity[0] && user1.rank[0] <= user2Filters.rankDisparity[2]);
|
||||
const rankMatchedUser2 = (user2.rank[0] >= user1Filters.rankDisparity[0] && user2.rank[0] <= user1Filters.rankDisparity[2]);
|
||||
const rankMatched = rankMatchedUser1 && rankMatchedUser2;
|
||||
|
||||
// Age Matched
|
||||
const ageMatched = (user1.age >= user2Filters.ageRange[0] && user1.age <= user2Filters.ageRange[1]) &&
|
||||
(user2.age >= user1Filters.ageRange[0] && user2.age <= user1Filters.ageRange[1]);
|
||||
|
||||
// Gender Matched
|
||||
let genderMatched = false;
|
||||
if(user1.gender !== -1 && user2.gender !== -1){
|
||||
genderMatched = user2Filters.genders[user1.gender] && user1Filters.genders[user2.gender];
|
||||
}
|
||||
|
||||
// They are match based on filters...
|
||||
if(regionMatched && playerTypeMatched && rankMatched && ageMatched && genderMatched){
|
||||
|
||||
// Notify each user of the match and remove the matched user from queue
|
||||
socket.emit('match_found', {user:user2});
|
||||
|
||||
if(!connectedUsers.has(user2Id)){
|
||||
socketModel.findOne({userId:receiverId}).exec().then( socketInfo => {
|
||||
if(socketInfo){
|
||||
connectedUsers.set(socketInfo.userId, socketInfo.socketId);
|
||||
io.to(socketInfo.socketId).emit('match_found', {user:user1});
|
||||
}
|
||||
});
|
||||
}else{
|
||||
io.to(connectedUsers?.get(user2Id)).emit('match_found', {user:user1}); // TODO: add DB call if not in there
|
||||
}
|
||||
|
||||
// Store matching in the DB
|
||||
const newMatch = new matching({
|
||||
firstUser: findMatchDTO.userId,
|
||||
secondUser: user2Id,
|
||||
});
|
||||
newMatch.save();
|
||||
|
||||
// Remove matched user from queue
|
||||
matchingQueue = immutableRemove(matchFound, matchingQueue);
|
||||
matchFound = i;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if(matchFound === -1){
|
||||
matchingQueue.push(findMatchDTO.userId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('stop_matching', (userId) => {
|
||||
// In case user id is invalid.
|
||||
if(!userId){
|
||||
socket.emit('error_stop_matching',{msg:'User id is invalid.'});
|
||||
return;
|
||||
}
|
||||
|
||||
// In case user id is not found
|
||||
if(!connectedUsers.has(userId)){
|
||||
socket.emit('error_stop_matching', {msg:'User id does not exist.'});
|
||||
return;
|
||||
}
|
||||
|
||||
// Removing user from the matching queue
|
||||
matchingQueue = immutableRemove(matchingQueue.indexOf(userId), matchingQueue);
|
||||
|
||||
socket.emit('success_stop_matching', {msg:`User with id ${userId} OFFLINE`});
|
||||
});
|
||||
|
||||
/* CHAT */
|
||||
socket.on("send_msg" , async (receiverId, msg) => { // msg = Text
|
||||
|
||||
if(!receiverId){
|
||||
socket.emit('error_send_msg', {msg:"Invalid receiver id."});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!connectedUsers.has(receiverId)){
|
||||
|
||||
// Check if it exists in DB
|
||||
const socketInfo = await socketModel.findOne({userId:receiverId}).exec();
|
||||
|
||||
if(!socketInfo){
|
||||
socket.emit('error_send_msg', {msg:"User with that id is not online."})
|
||||
return;
|
||||
}else{
|
||||
connectedUsers.set(receiverId, socketInfo.socketId);
|
||||
io.to(socketInfo.socketId).emit("receive_msg", {msg:msg});
|
||||
}
|
||||
|
||||
}else{
|
||||
io.to(connectedUsers?.get(receiverId)).emit("receive_msg", {msg:msg});
|
||||
}
|
||||
});
|
||||
|
||||
/* DISCONNECT*/
|
||||
socket.on("disconnect", (socket) => {
|
||||
|
||||
const connectedUserId = getUserIdFromSocketId(socket.id);
|
||||
if(!connectedUserId) return;
|
||||
|
||||
// Remove user from map
|
||||
if(connectedUsers.has(connectedUserId)){
|
||||
connectedUsers.delete(connectedUserId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* HELPER FUNCTIONS */
|
||||
|
||||
function getUserIdFromSocketId(socketId){
|
||||
let userId = "";
|
||||
for (let [uId, info] of connectedUsers) {
|
||||
if(info.socketId === socketId){
|
||||
userId = uId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
function immutableRemove(idx, array){
|
||||
|
||||
if(idx < 0 || idx >= array.length) return array;
|
||||
|
||||
newArray = [];
|
||||
for(let i = 0; i < array.length; i++){
|
||||
if(idx !== i) newArray.push(array[i]);
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
module.exports = { broadcasting };
|
||||
Reference in New Issue
Block a user