add more db infra

This commit is contained in:
Holly Stubbs 2024-04-22 02:01:14 +01:00
parent 8f00e688e0
commit 3159fc9c0a
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
14 changed files with 221 additions and 20 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
server/node_modules server/node_modules
server/config.json server/config.json
server/build server/build
server/logs

View file

@ -5,6 +5,10 @@
"port": 3306, "port": 3306,
"username": "user", "username": "user",
"password": "password", "password": "password",
"name": "MultiProbe" "name": "MultiProbe",
"pbkdf2": {
"itterations": 10000,
"keylength": 64
}
} }
} }

View file

@ -5,6 +5,9 @@ import FunkyArray from "./objects/FunkyArray";
import RemoteUser from "./objects/RemoteUser"; import RemoteUser from "./objects/RemoteUser";
import { MessageType } from "./enums/MessageType"; import { MessageType } from "./enums/MessageType";
import Database from "./objects/Database"; import Database from "./objects/Database";
import { Console } from "hsconsole";
Console.customHeader(`MultiProbe server started at ${new Date()}`);
const users = new FunkyArray<string, RemoteUser>(); const users = new FunkyArray<string, RemoteUser>();
@ -12,7 +15,7 @@ new Database(Config.database.address, Config.database.port, Config.database.user
const server = new WebSocketServer({ const server = new WebSocketServer({
port: Config.port port: Config.port
}, () => console.log(`Server listening at ${Config.port}`)); }, () => Console.printInfo(`Server listening at ${Config.port}`));
function sendToAllButSelf(user:RemoteUser, data:Buffer) { function sendToAllButSelf(user:RemoteUser, data:Buffer) {
users.forEach(otherUser => { users.forEach(otherUser => {

View file

@ -15,5 +15,11 @@ interface ConfigDatabase {
port: number, port: number,
username: string, username: string,
password: string, password: string,
name: string name: string,
pbkdf2: DatabasePbkdf2
}
interface DatabasePbkdf2 {
itterations: number,
keylength: number
} }

View file

@ -1,3 +1,4 @@
import { Console } from "hsconsole";
import { createPool, Pool, RowDataPacket } from "mysql2"; import { createPool, Pool, RowDataPacket } from "mysql2";
export type DBInDataType = string | number | null | undefined; export type DBInDataType = string | number | null | undefined;
@ -20,7 +21,7 @@ export default class Database {
database: databaseName database: databaseName
}); });
console.log(`DB connection pool created. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`); Console.printInfo(`DB connection pool created. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`);
Database.Instance = this; Database.Instance = this;
} }

34
server/objects/Party.ts Normal file
View file

@ -0,0 +1,34 @@
export default class Party {
public Id:number;
public PartyRef:string;
public Name:string;
public CreatedByUserId:number;
public CreatedDatetime:Date;
public LastModifiedByUserId?:number;
public LastModifiedDatetime?:Date;
public DeletedByUserId?:number;
public DeletedDatetime?:Date;
public IsDeleted:boolean;
public constructor(id?:number, partyRef?:string, name?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) {
if (typeof(id) == "number" && typeof(partyRef) == "string" && typeof(name) == "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.PartyRef = partyRef;
this.Name = name;
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.PartyRef = "";
this.Name = "";
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
this.IsDeleted = false;
}
}
}

View file

@ -5,10 +5,10 @@ export default class User {
public PasswordHash:string; public PasswordHash:string;
public CreatedByUserId:number; public CreatedByUserId:number;
public CreatedDatetime:Date; public CreatedDatetime:Date;
public LastModifiedByUserId:number; public LastModifiedByUserId?:number;
public LastModifiedDatetime:Date; public LastModifiedDatetime?:Date;
public DeletedByUserId:number; public DeletedByUserId?:number;
public DeletedDatetime:Date; public DeletedDatetime?:Date;
public IsDeleted:boolean; public IsDeleted:boolean;
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 constructor(id?:number, username?:string, passwordSalt?:string, passwordHash?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) {
@ -31,10 +31,6 @@ export default class User {
this.PasswordSalt = ""; this.PasswordSalt = "";
this.CreatedByUserId = Number.MIN_VALUE; this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0); 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; this.IsDeleted = false;
} }
} }

View file

