// Necessary imports
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const saltRounds = 10;
// Mongoose models
const UserModel = require("./User");
const StatModel = require("./Stat");
const ItemModel = require("./Item");
const GameDescriptionModel = require("./GameDescription");
const nodemailer = require("nodemailer");
// Initialize items (if necessary)
const initItems = require("./initItems");
const resetServer = require("./resetServer");
const csl = require("../controller/intelligentLogging");
// csl.silenced('bdd');
require("dotenv").config();
/**
* Sends a verification email to the specified email address with the provided verification code.
* @param {string} email - The recipient's email address
* @param {string} code - The verification code to be sent
*/
async function sendVerificationEmail(email, code) {
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: "sunglass.poker@gmail.com",
pass: "lmqnlxnhafubvuan",
},
});
const mailOptions = {
from: "sunglass.poker@gmail.com",
to: email,
subject: "Verify your email",
text: `Your verification code is: ${code}`,
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
console.error("Email send error:", error);
}
});
}
module.exports = function (app, bdd) {
// Logging database connection
csl.log("bdd", "bdd!", bdd);
// bdd = "localhost:10003";
// Connect to MongoDB database
mongoose.connect("mongodb://pokerBackEndServer:azerty@" + bdd + "/Poker", {});
const db = mongoose.connection;
db.on("error", csl.error.bind(console, "Error connecting to the database:"));
db.once("open", () => {
// Log successful database connection
csl.log("bdd", "Connected to MongoDB database");
// Reset server state
resetServer.emptyGameDesc();
resetServer.resetPlayerInGame();
// Initialize items
initItems();
});
const dao = {
/**
* Create new use to the database if the email and pseudo are not already used.
* The password will be hashed in the db
* The User will be associated with his stats (future) and all the basics item he's unlocking.
* @param {String} pseudo
* @param {String} email
* @param {String} password
* @returns Success status
*/
createUser: async (pseudo, email, password) => {
const hashedPassword = await bcrypt.hash(password, saltRounds);
try {
// Check uniqueness of pseudo and email
const existingUser = await UserModel.findOne({
$or: [{ pseudo }, { email }],
});
if (existingUser) {
return {
error: true,
code: 400,
data: {
error: "user_exists",
field: existingUser.pseudo === pseudo ? "pseudo" : "email",
},
};
}
// Function to find default items
async function findDefaultItem(name, category = null) {
const query = category
? { "names.en": name, category }
: { "names.en": name };
const item = await ItemModel.findOne(query);
if (!item) {
throw new Error(`Default item not found: ${name}`);
}
return item;
}
// Retrieve default items
const defaultAvatar = await findDefaultItem("Sun");
const defaultColor = await findDefaultItem("White", "colorAvatar");
const defaultSunglasses = await findDefaultItem("Nothing");
// Create user with default items
const newUser = new UserModel({
pseudo,
email,
password: hashedPassword,
itemsOwned: [
defaultAvatar._id,
defaultColor._id,
defaultSunglasses._id,
],
baseAvatar: defaultAvatar._id,
sunglasses: defaultSunglasses._id,
colorAvatar: defaultColor._id,
inGame: null,
});
const savedUser = await newUser.save();
// Create user statistics
const newStat = new StatModel({
maxCoins: 0,
maxGain: 0,
totalGain: 0,
experience: 0,
user: savedUser._id,
});
await newStat.save();
savedUser.stat = newStat._id;
await savedUser.save();
return savedUser;
} catch (error) {
csl.error("bdd", "Error creating user:", error);
if (error.message.startsWith("Default item not found")) {
return {
error: true,
code: 404,
data: { error: "item_not_found", message: error.message },
};
}
throw error;
}
},
/**
* Return the Token of the connection when the hashed password match to the username passed..
* @param {String} username
* @param {String} password
* @returns Success status & token
*/
loginUser: async (username, password) => {
try {
// Find user by username
const user = await UserModel.findOne({ pseudo: username });
if (!user) {
// If user not found, return error
return {
error: true,
code: 404,
data: { error: "user_not_found", message: "User not found" },
};
}
// Compare password with hashed password
const match = await bcrypt.compare(password, user.password);
if (!match) {
// If password does not match, return error
return {
error: true,
code: 401,
data: {
error: "invalid_credentials",
message: "Invalid credentials",
},
};
}
// Generate JWT token
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, {
expiresIn: "1h",
});
// Return success response with user details and token
return {
success: true,
user: {
id: user._id,
pseudo: user.pseudo,
email: user.email,
inGame: user.inGame,
},
token: token,
};
} catch (error) {
// Handle error
csl.error("bdd", "Error during login:", error);
throw error;
}
},
/**
* Search for a user based on the identifiers and value pass and set the specified field to the new value.
* @param {String} identifierType
* @param {*} identifierValue
* @param {String} field
* @param {*} value
* @returns Success status
*/
updateUserData: async (identifierType, identifierValue, field, value) => {
try {
// Find the user by the specified identifier and update the field with the new value
const updatedUser = await UserModel.findOneAndUpdate(
{ [identifierType]: identifierValue },
{ $set: { [field]: value } },
{ new: true, runValidators: true }
);
if (updatedUser) {
// If the user is updated successfully, return success:true with a message
return { success: true, message: `${field} updated successfully` };
} else {
// If the user is not found, return success:false with a failure message
return { success: false, message: `Failed to update ${field}` };
}
} catch (error) {
// Handle error
csl.error("bdd", "Error updating user data:", error);
throw error;
}
},
/**
* Buy a item in the shop if the user has enough money. Adds it to the user list and remove the corresponding money.
* @param {String} userId
* @param {String} itemId
* @returns Success status
*/
buyItem: async (userId, itemId) => {
try {
// Find the user by ID
const user = await UserModel.findById(userId);
if (!user) {
// If user not found, return failure message
return { success: false, message: "User not found" };
}
// Find the item by ID
const item = await ItemModel.findById(itemId);
if (!item) {
// If item not found, return failure message
return { success: false, message: "Item not found" };
}
// Check if user has enough coins to buy the item
if (user.coins < item.price) {
// If not enough coins, return failure message
return { success: false, message: "Not enough coins" };
}
// Deduct the item price from user's coins
user.coins -= item.price;
// Add the item to the user's owned items
user.itemsOwned.push(item._id);
// Save the updated user
await user.save();
// Return success message along with updated user information
return {
success: true,
message: "Item bought successfully",
user: {
id: user._id,
coins: user.coins,
itemsOwned: user.itemsOwned,
},
};
} catch (error) {
// Handle error
csl.error("bdd", "Error buying item:", error);
throw error;
}
},
/**
* Take a user id and send back the user Object from the database.
* @param {String} userId
* @returns Success status with the user object
*/
getUserInfo: async (userId) => {
// Log fetching info for the user ID
csl.log("bdd", "Fetching info for user ID:", userId);
try {
// Find the user by ID and populate avatar, sunglasses, and colorAvatar fields
const user = await UserModel.findById(userId)
.populate("baseAvatar")
.populate("sunglasses")
.populate("colorAvatar")
.exec();
if (!user) {
// Log if no user is found
csl.log("bdd", "No user found with ID:", userId);
return { success: false, message: "Utilisateur non trouvé" };
}
// Return user information with additional fields for avatar images and inGame status
return {
success: true,
user: {
...user._doc,
pseudo: user.pseudo,
baseAvatarImgSrc: user.baseAvatar ? user.baseAvatar.imgSrc : null,
sunglassesImgSrc: user.sunglasses ? user.sunglasses.imgSrc : null,
colorAvatarImgSrc: user.colorAvatar
? user.colorAvatar.imgSrc
: "#FFFFFF",
inGame: user.inGame,
},
};
} catch (error) {
// Log and throw error if fetching user information fails
csl.error("bdd", "Error fetching user information:", error);
throw error;
}
},
/**
* Send back all item purchasable from the database.
* @returns Success status with an Array of item .
*/
getItems: async () => {
try {
// Find all items in the database
const items = await ItemModel.find();
// Return success message along with the list of items
return { success: true, items: items };
} catch (error) {
// Log and throw error if fetching items fails
csl.error("bdd", "Error fetching items:", error);
throw new Error("Error fetching items from the database");
}
},
/**
* Change the ownership of the specific item for the user.
* @param {String} userId
* @param {String} itemId
* @returns Success status
*/
activateAvatar: async (userId, itemId) => {
try {
// Find the user by ID
const user = await UserModel.findById(userId);
if (!user) {
// Return error message if user is not found
return { success: false, message: "User not found" };
}
// Find the item by ID
const item = await ItemModel.findById(itemId);
if (!item) {
// Return error message if item is not found
return { success: false, message: "Item not found" };
}
// Check if the user owns the item
const itemExists = user.itemsOwned.some(
(id) => id.toString() === itemId
);
if (!itemExists) {
// Return error message if item is not owned by the user
return { success: false, message: "Item not owned" };
}
let itemType;
switch (item.category) {
case "baseAvatar":
// Set baseAvatar if the item category is baseAvatar
itemType = "baseAvatar";
user.baseAvatar = itemId;
break;
case "sunglasses":
// Set sunglasses if the item category is sunglasses
itemType = "sunglasses";
user.sunglasses = itemId;
break;
case "colorAvatar":
// Set colorAvatar if the item category is colorAvatar
itemType = "colorAvatar";
user.colorAvatar = itemId;
break;
default:
// Return error message for invalid item category
return { success: false, message: "Invalid item category" };
}
// Save the updated user data
await user.save();
// Return success message along with activated avatar component information
return {
success: true,
message: "Avatar component activated successfully",
itemType: itemType,
itemId: itemId,
};
} catch (error) {
// Log and throw error if activating avatar component fails
csl.error("bdd", "Error activating avatar component:", error);
throw error;
}
},
/**
* Return the players stats base on the page number and the number of stats asked.
* @param {*} page
* @param {*} nbRes
* @returns [...stats]
*/
getAllRanking: async (page, nbRes) => {
try {
// Find all users, sort them by coins in descending order, paginate the results,
// and select only pseudo and coins fields
const users = await UserModel.find()
.sort({ coins: -1 }) // Sort users by coins in descending order
.skip((page - 1) * nbRes) // Skip documents based on pagination
.limit(nbRes) // Limit the number of documents per page
.select({ pseudo: 1, coins: 1 }); // Select only pseudo and coins fields
// Return success message along with the data (users)
return { success: true, data: users };
} catch (err) {
// Log and return error message if fetching rankings fails
csl.error("Error fetching rankings:", err);
return { success: false, error: err };
}
},
/**
* Return the avatar infos of the user.
* @param {String} userId
* @returns Success & Object avatar
*/
getAvatarInfo: async (userId) => {
try {
// Find the user by ID and populate avatar-related fields with necessary information
const user = await UserModel.findById(userId)
.populate({
path: "baseAvatar",
select: "imgSrc eyePosition", // Only fetching necessary fields
})
.populate({
path: "sunglasses",
select: "imgSrc",
})
.populate({
path: "colorAvatar",
select: "imgSrc",
})
.exec();
// If user not found, log and return null
if (!user) {
csl.log("No user found with ID:", userId);
return null;
}
// Return avatar information including baseAvatar, sunglasses, and colorAvatar
return {
baseAvatar: user.baseAvatar
? {
imgSrc: user.baseAvatar.imgSrc,
eyePosition: user.baseAvatar.eyePosition,
}
: null,
sunglasses: user.sunglasses
? {
imgSrc: user.sunglasses.imgSrc,
}
: null,
colorAvatar: user.colorAvatar
? {
imgSrc: user.colorAvatar.imgSrc,
}
: null,
};
} catch (error) {
// Log and throw error if fetching avatar information fails
csl.error("Error fetching avatar information:", error);
throw error;
}
},
/**
* Create a new Game descriptor with the info of the game.
* @param {String} serverName
* @param {String} roomPassword
* @param {String} rank
* @param {String} master
* @returns Success & GameDescription created.
*/
createGameDescription: async function (
serverName,
roomPassword,
rank,
master = 0
) {
try {
// Check if a game with the same serverName exists, if so, delete it
const existingGame = await GameDescriptionModel.findOne({ serverName });
if (existingGame) {
return {
error: true,
code: 400,
data: {
error: "game_exists",
field: "game",
message: "Game name already exists",
},
};
}
// Create a new game description with provided parameters
const gameDescription = new GameDescriptionModel({
serverName,
roomPassword,
rank,
players: master === 0 ? [] : [master], // If master provided, add to players array
});
// Save the new game description
await gameDescription.save();
// Return success message with the created game description
return { error: false, code: 200, data: gameDescription };
} catch (error) {
// Log and return error message if creating game description fails
csl.error("bdd", "Error creating game description:", error);
return {
error: true,
code: 500,
data: { error: "Error creating game description" },
};
}
},
/**
* Update a GameDescription to include a new user in the game
* if he's not already in and update the user.inGame field.
* @param {String} gameId
* @param {String} userId
*/
addOnePlayerGameDesc: async function (gameId, userId) {
// Log the addition of a player to a game
csl.log("bdd", "add a player in game bdd");
// Find the game description by ID and add the player's ID to the players array
const gameDesc = await GameDescriptionModel.findById(gameId);
gameDesc.players.push(userId);
await gameDesc.save();
// Log the update of the inGame status for the player
csl.log("bdd", "update the inGame status of the player");
// Find the user by ID and update their inGame status to gameId
const user = await UserModel.findById(userId);
csl.log(userId, user);
user.inGame = gameId;
await user.save();
},
/**
* Destroy the gameDescription base on the id provided.
* @param {String} gameId
*/
removeGameDesc: async function (gameId) {
// Find and delete the game description by ID
await GameDescriptionModel.findByIdAndDelete(gameId);
},
/**
* Update a specific field of a gameDescription item base on the passed identifier.
* @param {String} identifierType
* @param {Any} identifierValue
* @param {String} field
* @param {Any} value
* @returns Success status & boolean
*/
updateGameDescription: async function (
identifierType,
identifierValue,
field,
value
) {
try {
// Find and update the game description based on identifierType and identifierValue
const updatedGameDesc = await GameDescriptionModel.findOneAndUpdate(
{ [identifierType]: identifierValue },
{ $set: { [field]: value } },
{ new: true, runValidators: true }
);
// Check if the update was successful and return a success message
if (updatedGameDesc) {
return { success: true, message: `${field} updated successfully` };
} else {
return { success: false, message: `Failed to update ${field}` };
}
} catch (error) {
// Log and throw error if updating game description fails
csl.error("bdd", "Error updating game description:", error);
throw error;
}
},
/**
* Find the player and update his inGame field and the corresponding game.
* Can be done safely with a player without knowing the inGame field.
* @param {String} id
*/
playerLeftGame: async function (id) {
try {
// Find the user by ID
const user = await UserModel.findById(id);
// Find the game description by user's inGame ID
const gameDesc = await GameDescriptionModel.findById(user.inGame);
csl.log("this ", user, " is leaving the game");
csl.log("game players before remove", gameDesc.players);
// If game description exists, remove the player's ID from the players array
if (gameDesc !== null) {
gameDesc.players = gameDesc.players.filter((p) => p !== id);
await gameDesc.save();
}
csl.log("game players after remove", gameDesc.players);
// Reset the user's inGame status to null
user.inGame = null;
csl.log(user, " removing user in game");
await user.save();
} catch (err) {
// Log error if player leaving game encounters an error
csl.error("bdd", "Error with player left game:", err);
}
},
/**
* Not a future proof solution. Game are relatively in small quantity
* but should be refactor to include a number of page maximum like @see getAllRanking
* @returns Success status & array of GameDescription
*/
gameRoomDescription: async function () {
try {
// Fetch all game descriptions from the database
const gameDescriptions = await GameDescriptionModel.find({});
// Return fetched game descriptions along with success status code 200
return { error: false, code: 200, data: gameDescriptions };
} catch (error) {
// Log and return error if fetching game descriptions fails
csl.error("bdd", "Error fetching game descriptions:", error);
return {
error: true,
code: 500,
data: { error: "Error fetching game descriptions" },
};
}
},
/**
* Add the coins in argument to the current amount of the user.
* This uses negative amount to remove player coins.
* @param {String} userId
* @param {int} coinsToAdd
* @returns Success status & updated coins amount
*/
updateUserCoins: async (userId, coinsToAdd) => {
try {
// Find user by ID
const user = await UserModel.findById(userId);
if (!user) {
return { success: false, message: "User not found" };
}
// Update user's coins by adding coinsToAdd
user.coins += coinsToAdd;
await user.save();
// Return success message along with updated coins count
return {
success: true,
updatedCoins: user.coins,
message: "Coins updated successfully",
};
} catch (error) {
// Log and return error if updating user coins fails
csl.error("Error updating user coins:", error);
return { success: false, message: "Failed to update user coins" };
}
},
/**
* Fetch the username based on the id.
* @param {String} userId
* @returns Success & username.
*/
getUserPseudoFromUserId: async (userId) => {
try {
// Find user by ID and return their pseudo (username)
const user = await UserModel.findOne({ _id: userId });
if (user) {
return user.pseudo; // Assuming user.userName is the field containing the user's name
} else {
throw new Error(`User with ID ${userId} not found`);
}
} catch (error) {
// Log and throw error if retrieving user data fails
csl.error("bdd", "Error retrieving user data:", error);
throw error;
}
},
/**
* Return all the game where a player could potentially join.
* @returns Success status & Array of gameDescription
*/
getAvailableGames: async function () {
try {
// Fetch all available games with status "WAITING" from the database
const availableGames = await GameDescriptionModel.find({
$or: [
{ roomPassword: { $exists: false } }, // No roomPassword field
{ roomPassword: "" }, // roomPassword field is an empty string
],
});
// Return fetched available games along with success status code 200
return { code: 200, data: availableGames };
} catch (error) {
// Return error along with error status code 500 if fetching available games fails
return { code: 500, error: error.message };
}
},
/**
* Return the serverName of a gameDescription
* @param {String} gameId
* @returns Success status & String
*/
getServerNameFromGameId: async function (gameId) {
csl.log("getServerNameFromGameId", "Argument gameId: ", gameId);
if (gameId.gameRoomId !== undefined) gameId = gameId.gameRoomId;
try {
gameRecord = await GameDescriptionModel.findOne({ _id: gameId });
if (gameRecord) {
return gameRecord.serverName;
} else {
throw new Error(`Game with ID ${gameId} not found`);
}
} catch (error) {
// Log and throw error if retrieving game data fails
csl.error("bdd", "Error retrieving game data:", error);
throw error;
}
},
/**
* Find the corresponding Gamedescription and change the status.
* @param {String} roomId
* @returns Success status
*/
updateStatusToInProgress: async function (roomId) {
try {
// Find and update the game description's status to "IN_PROGRESS" by room ID
const updatedRoom = await GameDescriptionModel.findOneAndUpdate(
{ _id: roomId },
{ $set: { status: "IN_PROGRESS" } },
{ new: true, runValidators: true }
);
// Return success message if update is successful
if (updatedRoom) {
return {
success: true,
message: "Status updated to IN_PROGRESS successfully",
};
} else {
return {
success: false,
message: "Failed to update status to IN_PROGRESS",
};
}
} catch (error) {
// Log and throw error if updating status to IN_PROGRESS fails
csl.error("Error updating status to IN_PROGRESS:", error);
throw error;
}
},
/**
* Search through the database to quickly verify if the email is already being used.
* If the email exists, generate a verification code and send it to the user's email.
* @param {String} email - The email to be checked
* @returns {Object} Success status and boolean
*/
checkEmail: async (email) => {
try {
// Find user by email
const user = await UserModel.findOne({ email: email });
if (user) {
// If user exists, generete verification code
const verificationCode = Math.random()
.toString(36)
.substring(2, 7)
.toUpperCase();
//Send the mail
await sendVerificationEmail(email, verificationCode);
const hashedVerificationCode = await bcrypt.hash(
verificationCode,
10
);
user.verificationCode = hashedVerificationCode;
user.verificationCodeExpires = Date.now() + 300000; //5 minutes
await user.save();
return { exists: true, message: "E-mail exists in the database" };
} else {
// If user does not exist, return exists:false with a message
return {
exists: false,
message: "E-mail does not exist in the database",
};
}
} catch (error) {
// Handle error
csl.error("bdd", "Error during email check:", error);
throw error;
}
},
/**
* Check the password hash to allow or not a player to join.
* @param {string} roomId
* @param {string} password
* @returns Success status
*/
verifyGamePassword: async (roomId, password) => {
try {
const gameDescription = await GameDescriptionModel.findById(roomId);
if (!gameDescription) {
return { success: false, error: "Game room not found" };
}
const match = await bcrypt.compare(
password,
gameDescription.roomPassword
);
if (match) {
return { success: true };
} else {
return { success: false, error: "Incorrect password" };
}
} catch (error) {
csl.error("Error verifying game password:", error);
return { success: false, error: "Internal server error" };
}
},
/**
* Allow a player to change the current password.
* The user is searched based on the email.
* @param {String} email - The user's email
* @param {String} code - The verification code
* @param {String} newPassword - The new password
* @returns {Object} Success status
*/
changePassword: async (email, code, newPassword) => {
try {
// Rechercher l'utilisateur par email
const user = await UserModel.findOne({ email: email });
// Vérifier si l'utilisateur existe
if (!user) {
return { success: false, message: "User not found" };
}
// Vérifier si le code de validation est expiré
if (user.verificationCodeExpires < Date.now()) {
return { success: false, message: "Verification code has expired" };
}
// Vérifier si le code de vérification est correct
const isCodeValid = await bcrypt.compare(code, user.verificationCode);
if (!isCodeValid) {
return { success: false, message: "Invalid verification code" };
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
// Mettre à jour le mot de passe
user.password = hashedPassword;
user.verificationCode = null;
user.verificationCodeExpires = null;
await user.save();
return { success: true, message: "Password updated successfully" };
} catch (error) {
// Log and handle the error
console.error("Error updating password:", error);
return { success: false, message: "Failed to update password" };
}
},
};
return dao;
};