diff --git a/Backend/package.json b/Backend/package.json index 6ba008f..0146d48 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -32,6 +32,7 @@ "express": "^4.19.2", "ioredis": "^5.4.2", "jsonwebtoken": "^9.0.2", + "luxon": "^3.5.0", "mongoose": "^8.4.4", "node-cron": "^3.0.3", "nodemailer": "^6.9.16", diff --git a/Backend/src/constants.js b/Backend/src/constants.js index b40d84a..e935f49 100644 --- a/Backend/src/constants.js +++ b/Backend/src/constants.js @@ -26,10 +26,28 @@ const TOURNAMENT_TYPES = Object.freeze([ "Squad", ]); +const USER_ROLES = Object.freeze([ + "USER", + "ADMIN", +]); + +const REGION = `Asia/Kolkata`; + +const GAME_ID = Object.freeze({ + "Battlegrounds Mobile India": "bgmiId", + "Call of Duty Mobile": "codmId", + "Valorant": "valorantId", + "Free Fire": "freefireId", + "Asphalt 9": "asphaltId", +}); + export { DB_NAME, COOKIE_OPTIONS, USER_BADGES, GAMES, TOURNAMENT_TYPES, + USER_ROLES, + REGION, + GAME_ID, }; \ No newline at end of file diff --git a/Backend/src/controllers/tournament.controller.js b/Backend/src/controllers/tournament.controller.js index eb5993b..ebcaa82 100644 --- a/Backend/src/controllers/tournament.controller.js +++ b/Backend/src/controllers/tournament.controller.js @@ -1,45 +1,56 @@ +import { DateTime } from "luxon"; + import asyncHandler from "../utils/asyncHandler.js"; import ApiError from "../utils/ApiError.js"; import ApiResponse from "../utils/ApiResponse.js"; +import { Game } from "../models/gameId.models.js"; import { Tournament } from "../models/tournament.models.js"; -import { Result } from "../models/result.models.js"; -import mongoose from "mongoose"; +import { REGION, GAME_ID } from "../constants.js"; +import { check24HourFormat, checkDateFormat } from "../helpers/checkDateTime.js"; -const createTournaments = asyncHandler(async(req,res)=>{ +const now = new Date(); - const {name, +// admin controlled routes +const createTournament = asyncHandler(async (req, res) => { + const { + name, matchDate, matchTime, registrationEndDate, + registrationEndTime, totalSlots, prizePool, type, game, entryFee, description, - instructions,} = req.body; - - const now = new Date(); - if(new Date(registrationEndDate) <= now){ - throw new ApiError(400,"Registration end date must be in future!") + instructions + } = req.body; + if (!check24HourFormat(matchTime) || !check24HourFormat(registrationEndTime)) { + throw new ApiError(400, "Invalid Time Format. Please Use hh:mm Format."); } - - if (new Date(matchDate) <= new Date(registrationEndDate)) { - throw new ApiError(400,"Match must be after registration ends!") + if (!checkDateFormat(matchDate) || !checkDateFormat(registrationEndDate)) { + throw new ApiError(400, "Invalid Date Format. Please Use yyyy-MM-dd Format."); + } + if(new Date(`${registrationEndDate}T${registrationEndTime}`) <= now){ + throw new ApiError(400, "Registration End Date Must Be Greater Than Current Date and Time."); + } + if (new Date(`${matchDate}T${matchTime}`) <= new Date(`${registrationEndDate}T${registrationEndTime}`)) { + throw new ApiError(400, "Match Date Must Be Greater Than Registration End Date and Time."); } - const existingTournament = await Tournament.findOne({ name }); - if (existingTournament) { - throw new ApiError(400, "Tournament already exists with this name"); + throw new ApiError(400, "Tournament With This Name Already Exists."); } - try { + const matchTiming = DateTime.fromISO(`${matchDate}T${matchTime}`, { zone: REGION }); + const registrationTiming = DateTime.fromISO(`${registrationEndDate}T${registrationEndTime}`, { zone: REGION }); const tournament = await Tournament.create({ name, - matchDate, - matchTime, - registrationEndDate, + matchDate: matchTiming.toFormat("dd-MM-yyyy"), + matchTime: matchTiming.toFormat("HH:mm"), + registrationEndDate: registrationTiming.toFormat("dd-MM-yyyy"), + registrationEndTime: registrationTiming.toFormat("HH:mm"), totalSlots, prizePool, type, @@ -47,163 +58,92 @@ const createTournaments = asyncHandler(async(req,res)=>{ entryFee, description, instructions, - }) - + }); return res .status(201) - .json( new ApiResponse(201,tournament,"Tournament Created Successfully!!")) - + .json( new ApiResponse(201, tournament, "Tournament Created Successfully!!")); } catch (error) { - throw new ApiError(500, error.message || "Internal Server Error") + throw new ApiError(500, error.message || "Internal Server Error"); } -}) - -const getTournaments = asyncHandler(async(_,res)=>{ +}); +// user accessed routes +const getTournaments = asyncHandler(async (req, res) => { try { - const tournaments = await Tournament.find({ isActive:true }) - .sort({createdAt:-1}) - .select("name matchDate matchTime registrationEndDate totalSlots filledSlots prizePool type game entryFee rating description instructions") + const tournaments = await Tournament.find({ + isActive: true, + isOngoing: true, + }).sort({ + createdAt:-1, + }).select( + "name matchDate matchTime registrationEndDate registrationEndTime totalSlots filledSlots prizePool type game entryFee rating description instructions" + ); return res .status(200) - .json(new ApiResponse(200,tournaments,"Tournaments fetched successfully!!")) + .json(new ApiResponse(200, tournaments, "Tournaments Fetched Successfully!!")); } catch (error) { - throw new ApiError(500, error.message || "Internal Server Error") + throw new ApiError(500, error.message || "Internal Server Error"); } }) -const tournamentInfo = asyncHandler(async(req,res)=>{ +const getTournamentInfo = asyncHandler(async (req,res) => { const { tournamentName } = req.body; - + if (!tournamentName) { + throw new ApiError(400, "Tournament Name is Required."); + } const tournament = await Tournament.findOne({ name: tournamentName }); if (!tournament) { throw new ApiError(404, "Tournament not found"); } - - return res. - status(200). - json(new ApiResponse(200, tournament, "Tournament fetched successfully!!")); - + try { + return res + .status(200) + .json(new ApiResponse(200, tournament, "Tournament fetched successfully!!")); + } catch (error) { + throw new ApiError(500, error.message || "Internal Server Error"); + } }) -const registerPlayer = asyncHandler(async (req, res) => { +const registerUser = asyncHandler(async (req, res) => { const { tournamentName } = req.body; const user = req.user; - const tournament = await Tournament.findOne({ name: tournamentName }); if (!tournament) { - throw new ApiError(404, "Tournament not found"); + throw new ApiError(404, "Tournament Not Found."); } - if (!tournament.isActive) { - throw new ApiError(400, "Tournament is not active"); - } - - if (tournament.registeredPlayers.includes(user._id)) { - throw new ApiError(400, "You are already registered for this tournament"); + throw new ApiError(400, "Tournament Is Not Active."); } - if (tournament.filledSlots >= tournament.totalSlots) { - throw new ApiError(400, "Tournament is full"); + throw new ApiError(400, "Tournament Is Full."); + } + const tournamentGame = GAME_ID[tournament.game]; + const userHasGameId = await Game.findOne({ owner: user._id }); + if (!userHasGameId || !userHasGameId[tournamentGame]) { + throw new ApiError(400, "Please Add Game Id First."); + } + + const userRegistered = await Tournament.findOne({ registeredPlayers: user._id }); + if (userRegistered) { + throw new ApiError(400, "User Already Registered For This Tournament."); } - try { + user.registeredTournaments.push(tournament._id); + await user.save(); tournament.registeredPlayers.push(user._id); tournament.filledSlots += 1; await tournament.save(); - - return res.status(200).json(new ApiResponse(200, "Registered Successfully!!")); - } catch (error) { - throw new ApiError(500, error.message || "Internal Server Error"); - } -}); - -const postResult = asyncHandler(async (req, res) => { - const { tournamentName, leaderboard } = req.body; - - const tournament = await Tournament.findOne({ name: tournamentName }); - if (!tournament) { - throw new ApiError(404, "Tournament not found"); - } - - if (!tournament.isActive) { - throw new ApiError(400, "Tournament is not active"); - } - - if(!leaderboard || leaderboard.length === 0){ - throw new ApiError(400, "Leaderboard is required"); - } - - try { - await Result.create({ - tournament: tournament._id, - leaderboard, - }); - return res - .status(201) - .json(new ApiResponse(201, "Result Posted Successfully!!")); + .status(200) + .json(new ApiResponse(200, null,"Registered Successfully!!")); } catch (error) { throw new ApiError(500, error.message || "Internal Server Error"); } }); -//for admin -const getResults = asyncHandler(async (req, res) => { - const { tournamentName } = req.body; - - const tournament = await Tournament.findOne({ name: tournamentName }); - - if (!tournament) { - throw new ApiError(404, "Tournament not found"); - } - - const results = await Result.find({ tournament: tournament._id }) - - if (!results) { - throw new ApiError(404, "Results not found"); - } - - return res - .status(200) - .json(new ApiResponse(200, results, "Results fetched successfully!!")); -}); - -//for user -const getIndividualResult = asyncHandler(async (req, res) => { - const { tournamentName } = req.body; - const user = req.user; - - if(!tournamentName){ - throw new ApiError(400, "Tournament name is required"); - } - - const tournament = await Tournament.findOne({ name: tournamentName }); - - if (!tournament) { - throw new ApiError(404, "Tournament not found"); - } - - const position = await Result.findOne( - { tournament: tournament._id, "leaderboard.player": user._id }, - { "leaderboard.$": 1 } - ).select("-tournament -_id -createdAt -updatedAt -__v"); - - if (!position) { - throw new ApiError(404, "Result not found"); - } - - return res - .status(200) - .json(new ApiResponse(200, position, "Result fetched successfully!!")); -}); - -export { - createTournaments, - getTournaments, - registerPlayer, - tournamentInfo, - postResult, - getResults, - getIndividualResult, -} \ No newline at end of file +export { + createTournament, + getTournaments, + getTournamentInfo, + registerUser, +}; \ No newline at end of file diff --git a/Backend/src/helpers/checkDateTime.js b/Backend/src/helpers/checkDateTime.js new file mode 100644 index 0000000..daba599 --- /dev/null +++ b/Backend/src/helpers/checkDateTime.js @@ -0,0 +1,22 @@ +import { DateTime } from "luxon"; + +const check24HourFormat = (time) => { + const parsedTime = DateTime.fromFormat(time, "HH:mm"); + if (!parsedTime.isValid) { + return false; + } + return true; +}; + +const checkDateFormat = (date) => { + const parsedDate = DateTime.fromFormat(date, "yyyy-MM-dd"); + if (!parsedDate.isValid) { + return false; + } + return true; +} + +export { + check24HourFormat, + checkDateFormat, +}; \ No newline at end of file diff --git a/Backend/src/middlewares/isAdmin.middleware.js b/Backend/src/middlewares/isAdmin.middleware.js new file mode 100644 index 0000000..c4b804f --- /dev/null +++ b/Backend/src/middlewares/isAdmin.middleware.js @@ -0,0 +1,27 @@ +import ApiError from "../utils/ApiError.js"; +import jwt from "jsonwebtoken"; +import asyncHandler from "../utils/asyncHandler.js"; +import { User } from "../models/user.models.js"; +import { USER_ROLES } from "../constants.js"; + +const isAdmin = asyncHandler(async (req, res, next) => { + const token = req.cookies?.token || req.header("Authorization")?.replace("Bearer ", ""); + if (!token) { + throw new ApiError(401, "Unauthorized Access."); + } + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.id); + if (!user) { + throw new ApiError(401, "Unauthorized Access."); + } + if (user.role !== USER_ROLES[1]) { + throw new ApiError(403, "Forbidden Access."); + } + try { + next(); + } catch (error) { + throw new ApiError(500, error.message || "Internal Server Error"); + } +}); + +export default isAdmin; \ No newline at end of file diff --git a/Backend/src/models/tournament.models.js b/Backend/src/models/tournament.models.js index 539c544..bb46248 100644 --- a/Backend/src/models/tournament.models.js +++ b/Backend/src/models/tournament.models.js @@ -8,15 +8,19 @@ const tournamentScehma = new Schema({ unique: true, }, matchDate: { - type: Date, + type: String, required: true, }, matchTime: { - type: Date, + type: String, required: true, }, registrationEndDate: { - type: Date, + type: String, + required: true, + }, + registrationEndTime: { + type: String, required: true, }, totalSlots: { @@ -64,10 +68,21 @@ const tournamentScehma = new Schema({ ref: "User", } ], + // for finished registration isActive: { type: Boolean, default: true, }, + // for ongoing tournaments (registration completed) + isOngoing: { + type: Boolean, + default: false, + }, + // for finished tournaments + isCompleted: { + type: Boolean, + default: false, + }, // idp: {}, // refundForm: {}, }, { diff --git a/Backend/src/models/user.models.js b/Backend/src/models/user.models.js index 566a762..35fbb4f 100644 --- a/Backend/src/models/user.models.js +++ b/Backend/src/models/user.models.js @@ -1,6 +1,6 @@ import mongoose, { Schema } from "mongoose"; import bcrypt from "bcryptjs"; -import { USER_BADGES } from "../constants.js"; +import { USER_BADGES, USER_ROLES } from "../constants.js"; const userSchema = new Schema({ email: { @@ -44,7 +44,18 @@ const userSchema = new Schema({ canChangePassword: { type: Boolean, default: false, - } + }, + role: { + type: String, + enum: USER_ROLES, + default: USER_ROLES[0], + }, + registeredTournaments: [ + { + type: Schema.Types.ObjectId, + ref: "Tournament", + } + ], }, { timestamps: true, }); diff --git a/Backend/src/routes/tournament.routes.js b/Backend/src/routes/tournament.routes.js index 6d8afe0..63255f5 100644 --- a/Backend/src/routes/tournament.routes.js +++ b/Backend/src/routes/tournament.routes.js @@ -1,19 +1,29 @@ import { Router } from "express"; import verifyToken from "../middlewares/auth.middleware.js"; -import { createTournaments , getTournaments , registerPlayer , tournamentInfo , postResult ,getResults ,getIndividualResult} from "../controllers/tournament.controller.js"; +import isAdmin from "../middlewares/isAdmin.middleware.js"; +import { + createTournament, + getTournaments, + getTournamentInfo, + registerUser, +} from "../controllers/tournament.controller.js"; + +import { postResult } from "../controllers/result.controller.js"; const router = Router(); //admin controlled routes -router.route("/createTournament").post(createTournaments) -router.route("/postResult").post(postResult) -router.route("/getResults").get(verifyToken,getResults) +router.route("/createTournament").post(isAdmin, createTournament); + +// router.route("/postResult").post(postResult); +// router.route("/getResults").get(verifyToken,getResults); //user accessed routes -router.route("/getAllTournaments").get(verifyToken,getTournaments) -router.route("/getTournamentInfo").get(verifyToken,tournamentInfo) -router.route("/getIndividualResult").get(verifyToken,getIndividualResult) -router.route("/registerTournament").post(verifyToken,registerPlayer) +router.route("/getTournaments").get(verifyToken, getTournaments); +router.route("/getTournamentInfo").get(verifyToken, getTournamentInfo); +router.route("/registerTournament").post(verifyToken, registerUser); + +// router.route("/getIndividualResult").get(verifyToken,getIndividualResult); -export default router \ No newline at end of file +export default router; \ No newline at end of file