@ -10,6 +10,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bufferstuff": "^1.5.1", "bufferstuff": "^1.5.1",
"hsconsole": "^1.0.2",
"mysql2": "^3.9.7", "mysql2": "^3.9.7",
"ws": "^8.16.0" "ws": "^8.16.0"
}, },
@ -557,6 +558,11 @@
"node": ">=0.3.1" "node": ">=0.3.1"
} }
}, },
"node_modules/dyetty": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dyetty/-/dyetty-1.0.1.tgz",
"integrity": "sha512-MQEccirDXkAQf5U1gIwcIz46+vMMEEyAl33nCqOJ7TeCRKgcHTZdG013gmWRWw3Q9wivnJqcJ04ohZnyF8nRew=="
},
"node_modules/error-ex": { "node_modules/error-ex": {
"version": "1.3.2", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -946,6 +952,14 @@
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
"dev": true "dev": true
}, },
"node_modules/hsconsole": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hsconsole/-/hsconsole-1.0.2.tgz",
"integrity": "sha512-st+jaSpNw3uoIhE5vl2lVN8Op8yQF2FyLRdBG68s8vqjduJdKUGtoEXd8Zxe6du1zzpFHHRcU3zJbAq8BOmYQA==",
"dependencies": {
"dyetty": "^1.0.1"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",

View file

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

53
server/repos/PartyRepo.ts Normal file
View file

@ -0,0 +1,53 @@
import Database from "../objects/Database";
import Party from "../objects/Party";
import User from "../objects/User";
import RepoBase from "./RepoBase";
export default class PartyRepo {
public static async selectById(id:number) {
const dbParty = await Database.Instance.query("SELECT * FROM Party WHERE Id = ? LIMIT 1", [id]);
if (dbParty == null || dbParty.length === 0) {
return null;
} else {
const party = new Party();
populatePartyFromDB(party, dbParty[0]);
return party;
}
}
public static async selectByPartyRef(partyRef:string) {
const dbParty = await Database.Instance.query("SELECT * FROM Party WHERE PartyRef = ? LIMIT 1", [partyRef]);
if (dbParty == null || dbParty.length === 0) {
return null;
} else {
const party = new Party();
populatePartyFromDB(party, dbParty[0]);
return party;
}
}
public static async insertUpdate(user:User) {
if (user.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT Party (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 Party 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 populatePartyFromDB(party:Party, dbParty:any) {
party.Id = dbParty.Id;
party.PartyRef = dbParty.PartyRef;
party.Name = dbParty.Name;
party.CreatedByUserId = dbParty.CreatedByUserId;
party.CreatedDatetime = dbParty.CreatedDatetime;
party.LastModifiedByUserId = dbParty.LastModifiedByUserId;
party.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.LastModifiedDatetime);
party.DeletedByUserId = dbParty.DeletedByUserId;
party.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.DeletedDatetime);
party.IsDeleted = dbParty.IsDeleted;
}

5
server/repos/RepoBase.ts Normal file
View file

@ -0,0 +1,5 @@
export default class RepoBase {
public static convertNullableDatetimeIntToDate(dateTimeInt?:number) {
return dateTimeInt ? new Date(dateTimeInt) : undefined;
}
}

View file

@ -1,7 +1,8 @@
import Database from "../objects/Database"; import Database from "../objects/Database";
import User from "../objects/User"; import User from "../objects/User";
import RepoBase from "./RepoBase";
export default class UsersRepo { export default class UserRepo {
public static async selectById(id:number) { public static async selectById(id:number) {
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE Id = ? LIMIT 1", [id]); const dbUser = await Database.Instance.query("SELECT * FROM User WHERE Id = ? LIMIT 1", [id]);
if (dbUser == null || dbUser.length === 0) { if (dbUser == null || dbUser.length === 0) {
@ -26,12 +27,12 @@ export default class UsersRepo {
public static async insertUpdate(user:User) { public static async insertUpdate(user:User) {
if (user.Id === Number.MIN_VALUE) { if (user.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT users (Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ await Database.Instance.query("INSERT User (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) 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 { } else {
await Database.Instance.query(`UPDATE users SET Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [ await Database.Instance.query(`UPDATE User 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 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
]); ]);
} }
} }
@ -45,8 +46,8 @@ function populateUserFromDB(user:User, dbUser:any) {
user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = dbUser.CreatedDatetime; user.CreatedDatetime = dbUser.CreatedDatetime;
user.LastModifiedByUserId = dbUser.LastModifiedByUserId; user.LastModifiedByUserId = dbUser.LastModifiedByUserId;
user.LastModifiedDatetime = dbUser.LastModifiedDatetime; user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
user.DeletedByUserId = dbUser.DeletedByUserId; user.DeletedByUserId = dbUser.DeletedByUserId;
user.DeletedDatetime = dbUser.DeletedDatetime; user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
user.IsDeleted = dbUser.IsDeleted; user.IsDeleted = dbUser.IsDeleted;
} }

View file

@ -0,0 +1,46 @@
import { Console } from "hsconsole";
import User from "../objects/User";
import PartyRepo from "../repos/PartyRepo";
import UserRepo from "../repos/UserRepo";
import PasswordUtility from "../utilities/PasswordUtility";
export default class UserService {
public static async GetUser(id:number) {
try {
return await UserRepo.selectById(id);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
}
}
public static async GetParty(id:number) {
try {
return await PartyRepo.selectById(id);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
}
}
public static async GetPartyByPartyRef(partyRef:string) {
try {
return await PartyRepo.selectByPartyRef(partyRef);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
}
}
public static async CreateUser(currentUserId:number, username:string, password:string) {
try {
const user = new User();
user.Username = username;
user.PasswordSalt = PasswordUtility.GenerateSalt();
user.PasswordHash = await PasswordUtility.HashPassword(user.PasswordSalt, password);
user.CreatedByUserId = currentUserId;
user.CreatedDatetime = new Date();
await UserRepo.insertUpdate(user);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
}
}
}

View file

@ -0,0 +1,36 @@
import { pbkdf2, randomBytes } from "crypto";
import Config from "../objects/Config";
export default abstract class PasswordUtility {
public static ValidatePassword(hash:string, salt:string, password:string) {
return new Promise<boolean>((resolve, reject) => {
pbkdf2(password, salt, Config.database.pbkdf2.itterations, Config.database.pbkdf2.keylength, "sha512", (err, derivedKey) => {
if (err) {
return reject(err);
} else {
if (derivedKey.toString("hex") !== hash) {
return resolve(false);
}
return resolve(true);
}
});
});
}
public static HashPassword(salt:string, password:string) {
return new Promise<string>((resolve, reject) => {
pbkdf2(password, salt, Config.database.pbkdf2.itterations, Config.database.pbkdf2.keylength, "sha512", (err, derivedKey) => {
if (err) {
return reject(err);
} else {
return resolve(derivedKey.toString("hex"));
}
});
});
}
public static GenerateSalt() {
return randomBytes(Config.database.pbkdf2.keylength).toString("hex");
}
}