Initial commit

This commit is contained in:
2025-03-13 16:05:09 +01:00
commit 5950d5ae9d
44 changed files with 5505 additions and 0 deletions

2311
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
backend/package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "doublesnake_backend",
"version": "1.0.0",
"description": "Eine Snake Game welches man zu zweit Spielen kann!",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "node ./src/server.js",
"nm": "nodemon ./src/server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"body-parser": "^1.20.3",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-session": "^1.18.1",
"mysql2": "^3.12.0",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"nodemon": "^3.1.9"
}
}

View File

@@ -0,0 +1,23 @@
const mySql = require("mysql2");
const UserManager = require("./UserManager/UserManager");
class DataBaseManager {
constructor(host, database) {
this.connection = mySql.createConnection({
host: host,
user: "root",
password:"",
database: database
});
this.connection.connect((err) => {
if (err) {
throw err;
}
});
this.usermanager = new UserManager(this.connection);
}
}
module.exports = DataBaseManager;

View File

@@ -0,0 +1,27 @@
const bcrypt = require("bcrypt");
class User {
constructor(userJSON) {
this.id = userJSON.id;
this.username = userJSON.username;
this.email = userJSON.email;
this.password = userJSON.password;
this.fullName = userJSON.fullName;
this.createdAt = userJSON.createdAt;
}
async doesPassMatch(passwordPlain) {
return await bcrypt.compare(passwordPlain, this.password);
}
toUserJSON(){
return {
id: this.id,
username: this.username,
email: this.email,
fullName: this.fullName,
}
}
}
module.exports = User;

View File

@@ -0,0 +1,102 @@
const mySql = require("mysql2")
const bcrypt = require("bcrypt")
const User = require("./User")
class UserManager {
/**@param {mySql.Connection} connection*/
constructor(connection) {
this.connection = connection;
}
// Gebe zumindest {username: "x", password: "x"} mit!
// 0: Fehler (User Existiert schon oder Fehler) | 1: Funktioniert (User Erstellt!)
async createUser(userJson) {
if (!userJson.username || !userJson.password) return 0;
const user = await this.getUser({username: userJson.username});
if(user) return 0;
try {
const passHash = await bcrypt.hash(userJson.password, 10);
this.connection.query(`INSERT INTO users (username, email, password, fullName) VALUES ("${userJson.username}",` +
` ${userJson.email ? `"${userJson.email}"` : "NULL"}, "${passHash}", ${userJson.fullName ? `"${userJson.fullName}"` : "NULL"})`);
return 1;
} catch (error) {
console.log(error);
}
}
// Gebe nichts mit um alle user zu bekommen oder {id: 1} oder {username: "x"} um darauf einen bestimmten zu bekommen;
async getUser(userData) {
let selectionString = "SELECT * FROM users";
if(userData?.id){
selectionString = `SELECT * FROM users WHERE id=${userData.id}`;
}
else if(userData?.username){
selectionString = `SELECT * FROM users WHERE username="${userData.username}"`;
}
try {
const response = await this.connection.promise().query(selectionString);
if(response[0].length === 0){
return null;
}
else if(response[0].length === 1){
const x = response[0][0];
return new User(x);
}
else{
let userArray = [];
response[0].forEach(x => {
const user = new User(x);
userArray.push(user);
});
return userArray;
}
} catch (error) {
console.log(error);
}
}
// 0: User Existiert nicht | 1: Funktioniert
// id = 1 | newUserData = {email: "x", password: "xx", "fullName": "xxx"} <-- BRaucht alle Argeumente sonst null, username nicht änderbar
async updateUser(id, newUserData){
const user = await this.getUser({id});
if(!user) return 0;
if(!newUserData.password) return 0;
try {
this.connection.query(
`UPDATE users SET
username="${newUserData.username}",
email=${newUserData.email ? `"${newUserData.email}"` : "NULL"},
fullName=${newUserData.fullName ? `"${newUserData.fullName}"` : "NULL"},
password="${newUserData.password}"
WHERE id = ${id}`
)
return 1;
} catch (error) {
console.log(error)
}
}
// 0: Es gibt den User nicht | 1: Funktioniert
async delteUser(id){
const user = await this.getUser({id});
if(!user) return 0;
try {
this.connection.query(`DELETE FROM users where id=${id}`);
return 1;
} catch (error) {
console.log(error)
}
}
}
module.exports = UserManager;

