const express = require("express"); const sql = require("mssql"); const bcrypt = require("bcrypt"); const cors = require("cors"); const jwt = require("jsonwebtoken") const dotenv = require("dotenv") const serverPort = 3001; const dev = process.argv.length > 2 && process.argv[2] == "-dev"; const app = express(); app.use(express.json()); app.use((req, res, next) =>{ if (req.path.endsWith(".html")) { res.redirect(req.path.replace(".html", "")); return; } next(); }) app.use(express.static("public")) app.use(cors({ origin: [ "https://project.rochesterx.dev", "https://localhost:3001" ] })); let dbConfig = null; dbConfig = { user: "server", password: "TorusField1*", server: "mssql.rochesterx.dev", database: "Project", options: { trustServerCertificate: true } }; let pool = null; setupPool(); dotenv.config(); const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) { throw new Error("JWT_SECRET not set in environment."); } async function setupPool() { pool = await sql.connect(dbConfig); } app.post("/register", async (req, res) => { const { username, password, role } = req.body; try { const hash = await bcrypt.hash(password, 10); await pool.request() .input("username", sql.VarChar, username) .input("hash", sql.VarChar, hash) .input("role", sql.VarChar, role) .query("INSERT INTO Users (Username, PasswordHash, Role, CreatedAt) VALUES (@username, @hash, @role, SYSUTCDATETIME())"); res.send({ success: true, message: "User registered" }) } catch (err) { if (err.message.includes("Violation of UNIQUE KEY constraint")) { res.status(500).send({ success: false, message: `Username "${username}" is already taken.` }); } res.status(500).send({ success: false, message: err.message }); } }); app.post("/login", async (req, res) => { const { username, password } = req.body; try { const result = await pool.request() .input("username", sql.VarChar, username) .query("SELECT * FROM Users WHERE Username = @username"); if (result.recordset.length == 0) return res.status(400).send({ message: "User not found" }); const hash = result.recordset[0].PasswordHash; const isDeleted = result.recordset[0].IsDeleted; if (isDeleted === true) { return res.status(400).json({ message: "User not found (deleted)" }) } const match = await bcrypt.compare(password, hash); if (match){ const token = jwt.sign(result.recordset[0], JWT_SECRET, { expiresIn: "1h" }); res.send({ success: true, message: "Login successful", token }); await pool.request() .input("username", sql.VarChar, username) .query("UPDATE Users SET LastLogin = SYSUTCDATETIME() WHERE Username = @username"); console.log("Issued token: " + JSON.stringify(token)) } else res.status(401).send({ success: false, message: "Invalid credentials" }); } catch (err) { res.status(500).send({ success: false, message: err.message }); } }); app.post("/getWatchlist", authenticate, async (req, res) => { const { id } = req.body; const watchlist = await pool.request() .input("userID", sql.Int, req.user.Id) .input("id", sql.Int, id) .query(`SELECT Player.PlayerName, Player.Team, Player.Position, Player.PlayerID FROM Watch JOIN Player ON Watch.PlayerID = Player.PlayerID WHERE UserID = @userID`); res.status(200).json({ watchlist: watchlist.recordset }); }); app.post("/isWatched", authenticate, async (req, res) => { const { id } = req.body; const watchlist = await pool.request() .input("userID", sql.Int, req.user.Id) .input("id", sql.Int, id) .query(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); const isWatched = watchlist.recordset.some(row => row.PlayerID === parseInt(id)); res.status(200).json({ isWatched: isWatched }); }); app.post("/toggleWatched", authenticate, async (req, res) => { const { id } = req.body; const watchlist = await pool.request() .input("userID", sql.Int, req.user.Id) .input("id", sql.Int, id) .query(`SELECT PlayerID FROM Watch WHERE UserID = @userID`); console.log(watchlist.recordset); const isWatched = watchlist.recordset.some(row => row.PlayerID === parseInt(id)); console.log(isWatched); if (isWatched) { const result = await pool.request() .input("userID", sql.Int, req.user.Id) .input("id", sql.Int, id) .query(`DELETE FROM Watch WHERE UserID = @userID AND PlayerID = @id`); res.status(200).json({ message: "No longer watching player" }); return; } // Otherwise, watch the player const result = await pool.request() .input("userID", sql.Int, req.user.Id) .input("id", sql.Int, id) .query(`INSERT INTO Watch (UserID, PlayerID) VALUES (@userID, @id)`); res.status(200).json({ message: "Watching Player" }); }); app.post("/getPlayers", authenticate, async (req, res) => { const { player, positions } = req.body; const result = await pool.request() .input("query", sql.VarChar, player) .input("one", sql.VarChar, positions[0]) .input("two", sql.VarChar, positions[1]) .input("three", sql.VarChar, positions[2]) .input("four", sql.VarChar, positions[3]) .query(`SELECT p.PlayerID, p.PlayerName, c.TotalValue, p.Team, p.Position FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerName LIKE '%' + @query + '%' AND p.Position IN (@one, @two, @three, @four);`); res.status(200).json({ query: player, matches: result.recordset }); }); app.post("/getHighest", authenticate, async (req, res) => { const { amount } = req.body; const result = await pool.request() .input("amount", sql.Int, amount) .query(` SELECT TOP (@amount) p.PlayerID, p.PlayerName, p.[Position], p.Team, TotalValue FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID ORDER BY TotalValue DESC; `); res.status(200).json({ matches: result.recordset }); }); app.post("/getHighestOffense", authenticate, async (req, res) => { const { amount } = req.body; const result = await pool.request() .input("amount", sql.Int, amount) .query(` SELECT TOP (@amount) Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, SUM(total_yards) AS TotalYards, SUM(passing_yards) AS PassingYards, SUM(rushing_yards) AS RushingYards, SUM(receiving_yards) AS RecievingYards, CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown) AS AmendedTotalTDs, CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END AS PassTDs, SUM(receiving_touchdown) AS ReceivingTDs, SUM(rush_touchdown) AS RushTDs, SUM(offense_snaps) * 1.0 / SUM(team_offense_snaps) AS SnapPercentage, SUM(interception) + sum(fumble_lost) AS Turnovers, SUM(tackled_for_loss) AS TackledForLoss, CASE WHEN Player.Position = 'QB' THEN SUM(qb_dropback) - SUM(pass_attempts) - SUM(qb_scramble) ELSE 0 END AS Sacks, SUM(safety) AS Safties, SUM(total_yards) + ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) + (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) - ((SUM(interception) + sum(fumble_lost)) * 75) - ( SUM(tackled_for_loss)) - (SUM(safety) * 100.0) AS OffenseScore, AvgPerYear, (SUM(total_yards) + ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) + (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) - ((SUM(interception) + sum(fumble_lost)) * 75) - ( SUM(tackled_for_loss) ) - (SUM(safety) * 100.0)) / AvgPerYear AS PaydirtScore FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract ON Player.PlayerID = Contract.PlayerID WHERE season = 2024 AND SeasonType = 'REG' GROUP BY Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], Contract.AvgPerYear ORDER BY OffenseScore DESC; `); res.status(200).json({ matches: result.recordset }); }); app.post("/getPlayerStats", authenticate, async (req, res) => { const { playerID } = req.body; const result = await pool.request() .input("playerID", sql.Int, playerID) .query(` SELECT Player.PlayerID, Player.PlayerName, Season, SeasonType, Week, pass_attempts, complete_pass, total_yards, total_tds, interception, receptions, receiving_yards, receiving_touchdown, rush_attempts, rushing_yards, rush_touchdown, fumble FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID WHERE Player.PlayerID = @playerID ORDER BY Season, week; `); res.status(200).json({ matches: result.recordset }); }); app.post("/getPlayerScores", authenticate, async (req, res) => { const { playerID } = req.body; const result = await pool.request() .input("playerID", sql.Int, playerID) .query(` SELECT Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, SUM(total_yards) AS TotalYards, SUM(passing_yards) AS PassingYards, SUM(rushing_yards) AS RushingYards, SUM(receiving_yards) AS RecievingYards, CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown) AS AmendedTotalTDs, CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END AS PassTDs, SUM(receiving_touchdown) AS ReceivingTDs, SUM(rush_touchdown) AS RushTDs, SUM(offense_snaps) * 1.0 / SUM(team_offense_snaps) AS SnapPercentage, SUM(interception) + sum(fumble_lost) AS Turnovers, SUM(tackled_for_loss) AS TackledForLoss, CASE WHEN Player.Position = 'QB' THEN SUM(qb_dropback) - SUM(pass_attempts) - SUM(qb_scramble) ELSE 0 END AS Sacks, SUM(safety) AS Safties, SUM(total_yards) + ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) + (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) - ((SUM(interception) + sum(fumble_lost)) * 75) - ( SUM(tackled_for_loss) ) - (SUM(safety) * 100.0) AS OffenseScore, AvgPerYear, (SUM(total_yards) + ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) + (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) - ((SUM(interception) + sum(fumble_lost)) * 75) - ( SUM(tackled_for_loss) ) - (SUM(safety) * 100.0)) / AvgPerYear * 1000000 AS PaydirtScore FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract ON Player.PlayerID = Contract.PlayerID WHERE season = 2024 AND SeasonType = 'REG' AND Player.PlayerID = @playerID GROUP BY Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], Contract.AvgPerYear ORDER BY PaydirtScore DESC; `); res.status(200).json({ matches: result.recordset }); }); app.post("/getHighestPaydirt", authenticate, async (req, res) => { const { amount } = req.body; const result = await pool.request() .input("amount", sql.Int, amount) .query(` SELECT TOP (@amount) Player.PlayerID, Player.[Position], Player.PlayerName, Player.Team, SUM(total_yards) AS TotalYards, SUM(passing_yards) AS PassingYards, SUM(rushing_yards) AS RushingYards, SUM(receiving_yards) AS RecievingYards, CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown) AS AmendedTotalTDs, CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END AS PassTDs, SUM(receiving_touchdown) AS ReceivingTDs, SUM(rush_touchdown) AS RushTDs, SUM(offense_snaps) * 1.0 / SUM(team_offense_snaps) AS SnapPercentage, SUM(interception) + sum(fumble_lost) AS Turnovers, SUM(tackled_for_loss) AS TackledForLoss, CASE WHEN Player.Position = 'QB' THEN SUM(qb_dropback) - SUM(pass_attempts) - SUM(qb_scramble) ELSE 0 END AS Sacks, SUM(safety) AS Safties, SUM(total_yards) + ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) + (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) - ((SUM(interception) + sum(fumble_lost)) * 75) - ( SUM(tackled_for_loss)) - (SUM(safety) * 100.0) AS OffenseScore, AvgPerYear, (SUM(total_yards) + ((CASE WHEN Player.Position = 'QB' THEN (SUM(pass_touchdown)) ELSE 0 END + SUM(receiving_touchdown) + SUM(rush_touchdown)) * 50) + (SUM(offense_snaps) * 100.0 / SUM(team_offense_snaps)) - ((SUM(interception) + sum(fumble_lost)) * 75) - ( SUM(tackled_for_loss) ) - (SUM(safety) * 100.0)) / AvgPerYear * 1000000 AS PaydirtScore FROM Player JOIN DatasetPlayerStats ON Player.PlayerID = DatasetPlayerStats.PlayerID JOIN Contract ON Player.PlayerID = Contract.PlayerID WHERE season = 2024 AND SeasonType = 'REG' GROUP BY Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], Contract.AvgPerYear ORDER BY PaydirtScore DESC; `); res.status(200).json({ matches: result.recordset }); }); app.post("/getPlayer", authenticate, async (req, res) => { const { id } = req.body; const result = await pool.request() .input("query", sql.VarChar, id) .query(`SELECT p.PlayerName, p.PlayerID, c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, c.EndYear, p.Team, p.Position, p.Height, p.Weight FROM Player AS p JOIN Contract AS c ON p.PlayerID = c.PlayerID WHERE p.PlayerID = @query`); if (result.recordset.length !== 1) { res.status(400).json({ success: false }) return; } res.status(200).json({ success: true, match: result.recordset[0] }); }); app.post("/getInfo", authenticate, async (req, res) => { const userData = req.user; res.status(200).json(userData); }); app.post("/getCourses", authenticate, async (req, res) => { const result = await pool.request() .query("SELECT * FROM Courses"); const courses = result.recordset; res.status(200).json(courses); }); app.post("/setInfo", authenticate, async (req, res) => { const { firstName, lastName, dob } = req.body; try { await pool.request() .input("username", sql.VarChar, req.user.Username) .input("firstName", sql.NVarChar(50), firstName) .input("lastName", sql.NVarChar(50), lastName) .input("dob", sql.Date, dob || null) .query(` UPDATE Users SET FirstName = @firstName, LastName = @lastName, DOB = @dob WHERE Username = @username `); } catch (error) { console.log(error); if (error.message.includes("failed for parameter 'dob'.")) { res.status(500).json({ message: "Must input date of birth" }) return; } res.status(500).json({ message: "Update request failed" }) return; } var updatedUser = req.user; updatedUser.FirstName = firstName; updatedUser.LastName = lastName; updatedUser.DOB = dob; const token = jwt.sign(updatedUser, JWT_SECRET); console.log("Issued token: " + JSON.stringify(token)) res.status(200).send({ success: true, message: "Information updated successfully", token }); }); app.post("/delete", authenticate, async (req, res) => { let { username, actor } = req.body; if (username === true) { username = req.user.Username; } if (actor === true) { actor = req.user.Username; } console.log(`Deleting user ${username}`); await pool.request() .input("username", sql.VarChar, username) .input("actor", sql.VarChar, actor) .query("UPDATE Users SET IsDeleted = 1, DeletedAt = SYSUTCDATETIME(), DeletedBy = @actor WHERE Username = @username"); console.log(`User ${username} deleted`); res.status(200).json({ message: `User "${username}" deleted.` }); }); async function authenticate(req, res, next) { try { const authenticationHeader = req.headers["authorization"]; console.log("authenticationheader: " + authenticationHeader); const token = authenticationHeader.split(" ")[1]; console.log(JSON.stringify(authenticationHeader)); const decoded = jwt.verify(token, JWT_SECRET); req.user = decoded; console.log(req.user); console.log(decoded.Username + " authenticated"); } catch (error) { console.log("Authentication header missing"); console.log(error); res.status(401).json({ message: "You are not logged in", error: error, logout: true }); return; } next(); } app.get("/player/:id", (req, res) => { res.sendFile(__dirname + "/public/player.html"); }) app.get("/search", (req, res) => { res.sendFile(__dirname + "/public/search.html"); }) app.get("/home", (req, res) => { res.sendFile(__dirname + "/public/home.html"); }) app.get("/info", (req, res) => { res.sendFile(__dirname + "/public/info.html"); }) app.get("/register", (req, res) => { res.sendFile(__dirname + "/public/register.html"); }) app.get("/login", (req, res) => { res.sendFile(__dirname + "/public/login.html"); }) app.listen(serverPort, "0.0.0.0", () => console.log(`Running ${dev ? "dev " : ""}server on port ${serverPort}`));