From 3159fc9c0a059c9cb78bd61dd127abf4b322de8d Mon Sep 17 00:00:00 2001 From: Holly Date: Mon, 22 Apr 2024 02:01:14 +0100 Subject: [PATCH] add more db infra --- .gitignore | 3 +- server/config.example.json | 6 +++- server/index.ts | 5 ++- server/objects/Config.ts | 8 ++++- server/objects/Database.ts | 3 +- server/objects/Party.ts | 34 ++++++++++++++++++ server/objects/User.ts | 12 +++---- server/package-lock.json | 14 ++++++++ server/package.json | 1 + server/repos/PartyRepo.ts | 53 +++++++++++++++++++++++++++++ server/repos/RepoBase.ts | 5 +++ server/repos/UserRepo.ts | 15 ++++---- server/services/UserService.ts | 46 +++++++++++++++++++++++++ server/utilities/PasswordUtility.ts | 36 ++++++++++++++++++++ 14 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 server/objects/Party.ts create mode 100644 server/repos/PartyRepo.ts create mode 100644 server/repos/RepoBase.ts create mode 100644 server/services/UserService.ts create mode 100644 server/utilities/PasswordUtility.ts diff --git a/.gitignore b/.gitignore index 50a2a09..1aa605f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ server/node_modules server/config.json -server/build \ No newline at end of file +server/build +server/logs \ No newline at end of file diff --git a/server/config.example.json b/server/config.example.json index 734423c..2056d9f 100644 --- a/server/config.example.json +++ b/server/config.example.json @@ -5,6 +5,10 @@ "port": 3306, "username": "user", "password": "password", - "name": "MultiProbe" + "name": "MultiProbe", + "pbkdf2": { + "itterations": 10000, + "keylength": 64 + } } } \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index 7c2da45..a990a76 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,6 +5,9 @@ import FunkyArray from "./objects/FunkyArray"; import RemoteUser from "./objects/RemoteUser"; import { MessageType } from "./enums/MessageType"; import Database from "./objects/Database"; +import { Console } from "hsconsole"; + +Console.customHeader(`MultiProbe server started at ${new Date()}`); const users = new FunkyArray(); @@ -12,7 +15,7 @@ new Database(Config.database.address, Config.database.port, Config.database.user const server = new WebSocketServer({ port: Config.port -}, () => console.log(`Server listening at ${Config.port}`)); +}, () => Console.printInfo(`Server listening at ${Config.port}`)); function sendToAllButSelf(user:RemoteUser, data:Buffer) { users.forEach(otherUser => { diff --git a/server/objects/Config.ts b/server/objects/Config.ts index f528254..eb855d9 100644 --- a/server/objects/Config.ts +++ b/server/objects/Config.ts @@ -15,5 +15,11 @@ interface ConfigDatabase { port: number, username: string, password: string, - name: string + name: string, + pbkdf2: DatabasePbkdf2 +} + +interface DatabasePbkdf2 { + itterations: number, + keylength: number } \ No newline at end of file diff --git a/server/objects/Database.ts b/server/objects/Database.ts index dbd9256..6f93de3 100644 --- a/server/objects/Database.ts +++ b/server/objects/Database.ts @@ -1,3 +1,4 @@ +import { Console } from "hsconsole"; import { createPool, Pool, RowDataPacket } from "mysql2"; export type DBInDataType = string | number | null | undefined; @@ -20,7 +21,7 @@ export default class Database { 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; } diff --git a/server/objects/Party.ts b/server/objects/Party.ts new file mode 100644 index 0000000..6c9fdac --- /dev/null +++ b/server/objects/Party.ts @@ -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; + } + } +} \ No newline at end of file diff --git a/server/objects/User.ts b/server/objects/User.ts index 9ca909f..c788d11 100644 --- a/server/objects/User.ts +++ b/server/objects/User.ts @@ -5,10 +5,10 @@ export default class User { public PasswordHash:string; public CreatedByUserId:number; public CreatedDatetime:Date; - public LastModifiedByUserId:number; - public LastModifiedDatetime:Date; - public DeletedByUserId:number; - public DeletedDatetime:Date; + public LastModifiedByUserId?:number; + public LastModifiedDatetime?:Date; + public DeletedByUserId?:number; + public DeletedDatetime?:Date; 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) { @@ -31,10 +31,6 @@ export default class User { 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; } } diff --git a/server/package-lock.json b/server/package-lock.json index b873599..7d220a0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "bufferstuff": "^1.5.1", + "hsconsole": "^1.0.2", "mysql2": "^3.9.7", "ws": "^8.16.0" }, @@ -557,6 +558,11 @@ "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": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -946,6 +952,14 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "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": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", diff --git a/server/package.json b/server/package.json index 7d32b2c..3f3c5ac 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "license": "MIT", "dependencies": { "bufferstuff": "^1.5.1", + "hsconsole": "^1.0.2", "mysql2": "^3.9.7", "ws": "^8.16.0" }, diff --git a/server/repos/PartyRepo.ts b/server/repos/PartyRepo.ts new file mode 100644 index 0000000..bf7a8a8 --- /dev/null +++ b/server/repos/PartyRepo.ts @@ -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; +} \ No newline at end of file diff --git a/server/repos/RepoBase.ts b/server/repos/RepoBase.ts new file mode 100644 index 0000000..ba85361 --- /dev/null +++ b/server/repos/RepoBase.ts @@ -0,0 +1,5 @@ +export default class RepoBase { + public static convertNullableDatetimeIntToDate(dateTimeInt?:number) { + return dateTimeInt ? new Date(dateTimeInt) : undefined; + } +} \ No newline at end of file diff --git a/server/repos/UserRepo.ts b/server/repos/UserRepo.ts index aeb9f70..04df783 100644 --- a/server/repos/UserRepo.ts +++ b/server/repos/UserRepo.ts @@ -1,7 +1,8 @@ import Database from "../objects/Database"; import User from "../objects/User"; +import RepoBase from "./RepoBase"; -export default class UsersRepo { +export default class UserRepo { 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) { @@ -26,12 +27,12 @@ export default class UsersRepo { 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) + 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) ]); } 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 + 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 ]); } } @@ -45,8 +46,8 @@ function populateUserFromDB(user:User, dbUser:any) { user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedDatetime = dbUser.CreatedDatetime; user.LastModifiedByUserId = dbUser.LastModifiedByUserId; - user.LastModifiedDatetime = dbUser.LastModifiedDatetime; + user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime); user.DeletedByUserId = dbUser.DeletedByUserId; - user.DeletedDatetime = dbUser.DeletedDatetime; + user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime); user.IsDeleted = dbUser.IsDeleted; } \ No newline at end of file diff --git a/server/services/UserService.ts b/server/services/UserService.ts new file mode 100644 index 0000000..8200491 --- /dev/null +++ b/server/services/UserService.ts @@ -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}`); + } + } +} \ No newline at end of file diff --git a/server/utilities/PasswordUtility.ts b/server/utilities/PasswordUtility.ts new file mode 100644 index 0000000..e04c660 --- /dev/null +++ b/server/utilities/PasswordUtility.ts @@ -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((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((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"); + } +} \ No newline at end of file