View File

@@ -0,0 +1,68 @@
const express = require("express");
const sessions = require("express-session");
const bp = require("body-parser");
const path = require("path");
const DataBaseManager = require("../Database/DataBaseManager");
const AccountRoute = require("./Routes/AccountRoute");
const DashboardRoute = require("./Routes/DashboardRoute");
class ExpressManager{
/**@param {DataBaseManager} dbManager*/
constructor(dbManager){
this.db = dbManager;
this.app = express();
this.sessionMiddleware = sessions({
secret: process.env.SESSION_KEY,
saveUninitialized: false,
resave: false,
cookie: {maxAge: 24 * 60 * 60 * 1000}
});
this.app.use((req, res, next) => { this.logger(req, res, next) });
this.app.use(this.sessionMiddleware);
this.app.use((req, res, next) => { this.needAuth(req, res, next) });
this.app.use(bp.urlencoded({extended: false}));
this.app.use(express.json());
this.app.use(bp.json());
this.app.use(express.static(path.join(__dirname, "./../../../frontend")));
// Routen wo man für Angemeldet sein muss
this.authRoutes = [
"/api/dashboard",
"/dashboard"
];
// Routen Einbinden
this.app.use("/api/account/", new AccountRoute(this.db).router);
this.app.use("/api/dashboard/", new DashboardRoute(this.db).router);
}
/**@param {express.Request} req @param {express.Response} res @param {express.NextFunction} next*/
logger(req, res, next){
const date = new Date();
console.log(`${date.toTimeString().slice(0, 8)} | ${req.method} | ${req.url}`);
next();
}
/**@param {express.Request} req @param {express.Response} res @param {express.NextFunction} next*/
needAuth(req, res, next){
let isProtectedRoute = false;
this.authRoutes.forEach(route => {
if(req.url.startsWith(route)) isProtectedRoute = true;
});
// Geht zum Login wenn User versucht Routen aufzurufen wofür man angemeldet sein muss
if(isProtectedRoute && !req.session.user?.isSet) return res.redirect("/login");
// Geht zum Dashboard wenn der Nutzer versucht sich zu registrieren oder einzuloggen wenn er angemeldet ist
if(req.session.user?.isSet && (req.url.startsWith("/login") || req.url.startsWith("/register"))){
return res.redirect("/dashboard");
}
next();
}
}
module.exports = ExpressManager;

View File

@@ -0,0 +1,84 @@
const express = require("express");
const User = require("../../Database/UserManager/User");
const DataBaseManager = require("../../Database/DataBaseManager");
const bcrypt = require("bcrypt");
class AccountRoute {
/**@param {DataBaseManager} dbManager*/
constructor(dbManager) {
this.router = express.Router();
this.db = dbManager;
this.router.post("/register", async (req, res) => await this.register(req, res));
this.router.post("/login", async (req, res) => await this.login(req, res));
this.router.post("/update", async (req, res) => await this.update(req, res));
this.router.get("/logout", async (req, res) => await this.logout(req, res));
}
/**@param {express.Request} req @param {express.Response}*/
async register(req, res){
const body = req.body;
if(!body.username || !body.password) return res.redirect("/register?error=1");
const user = new User(body);
const result = await this.db.usermanager.createUser(user);
if(result !== 1) return res.redirect("/register?error=2");
res.redirect("/login");
}
/**@param {express.Request} req @param {express.Response}*/
async login(req, res){
const body = req.body;
if(!body.username || !body.password) return res.redirect("/login?error=1")
const user = await this.db.usermanager.getUser({username: body.username});
if(!user) return res.redirect("/login?error=2")
const passwordMatch = await user.doesPassMatch(body.password);
if(!passwordMatch) return res.redirect("/login?error=2")
req.session.user = {
id: user.id,
username: user.username,
isSet: true
}
res.redirect("/dashboard")
}
/**@param {express.Request} req @param {express.Response}*/
async update(req, res){
const user = await this.db.usermanager.getUser({id: req.session.user.id});
const body = req.body;
if(user.username !== body.username){
const checkUser = await this.db.usermanager.getUser({username: body.username});
if(checkUser) return res.redirect("/dashboard/account/?error=1");
user.username = body.username;
}
user.email = body.email;
user.fullName = body.fullName;
if(body.password !== ""){
const passHash = await bcrypt.hash(body.password, 10);
user.password = passHash;
}
await this.db.usermanager.updateUser(user.id, user);
res.redirect("/dashboard/account");
}
/**@param {express.Request} req @param {express.Response}*/
async logout(req, res){
req.session.destroy();
res.redirect("/");
}
}
module.exports = AccountRoute;

