515 lines
18 KiB
JavaScript
515 lines
18 KiB
JavaScript
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) ORDER BY p.PlayerName;`);
|
|
|
|
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, TrueAvgPerYear, Years
|
|
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,
|
|
c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, c.EndYear, Player.Height, Player.Weight,
|
|
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 AS c ON Player.PlayerID = c.PlayerID
|
|
WHERE season = 2024 AND SeasonType = 'REG' AND Player.PlayerID = @playerID
|
|
GROUP BY c.TotalValue, c.TrueAvgPerYear, c.Years, c.StartYear, Player.Height, Player.Weight, c.EndYear, Player.PlayerID, Player.PlayerName, Player.Team, Player.[Position], c.AvgPerYear
|
|
ORDER BY PaydirtScore DESC;
|
|
`);
|
|
console.log(result.recordset);
|
|
|
|
res.status(200).json({ match: result.recordset[0] });
|
|
});
|
|
|
|
|
|
|
|
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 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}`));
|