add db base

This commit is contained in:
Holly Stubbs 2024-04-21 15:35:47 +01:00
parent c895312f7a
commit 8f00e688e0
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
9 changed files with 352 additions and 33 deletions

View file

@ -0,0 +1,10 @@
{
"port": 38195,
"database": {
"address": "localhost",
"port": 3306,
"username": "user",
"password": "password",
"name": "MultiProbe"
}
}

View file

@ -2,16 +2,19 @@ import { createReader, createWriter, Endian } from "bufferstuff";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import Config from "./objects/Config"; import Config from "./objects/Config";
import FunkyArray from "./objects/FunkyArray"; import FunkyArray from "./objects/FunkyArray";
import User from "./objects/User"; import RemoteUser from "./objects/RemoteUser";
import { MessageType } from "./enums/MessageType"; import { MessageType } from "./enums/MessageType";
import Database from "./objects/Database";
const users = new FunkyArray<string, User>(); const users = new FunkyArray<string, RemoteUser>();
new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name);
const server = new WebSocketServer({ const server = new WebSocketServer({
port: Config.port port: Config.port
}, () => console.log(`Server listening at ${Config.port}`)); }, () => console.log(`Server listening at ${Config.port}`));
function sendToAllButSelf(user:User, data:Buffer) { function sendToAllButSelf(user:RemoteUser, data:Buffer) {
users.forEach(otherUser => { users.forEach(otherUser => {
if (otherUser.id !== user.id && otherUser.currentURL === user.currentURL) { if (otherUser.id !== user.id && otherUser.currentURL === user.currentURL) {
otherUser.send(data); 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 => { users.forEach(otherUser => {
if (otherUser.currentURL === user.currentURL) { if (otherUser.currentURL === user.currentURL) {
otherUser.send(data); otherUser.send(data);
@ -29,7 +32,7 @@ function sendToAll(user:User, data:Buffer) {
server.on("connection", (socket) => { server.on("connection", (socket) => {
const myUUID = crypto.randomUUID(); const myUUID = crypto.randomUUID();
let user:User; let user:RemoteUser;
function closeOrError() { function closeOrError() {
if (users.has(myUUID)) { if (users.has(myUUID)) {
@ -60,7 +63,7 @@ server.on("connection", (socket) => {
page = ""; page = "";
} }
let lengthOfUsernames = 0; let lengthOfUsernames = 0;
const usersOnPage = new Array<User>(); const usersOnPage = new Array<RemoteUser>();
await users.forEach(otherUser => { await users.forEach(otherUser => {
if (otherUser.currentURL === page) { if (otherUser.currentURL === page) {
usersOnPage.push(otherUser); usersOnPage.push(otherUser);
@ -71,7 +74,7 @@ server.on("connection", (socket) => {
for (const otherUser of usersOnPage) { for (const otherUser of usersOnPage) {
usersToSend.writeUInt(otherUser.id).writeShortString(otherUser.username).writeFloat(otherUser.cursorX).writeInt(otherUser.cursorY); 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()); sendToAllButSelf(user, createWriter(Endian.LE, 6 + username.length).writeByte(MessageType.ClientJoined).writeUInt(user.id).writeShortString(username).toBuffer());
user.send(usersToSend.toBuffer()); user.send(usersToSend.toBuffer());
break; break;

View file

@ -6,4 +6,14 @@ export default class Config {
public constructor() { throw new Error("Static Class"); } 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
} }

101
server/objects/Database.ts Normal file
View file

@ -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<DBInDataType>) {
return new Promise<boolean>((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<DBInDataType>) {
return new Promise<RowDataPacket[]>((resolve, reject) => {
this.connectionPool.getConnection((err, connection) => {
if (err) {
return reject(err);
} else {
// Use old query
if (data == null) {
connection.query<RowDataPacket[]>(query, (err, rows) => {
connection.release();
if (err) {
return reject(err);
}
resolve(rows);
connection.release();
});
}
// Use new prepared statements w/ placeholders
else {
connection.execute<RowDataPacket[]>(query, data, (err, rows) => {
connection.release();
if (err) {
return reject(err);
}
resolve(rows);
connection.release();
});
}
}
});
});
}
public async querySingle(query:string, data?:Array<DBInDataType>) {
const dbData = await this.query(query, data);
if (dbData != null && dbData.length > 0) {
return dbData[0];
}
return null;
}
}

View file

@ -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);
}
}

View file

@ -1,29 +1,41 @@
import { WebSocket } from "ws";
export default class User { 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 constructor(id?:number, username?:string, passwordSalt?:string, passwordHash?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) {
public readonly id:number; 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") {
public readonly username:string; this.Id = id;
public readonly currentURL:string; this.Username = username;
public readonly rawURL:string = ""; this.PasswordHash = passwordHash;
public cursorX:number = 0; this.PasswordSalt = passwordSalt;
public cursorY:number = 0; this.CreatedByUserId = createdByUserId;
public allowedPings:number; this.CreatedDatetime = createdDateTime;
public lastPingReset:number; this.LastModifiedByUserId = lastModifiedByUserId;
this.LastModifiedDatetime = lastModifiedDatetime;
constructor(socket:WebSocket, username:string, currentURL:string, rawURL:string) { this.DeletedByUserId = deletedByUserId;
this.socket = socket; this.DeletedDatetime = deletedDatetime;
this.id = User.USER_IDS++; this.IsDeleted = isDeleted;
this.username = username; } else {
this.currentURL = currentURL; this.Id = Number.MIN_VALUE;
this.rawURL = rawURL; this.Username = "";
this.allowedPings = 10; this.PasswordHash = "";
this.lastPingReset = Date.now(); this.PasswordSalt = "";
} this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
send(data:Buffer) { this.LastModifiedByUserId = Number.MIN_VALUE;
this.socket.send(data); this.LastModifiedDatetime = new Date(0);
this.DeletedByUserId = Number.MIN_VALUE;
this.DeletedDatetime = new Date(0);
this.IsDeleted = false;
}
} }
} }

101
server/package-lock.json generated
View file

@ -10,6 +10,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bufferstuff": "^1.5.1", "bufferstuff": "^1.5.1",
"mysql2": "^3.9.7",
"ws": "^8.16.0" "ws": "^8.16.0"
}, },
"devDependencies": { "devDependencies": {
@ -539,6 +540,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/diff": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -761,6 +770,14 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-intrinsic": {
"version": "1.2.4", "version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@ -929,6 +946,17 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true "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": { "node_modules/ignore-by-default": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "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" "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": { "node_modules/is-regex": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@ -1243,6 +1276,11 @@
"node": ">=4" "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": { "node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@ -1282,6 +1320,51 @@
"node": "*" "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": { "node_modules/nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -1629,6 +1712,11 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/semver": {
"version": "7.6.0", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
@ -1644,6 +1732,11 @@
"node": ">=10" "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": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "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==", "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==",
"dev": true "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": { "node_modules/string.prototype.padend": {
"version": "3.1.6", "version": "3.1.6",
"resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz",

View file

@ -16,6 +16,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bufferstuff": "^1.5.1", "bufferstuff": "^1.5.1",
"mysql2": "^3.9.7",
"ws": "^8.16.0" "ws": "^8.16.0"
}, },
"devDependencies": { "devDependencies": {

52
server/repos/UserRepo.ts Normal file
View file

@ -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;
}