View File

@@ -0,0 +1,23 @@
const express = require("express");
const User = require("../../Database/UserManager/User");
const DataBaseManager = require("../../Database/DataBaseManager");
class DashboardRoute{
/**@param {DataBaseManager} dbManager*/
constructor(dbManager){
this.router = express.Router();
this.db = dbManager;
this.router.get("/", async (req, res) => await this.userInfo(req, res))
}
/**@param {express.Request} req @param {express.Response}*/
async userInfo(req, res){
const user = await this.db.usermanager.getUser({id: req.session.user.id});
res.json(user.toUserJSON());
}
}
module.exports = DashboardRoute;

View File

@@ -0,0 +1,50 @@
const socketIO = require("socket.io");
const SocketUser = require("./SocketUser");
class Lobby {
/** @param {socketIO.Server} io @param {Map<number, SocketUser>} users*/
constructor(io, code) {
this.io = io;
this.code = code;
/** @type {Array<SocketUser>} */
this.users = [];
}
addUser(user) {
if (this.users.length >= 2) return 1;
this.users.push(user);
this.sendLobbyUserUpdate();
return 0;
}
removeUser(user) {
const index = this.users.findIndex(u => u.id === user.id);
if (index === -1) return 1;
this.users.splice(index, 1);
this.sendLobbyUserUpdate();
if (this.users.length === 0) return 2;
return 0;
}
sendLobbyUserUpdate() {
const data = {
code: this.code,
users: this.users.map(user => ({
id: user.id,
username: user.username
}))
}
this.io.to(`lobby-${this.code}`).emit("lobbyUserUpdate", data);
}
}
module.exports = Lobby;

View File

@@ -0,0 +1,12 @@
const socketIO = require("socket.io");
class SocketUser {
/** @param {number} id @param {string} username @param {socketIO.Socket} socket */
constructor(id, username, socket) {
this.id = id;
this.username = username;
this.socket = socket;
}
}
module.exports = SocketUser;

View File

@@ -0,0 +1,60 @@
const socketIO = require("socket.io");
const LobbyManager = require("./LobbyManager");
const SocketUser = require("./Classes/SocketUser");
class ClientHandler {
/** @param {socketIO.Socket} socket @param {LobbyManager} lobbyManager */
constructor(socket, lobbyManager) {
this.socket = socket;
this.lobbyManager = lobbyManager;
this.user = new SocketUser(
this.socket.request.session.user.id,
this.socket.request.session.user.username,
this.socket
);
this.currentLobbyCode = null;
this.socket.on("createLobby", () => { this.createLobby() })
this.socket.on("joinLobby", (code) => { this.joinLobby(code) })
this.socket.on("refreshLobby", () => { this.refreshLobby() })
this.socket.on("disconnect", () => { this.disconnect() })
}
createLobby(){
if(this.currentLobbyCode) return;
const lobbyCode = this.lobbyManager.createLobby(this.user);
this.currentLobbyCode = lobbyCode;
this.socket.join(`lobby-${lobbyCode}`);
this.socket.emit("lobbyCreated", lobbyCode);
this.refreshLobby();
}
joinLobby(code){
if(this.currentLobbyCode) return;
const response = this.lobbyManager.joinLobby(code, this.user);
if(response === 1) return this.socket.emit("lobbyJoinError", "Diese Lobby existiert nicht");
if(response === 2) return this.socket.emit("lobbyJoinError", "Die Lobby ist schon voll");
this.currentLobbyCode = response;
this.socket.join(`lobby-${response}`)
this.socket.emit("lobbyJoined", response);
this.refreshLobby();
}
refreshLobby(){
this.lobbyManager.refreshLobby(this.currentLobbyCode);
}
disconnect(){
this.lobbyManager.removeFromLobby(this.currentLobbyCode, this.user);
}
}
module.exports = ClientHandler;

View File

