From 8f00e688e0d98fd16517b37523112efc6e67c078 Mon Sep 17 00:00:00 2001 From: Holly Date: Sun, 21 Apr 2024 15:35:47 +0100 Subject: [PATCH] add db base --- server/config.example.json | 10 ++++ server/index.ts | 17 +++--- server/objects/Config.ts | 12 ++++- server/objects/Database.ts | 101 +++++++++++++++++++++++++++++++++++ server/objects/RemoteUser.ts | 29 ++++++++++ server/objects/User.ts | 62 ++++++++++++--------- server/package-lock.json | 101 +++++++++++++++++++++++++++++++++++ server/package.json | 1 + server/repos/UserRepo.ts | 52 ++++++++++++++++++ 9 files changed, 352 insertions(+), 33 deletions(-) create mode 100644 server/config.example.json create mode 100644 server/objects/Database.ts create mode 100644 server/objects/RemoteUser.ts create mode 100644 server/repos/UserRepo.ts diff --git a/server/config.example.json b/server/config.example.json new file mode 100644 index 0000000..734423c --- /dev/null +++ b/server/config.example.json @@ -0,0 +1,10 @@ +{ + "port": 38195, + "database": { + "address": "localhost", + "port": 3306, + "username": "user", + "password": "password", + "name": "MultiProbe" + } +} \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index 1f6ff65..7c2da45 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2,16 +2,19 @@ import { createReader, createWriter, Endian } from "bufferstuff"; import { WebSocketServer } from "ws"; import Config from "./objects/Config"; import FunkyArray from "./objects/FunkyArray"; -import User from "./objects/User"; +import RemoteUser from "./objects/RemoteUser"; import { MessageType } from "./enums/MessageType"; +import Database from "./objects/Database"; -const users = new FunkyArray(); +const users = new FunkyArray(); + +new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name); const server = new WebSocketServer({ port: Config.port }, () => console.log(`Server listening at ${Config.port}`)); -function sendToAllButSelf(user:User, data:Buffer) { +function sendToAllButSelf(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { if (otherUser.id !== user.id && otherUser.currentURL === user.currentURL) { otherUser.send(data); @@ -19,7 +22,7 @@ function sendToAllButSelf(user:User, data:Buffer) { }); } -function sendToAll(user:User, data:Buffer) { +function sendToAll(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { if (otherUser.currentURL === user.currentURL) { otherUser.send(data); @@ -29,7 +32,7 @@ function sendToAll(user:User, data:Buffer) { server.on("connection", (socket) => { const myUUID = crypto.randomUUID(); - let user:User; + let user:RemoteUser; function closeOrError() { if (users.has(myUUID)) { @@ -60,7 +63,7 @@ server.on("connection", (socket) => { page = ""; } let lengthOfUsernames = 0; - const usersOnPage = new Array(); + const usersOnPage = new Array(); await users.forEach(otherUser => { if (otherUser.currentURL === page) { usersOnPage.push(otherUser); @@ -71,7 +74,7 @@ server.on("connection", (socket) => { for (const otherUser of usersOnPage) { usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY); } - user = users.set(myUUID, new User(socket, username, page, rawURL)); + user = users.set(myUUID, new RemoteUser(socket, username, page, rawURL)); sendToAllButSelf(user, createWriter(Endian.LE, 6 + username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(username).toBuffer()); user.send(usersToSend.toBuffer()); break; diff --git a/server/objects/Config.ts b/server/objects/Config.ts index 9e7cbe1..f528254 100644 --- a/server/objects/Config.ts +++ b/server/objects/Config.ts @@ -5,5 +5,15 @@ const config = JSON.parse(readFileSync("./config.json").toString()); export default class Config { public constructor() { throw new Error("Static Class"); } - public static port:number = config.port; + public static port:number = config.port; + + public static database:ConfigDatabase = config.database; +} + +interface ConfigDatabase { + address: string, + port: number, + username: string, + password: string, + name: string } \ No newline at end of file diff --git a/server/objects/Database.ts b/server/objects/Database.ts new file mode 100644 index 0000000..dbd9256 --- /dev/null +++ b/server/objects/Database.ts @@ -0,0 +1,101 @@ +import { createPool, Pool, RowDataPacket } from "mysql2"; + +export type DBInDataType = string | number | null | undefined; + +export default class Database { + private connectionPool:Pool; + private static readonly CONNECTION_LIMIT = 128; + + public connected:boolean = false; + + public static Instance:Database; + + public constructor(databaseAddress:string, databasePort:number = 3306, databaseUsername:string, databasePassword:string, databaseName:string) { + this.connectionPool = createPool({ + connectionLimit: Database.CONNECTION_LIMIT, + host: databaseAddress, + port: databasePort, + user: databaseUsername, + password: databasePassword, + database: databaseName + }); + + console.log(`DB connection pool created. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`); + + Database.Instance = this; + } + + public execute(query:string, data?:Array) { + return new Promise((resolve, reject) => { + this.connectionPool.getConnection((err, connection) => { + if (err) { + return reject(err); + } + + if (data == null) { + connection.execute(query, (err, result) => { + if (err) { + connection.release(); + return reject(err); + } + + resolve(result !== undefined); + }); + } else { + connection.execute(query, data, (err, result) => { + if (err) { + connection.release(); + return reject(err); + } + + resolve(result !== undefined); + }); + } + }); + }); + } + + public query(query:string, data?:Array) { + return new Promise((resolve, reject) => { + this.connectionPool.getConnection((err, connection) => { + if (err) { + return reject(err); + } else { + // Use old query + if (data == null) { + connection.query(query, (err, rows) => { + connection.release(); + if (err) { + return reject(err); + } + + resolve(rows); + connection.release(); + }); + } + // Use new prepared statements w/ placeholders + else { + connection.execute(query, data, (err, rows) => { + connection.release(); + if (err) { + return reject(err); + } + + resolve(rows); + connection.release(); + }); + } + } + }); + }); + } + + public async querySingle(query:string, data?:Array) { + const dbData = await this.query(query, data); + if (dbData != null && dbData.length > 0) { + return dbData[0]; + } + + return null; + } +} \ No newline at end of file diff --git a/server/objects/RemoteUser.ts b/server/objects/RemoteUser.ts new file mode 100644 index 0000000..cce9dac --- /dev/null +++ b/server/objects/RemoteUser.ts @@ -0,0 +1,29 @@ +import { WebSocket } from "ws"; + +export default class RemoteUser { + private static USER_IDS = 0; + + private readonly socket:WebSocket; + public readonly id:number; + public readonly username:string; + public readonly currentURL:string; + public readonly rawURL:string = ""; + public cursorX:number = 0; + public cursorY:number = 0; + public allowedPings:number; + public lastPingReset:number; + + constructor(socket:WebSocket, username:string, currentURL:string, rawURL:string) { + this.socket = socket; + this.id = RemoteUser.USER_IDS++; + this.username = username; + this.currentURL = currentURL; + this.rawURL = rawURL; + this.allowedPings = 10; + this.lastPingReset = Date.now(); + } + + send(data:Buffer) { + this.socket.send(data); + } +} \ No newline at end of file diff --git a/server/objects/User.ts b/server/objects/User.ts index d1372a3..9ca909f 100644 --- a/server/objects/User.ts +++ b/server/objects/User.ts @@ -1,29 +1,41 @@ -import { WebSocket } from "ws"; - export default class User { - private static USER_IDS = 0; + public Id:number; + public Username:string; + public PasswordSalt:string; + public PasswordHash:string; + public CreatedByUserId:number; + public CreatedDatetime:Date; + public LastModifiedByUserId:number; + public LastModifiedDatetime:Date; + public DeletedByUserId:number; + public DeletedDatetime:Date; + public IsDeleted:boolean; - private readonly socket:WebSocket; - public readonly id:number; - public readonly username:string; - public readonly currentURL:string; - public readonly rawURL:string = ""; - public cursorX:number = 0; - public cursorY:number = 0; - public allowedPings:number; - public lastPingReset:number; - - constructor(socket:WebSocket, username:string, currentURL:string, rawURL:string) { - this.socket = socket; - this.id = User.USER_IDS++; - this.username = username; - this.currentURL = currentURL; - this.rawURL = rawURL; - this.allowedPings = 10; - this.lastPingReset = Date.now(); - } - - send(data:Buffer) { - this.socket.send(data); + public constructor(id?:number, username?:string, passwordSalt?:string, passwordHash?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) { + if (typeof(id) == "number" && typeof(username) == "string" && typeof(passwordHash) == "string" && typeof(passwordSalt) == "string" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") { + this.Id = id; + this.Username = username; + this.PasswordHash = passwordHash; + this.PasswordSalt = passwordSalt; + this.CreatedByUserId = createdByUserId; + this.CreatedDatetime = createdDateTime; + this.LastModifiedByUserId = lastModifiedByUserId; + this.LastModifiedDatetime = lastModifiedDatetime; + this.DeletedByUserId = deletedByUserId; + this.DeletedDatetime = deletedDatetime; + this.IsDeleted = isDeleted; + } else { + this.Id = Number.MIN_VALUE; + this.Username = ""; + this.PasswordHash = ""; + this.PasswordSalt = ""; + this.CreatedByUserId = Number.MIN_VALUE; + this.CreatedDatetime = new Date(0); + this.LastModifiedByUserId = Number.MIN_VALUE; + this.LastModifiedDatetime = new Date(0); + this.DeletedByUserId = Number.MIN_VALUE; + this.DeletedDatetime = new Date(0); + this.IsDeleted = false; + } } } \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index c734060..b873599 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "bufferstuff": "^1.5.1", + "mysql2": "^3.9.7", "ws": "^8.16.0" }, "devDependencies": { @@ -539,6 +540,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -761,6 +770,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -929,6 +946,17 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1122,6 +1150,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -1243,6 +1276,11 @@ "node": ">=4" } }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1282,6 +1320,51 @@ "node": "*" } }, + "node_modules/mysql2": { + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", + "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", + "dependencies": { + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru-cache": "^8.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -1629,6 +1712,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -1644,6 +1732,11 @@ "node": ">=10" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1787,6 +1880,14 @@ "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/string.prototype.padend": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", diff --git a/server/package.json b/server/package.json index ce57416..7d32b2c 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "license": "MIT", "dependencies": { "bufferstuff": "^1.5.1", + "mysql2": "^3.9.7", "ws": "^8.16.0" }, "devDependencies": { diff --git a/server/repos/UserRepo.ts b/server/repos/UserRepo.ts new file mode 100644 index 0000000..aeb9f70 --- /dev/null +++ b/server/repos/UserRepo.ts @@ -0,0 +1,52 @@ +import Database from "../objects/Database"; +import User from "../objects/User"; + +export default class UsersRepo { + public static async selectById(id:number) { + const dbUser = await Database.Instance.query("SELECT * FROM User WHERE Id = ? LIMIT 1", [id]); + if (dbUser == null || dbUser.length === 0) { + return null; + } else { + const user = new User(); + populateUserFromDB(user, dbUser[0]); + return user; + } + } + + public static async selectByUsername(username:string) { + const dbUser = await Database.Instance.query("SELECT * FROM User WHERE Username = ? LIMIT 1", [username]); + if (dbUser == null || dbUser.length === 0) { + return null; + } else { + const user = new User(); + populateUserFromDB(user, dbUser[0]); + return user; + } + } + + public static async insertUpdate(user:User) { + if (user.Id === Number.MIN_VALUE) { + await Database.Instance.query("INSERT users (Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ + user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId, user.LastModifiedDatetime.getTime(), user.DeletedByUserId, user.DeletedDatetime.getTime(), Number(user.IsDeleted) + ]); + } else { + await Database.Instance.query(`UPDATE users SET Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [ + user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId, user.LastModifiedDatetime.getTime(), user.DeletedByUserId, user.DeletedDatetime.getTime(), Number(user.IsDeleted), user.Id + ]); + } + } +} + +function populateUserFromDB(user:User, dbUser:any) { + user.Id = dbUser.Id; + user.Username = dbUser.Username; + user.PasswordHash = dbUser.PasswordHash; + user.PasswordSalt = dbUser.PasswordSalt; + user.CreatedByUserId = dbUser.CreatedByUserId; + user.CreatedDatetime = dbUser.CreatedDatetime; + user.LastModifiedByUserId = dbUser.LastModifiedByUserId; + user.LastModifiedDatetime = dbUser.LastModifiedDatetime; + user.DeletedByUserId = dbUser.DeletedByUserId; + user.DeletedDatetime = dbUser.DeletedDatetime; + user.IsDeleted = dbUser.IsDeleted; +} \ No newline at end of file