@@ -0,0 +1,65 @@
const socketIO = require("socket.io");
const Lobby = require("./Classes/Lobby");
const SocketUser = require("./Classes/SocketUser");
class LobbyManager {
/** @param {socketIO.Server} io */
constructor(io) {
this.io = io;
/** @type {Map<string, Lobby>}*/
this.lobbys = new Map();
// Zeigt die Anzahl der Lobbys an | Für Testing
// setInterval(() => {
// console.log(this.lobbys.size);
// }, 1000);
}
/** @param {SocketUser} user */
createLobby(user){
const code = this.generateNonExistingCode();
const lobby = new Lobby(this.io, code);
lobby.addUser(user);
this.lobbys.set(code, lobby);
return code;
}
/** @param {string} code @param {SocketUser} user */
joinLobby(code, user){
if (!this.lobbys.has(code)) return 1;
const lobby = this.lobbys.get(code);
const response = lobby.addUser(user);
if(response === 1) return 2;
return lobby.code;
}
generateNonExistingCode() {
let code;
do {
code = Math.random().toString(36).substring(2, 8).toUpperCase();
} while (this.lobbys.has(code));
return code;
}
/** @param {SocketUser} user */
removeFromLobby(code, user){
if(!this.lobbys.has(code)) return;
const lobby = this.lobbys.get(code);
const reponse = lobby.removeUser(user);
if (reponse === 2) this.lobbys.delete(code);
}
refreshLobby(code){
if(!this.lobbys.has(code)) return;
const lobby = this.lobbys.get(code);
lobby.sendLobbyUserUpdate();
}
}
module.exports = LobbyManager;

View File

@@ -0,0 +1,69 @@
const socketIO = require("socket.io");
const http = require("http");
const DataBaseManager = require("../Database/DataBaseManager");
const ExpressManager = require("../Express/ExpressManager");
const LobbyManager = require("./LobbyManager/LobbyManager");
const ClientHandler = require("./LobbyManager/ClientHandler");
require("dotenv").config();
class SocketIOManager {
/** @param {DataBaseManager} dbManager @param {ExpressManager} expressManager */
constructor(dbManager, expressManager) {
this.db = dbManager;
this.express = expressManager;
this.server = http.createServer(this.express.app);
/** @type {import("socket.io").Server} */
this.io = socketIO(this.server);
// Dadurch bekommen wir Zugriff auf den Eingeloggten Nutzer
this.io.use((socket, next) => {
this.express.sessionMiddleware(socket.request, socket.request.res || {}, next);
});
this.io.use((socket, next) => {this.useAuth(socket, next)});
/*
Der LobbyManager kümmert sich um alle Lobbys
Der LobbyHandler kümmert sich um die einzelnen Anfragen der Clients
*/
this.lobbyManager = new LobbyManager(this.io);
this.io.on("connection", (socket) => { this.sendToRightManager(socket) });
// Startet express und socket.io
this.server.listen(process.env.PORT, () => {
console.log(`Der Server ist gestartet und unter http://localhost:${process.env.PORT} erreichbar!`);
});
}
/** @param {socketIO.Socket} socket @param {socketIO.ExtendedError} next */
useAuth(socket, next){
// User muss eingeloggt sein
if(!socket.request.session.user) return next(new Error("Nicht Eingeloggt!"));
// Der User darf nur eine verbindung gleichzeitig haben
const userId = socket.request.session.user.id;
const socketConnections = this.io.sockets.sockets;
socketConnections.forEach((socket) => {
if(socket.request.session.user.id === userId) return next(new Error("Irgendwer anders spielt schon!"));
});
next();
}
/** @param {socketIO.Socket} socket - The socket instance. */
sendToRightManager(socket){
socket.on("requestIdentification", () => {
socket.emit("identification", socket.request.session.user);
});
socket.on("hereForLobby", () => {
new ClientHandler(socket, this.lobbyManager);
});
}
}
module.exports = SocketIOManager;

7
backend/src/server.js Normal file
View File

@@ -0,0 +1,7 @@
const DataBaseManager = require("./Database/DataBaseManager");
const ExpressManager = require("./Express/ExpressManager");
const SocketIOManager = require("./SocketIO/SocketIOManager");
const databaseManager = new DataBaseManager("localhost", "doublesnake");
const expressManger = new ExpressManager(databaseManager);
const socketIOManager = new SocketIOManager(databaseManager, expressManger);