diff --git a/.gitignore b/.gitignore index abc1ca7..ef38d35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ build/ logs/ +images/ config.json \ No newline at end of file diff --git a/controllers/AccountController.ts b/controllers/AccountController.ts index e688898..2f3324f 100644 --- a/controllers/AccountController.ts +++ b/controllers/AccountController.ts @@ -1,5 +1,6 @@ import LoginViewModel from "../models/account/LoginViewModel"; import RegisterViewModel from "../models/account/RegisterViewModel"; +import Config from "../objects/Config"; import Session from "../objects/Session"; import UserService from "../services/UserService"; import Controller from "./Controller"; @@ -32,13 +33,22 @@ export default class AccountController extends Controller { } public async Register_Post_AllowAnonymous(registerViewModel: RegisterViewModel) { - if (typeof(registerViewModel.username) !== "string" || typeof(registerViewModel.password) !== "string") { + if (typeof(registerViewModel.username) !== "string" || typeof(registerViewModel.password) !== "string" || typeof(registerViewModel.registerKey) !== "string" || typeof(registerViewModel.password2) !== "string" || typeof(registerViewModel.email) !== "string") { return this.badRequest(); } - const username = registerViewModel.username.replaceAll("<", "<").replaceAll(">", ">"); - if (!await UserService.CreateUser(0, username, registerViewModel.password)) { + if (registerViewModel.registerKey !== Config.accounts.signup.key) { registerViewModel.password = ""; + registerViewModel.password2 = ""; + registerViewModel.message = "Incorrect Registration Key."; + + return this.view(registerViewModel); + } + + const username = registerViewModel.username.replaceAll("<", "<").replaceAll(">", ">"); + if (!await UserService.CreateUser(0, username, registerViewModel.email.trim(), registerViewModel.password)) { + registerViewModel.password = ""; + registerViewModel.password2 = ""; registerViewModel.message = "Sorry! That username is already taken."; return this.view(registerViewModel); @@ -47,6 +57,7 @@ export default class AccountController extends Controller { const user = await UserService.GetUserByUsername(username); if (!user) { registerViewModel.password = ""; + registerViewModel.password2 = ""; registerViewModel.message = "Failed to create your account, please try again later."; return this.view(registerViewModel); diff --git a/controllers/ApiController.ts b/controllers/ApiController.ts new file mode 100644 index 0000000..f2e6b01 --- /dev/null +++ b/controllers/ApiController.ts @@ -0,0 +1,5 @@ +import Controller from "./Controller"; + +export default class ApiController extends Controller { + +} \ No newline at end of file diff --git a/controllers/Controller.ts b/controllers/Controller.ts index 3fdfb23..0f20a7c 100644 --- a/controllers/Controller.ts +++ b/controllers/Controller.ts @@ -4,6 +4,7 @@ import Session from "../objects/Session"; import SessionUser from "../objects/SessionUser"; import RequestCtx from "../objects/RequestCtx"; import UserType from "../enums/UserType"; +import { cyan } from "dyetty"; // prepare for ts-ignore :3 // TODO: figure out some runtime field / type checking so @@ -12,6 +13,10 @@ export default abstract class Controller { public static FastifyInstance:FastifyInstance; public static RegisteredPaths:Array = []; + private logInfo(logText: string) { + Console.printInfo(`[ ${cyan("CONTROLLER")} ] ${logText}`); + } + public constructor() { const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); const rawControllerParts = this.constructor.name.split("_"); @@ -24,7 +29,7 @@ export default abstract class Controller { const userType = prop.split("$")[1]; // @ts-ignore controllerAuthLevels.push(UserType[userType]); - Console.printInfo(`Set Auth level requirement for ${this.constructor.name} to ${userType}`); + this.logInfo(`Set Auth level requirement for ${this.constructor.name} to ${userType}`); } } @@ -91,21 +96,21 @@ export default abstract class Controller { thisMethodHttpMethod = param.toLowerCase(); // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName === "index" ? "" : methodName}`, requestHandler); - Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName === "index" ? "" : methodName}" as ${param}`); + this.logInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName === "index" ? "" : methodName}" as ${param}`); Controller.RegisteredPaths.push(`/${controllerName}/${methodName === "index" ? "" : methodName}`); if (methodName === "index") { // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName}`, requestHandler); - Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName}" as ${param}`); + this.logInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName}" as ${param}`); Controller.RegisteredPaths.push(`/${controllerName}/${methodName}`); // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}`, requestHandler); - Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}" as ${param}`); + this.logInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}" as ${param}`); Controller.RegisteredPaths.push(`/${controllerName}`); } else if (controllerName === "home") { // @ts-ignore Controller.FastifyInstance[param.toLowerCase()](`/${methodName}`, requestHandler); - Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${methodName}" as ${param}`); + this.logInfo(`Registered ${this.constructor.name}.${method} to "/${methodName}" as ${param}`); Controller.RegisteredPaths.push(`/${methodName}`); } } else if (param.startsWith("Auth")) { @@ -116,7 +121,7 @@ export default abstract class Controller { } // @ts-ignore actionAuthLevels[nameWithMethod].push(UserType[userType]); - Console.printInfo(`Set Auth level requirement for ${this.constructor.name}.${method} to ${userType}`); + this.logInfo(`Set Auth level requirement for ${this.constructor.name}.${method} to ${userType}`); } } @@ -124,7 +129,7 @@ export default abstract class Controller { for (const httpMethod of funcMethods) { // @ts-ignore Controller.FastifyInstance[httpMethod.toLowerCase()](`/`, requestHandler); - Console.printInfo(`Registered ${this.constructor.name}.${method} to "/" as ${httpMethod}`); + this.logInfo(`Registered ${this.constructor.name}.${method} to "/" as ${httpMethod}`); Controller.RegisteredPaths.push(`/`); } } diff --git a/controllers/HomeController.ts b/controllers/HomeController.ts index 0d62405..0f2962a 100644 --- a/controllers/HomeController.ts +++ b/controllers/HomeController.ts @@ -1,14 +1,29 @@ +import Config from "../objects/Config"; +import HashFS from "../objects/HashFS"; import Controller from "./Controller"; +import { randomBytes } from "crypto"; export default class HomeController extends Controller { public Index_Get_AllowAnonymous() { return this.view(); } - public Upload_Post_AllowAnonymous() { - console.log(this.req.headers.authorization); - console.log(this.req.body); + public async Upload_Post_AllowAnonymous() { + const data = await this.req.file(); + if (data && data.type === "file") { + let uploadKey: string = ""; + if ("upload-key" in this.req.headers) { + // @ts-ignore + uploadKey = this.req.headers["upload-key"]; + if (uploadKey !== Config.accounts.signup.key) { + return this.unauthorised("Upload key invalid or missing."); + } + } + //console.log(uploadKey); + //console.log(data.mimetype); + const hash = await HashFS.GetHashFSInstance("images").AddFromStream(data.file); + } - return this.ok(); + return this.badRequest(); } } \ No newline at end of file diff --git a/entities/Image.ts b/entities/Image.ts new file mode 100644 index 0000000..ee4c68e --- /dev/null +++ b/entities/Image.ts @@ -0,0 +1,17 @@ +export default class Image { + public Id: number = Number.MIN_VALUE; + public UserId: number = Number.MIN_VALUE; + public DomainId: number = Number.MIN_VALUE; + public FileName: string = ""; + public ImageTag: string = ""; + public ImageType: string = ""; + public Hash: string = ""; + public FileSize: number = Number.MIN_VALUE; + public CreatedByUserId: number = Number.MIN_VALUE; + public CreatedDatetime: Date = new Date(); + public LastModifiedByUserId?: number; + public LastModifiedDatetime?: Date; + public DeletedByUserId?: number; + public DeletedDatetime?: Date; + public IsDeleted: boolean = false; +} \ No newline at end of file diff --git a/entities/User.ts b/entities/User.ts index 3cca44a..bfd413c 100644 --- a/entities/User.ts +++ b/entities/User.ts @@ -9,6 +9,7 @@ export default class User { public PasswordSalt: string = ""; public ApiKey: string = ""; public UploadKey: string = ""; + public Verified: boolean = false; public CreatedByUserId: number = Number.MIN_VALUE; public CreatedDatetime: Date = new Date(); public LastModifiedByUserId?: number; diff --git a/index.ts b/index.ts index 5eeec96..1e22086 100644 --- a/index.ts +++ b/index.ts @@ -12,8 +12,10 @@ import HomeController from "./controllers/HomeController"; import Database from "./objects/Database"; import { join } from "path"; import AccountController from "./controllers/AccountController"; -import { magenta, blue, cyan } from "dyetty"; +import { magenta, blue, cyan, green, red } from "dyetty"; import ConsoleUtility from "./utilities/ConsoleUtility"; +import HashFS from "./objects/HashFS"; +import { existsSync, mkdirSync, rmSync } from "fs"; Console.customHeader(`EUS server started at ${new Date()}`); @@ -56,7 +58,7 @@ fastify.addHook("preValidation", (req, res, done) => { return done(); } else { // @ts-ignore - req.logType = magenta(" STATIC "); + req.logType = magenta("STATIC"); } done(); @@ -70,20 +72,32 @@ fastify.addHook("onSend", (req, res, _payload, done) => { }); fastify.setNotFoundHandler(async (req, res) => { + return res.status(404).view("views/404.ejs", { session: null }); }); -new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name); +HashFS.STARTUP_DIR = __dirname; +new HashFS("images"); -Controller.FastifyInstance = fastify; -new AccountController(); -new HomeController(); +if (Config.database.enabled) { + new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name); +} else { + Console.printInfo(`[ ${red("DATABASE")} ] Database is disabled.`); +} -fastify.listen({ port: Config.ports.web, host: "127.0.0.1" }, (err, address) => { +if (Config.controllers.enabled && Config.database.enabled) { + Controller.FastifyInstance = fastify; + new AccountController(); + new HomeController(); +} else { + Console.printInfo(`[ ${red("CONTROLLER")} ] Controllers are disabled${Config.controllers.enabled && !Config.database.enabled ? " because the database is disabled but required by the controllers." : "."} Server will operate in static mode only.`); +} + +fastify.listen({ port: Config.hosts.webPort, host: Config.hosts.webHost }, (err, address) => { if (err) { Console.printError(`Error occured while spinning up fastify:\n${err}`); process.exit(1); } - Console.printInfo(`Fastify listening at ${address.replace("0.0.0.0", "localhost").replace("127.0.0.1", "localhost")}`); + Console.printInfo(`[ ${green("MAIN")} ] Listening at ${address.replace("0.0.0.0", "localhost").replace("127.0.0.1", "localhost")}`); }); \ No newline at end of file diff --git a/models/account/RegisterViewModel.ts b/models/account/RegisterViewModel.ts index e6c7a67..05eddc2 100644 --- a/models/account/RegisterViewModel.ts +++ b/models/account/RegisterViewModel.ts @@ -1,5 +1,8 @@ export default interface RegisterViewModel { message?: string, + registerKey: string username: string, - password: string + email: string, + password: string, + password2: string } \ No newline at end of file diff --git a/objects/Config.ts b/objects/Config.ts index 26aace0..8de722f 100644 --- a/objects/Config.ts +++ b/objects/Config.ts @@ -2,18 +2,21 @@ import { readFileSync } from "fs"; const config = JSON.parse(readFileSync("./config.json").toString()); export default abstract class Config { - public static ports:IPorts = config.ports; - public static database:IDatabase = config.database; - public static session:ISession = config.session; - public static controllers:IControllers = config.controllers; - public static accounts:IAccounts = config.accounts; + public static instance: string = config.instance; + public static hosts: IHosts = config.hosts; + public static database: IDatabase = config.database; + public static session: ISession = config.session; + public static controllers: IControllers = config.controllers; + public static accounts: IAccounts = config.accounts; } -interface IPorts { - web: number +interface IHosts { + webHost: string, + webPort: number } interface IDatabase { + enabled: boolean, address: string, port: number, username: string, diff --git a/objects/Database.ts b/objects/Database.ts index 6f93de3..ce00218 100644 --- a/objects/Database.ts +++ b/objects/Database.ts @@ -1,3 +1,4 @@ +import { blue } from "dyetty"; import { Console } from "hsconsole"; import { createPool, Pool, RowDataPacket } from "mysql2"; @@ -21,7 +22,7 @@ export default class Database { database: databaseName }); - Console.printInfo(`DB connection pool created. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`); + Console.printInfo(`[ ${blue("DATABASE")} ] DB connection pool created. MAX_CONNECTIONS = ${Database.CONNECTION_LIMIT}`); Database.Instance = this; } diff --git a/objects/HashFS.ts b/objects/HashFS.ts new file mode 100644 index 0000000..4280024 --- /dev/null +++ b/objects/HashFS.ts @@ -0,0 +1,140 @@ +// ! Hashed File Store (not file system!!) + +import { join } from "path"; +import { existsSync, mkdirSync, createWriteStream, rename, stat, writeFile, copyFile, rm, rmSync, } from "fs"; +import { Console } from "hsconsole"; +import { yellow } from "dyetty"; +import { createHash, randomBytes } from "crypto"; +import FunkyArray from "funky-array"; +import { Stream } from "stream"; + +export default class HashFS { + public static STARTUP_DIR: string; + private static HASHFS_INSTANCES: FunkyArray = new FunkyArray(); + + public static GetHashFSInstance(name: string) { + const instance = this.HASHFS_INSTANCES.get(name); + if (!instance) { + throw `Attempted to get nonexistent HashFS instance "${name}"`; + } + + return instance; + } + + private readonly path: string; + private readonly tempPath: string; + private readonly folder: string; + + private logInfo(logText: string) { + Console.printInfo(`[ ${yellow(`HashFS: ${this.folder}`)} ] ${logText}`); + } + + public constructor(folder: string) { + HashFS.HASHFS_INSTANCES.set(folder, this); + + this.folder = folder; + this.path = join(HashFS.STARTUP_DIR, folder); + + let firstCreation = false; + if (!existsSync(this.path)) { + this.logInfo(`Creating HashFS for "${folder}"...`); + mkdirSync(this.path); + firstCreation = true; + } + + this.logInfo(`Validating file store...`); + let issuesRepaired = 0; + for (let i = 0; i < 16; i++) { + const hashRootFolderPath = join(this.path, i.toString(16)); + if (!existsSync(hashRootFolderPath)) { + mkdirSync(hashRootFolderPath); + this.logInfo(`"${i.toString(16)}" does not exist, creating...`); + issuesRepaired++; + } + for (let i1 = 0; i1 < 16; i1++) { + const subFolderPath = join(hashRootFolderPath, (i * 16 + i1).toString(16).padStart(2, "0")); + if (!existsSync(subFolderPath)) { + this.logInfo(`"${i.toString(16)}/${(i * 16 + i1).toString(16).padStart(2, "0")}" does not exist, creating...`); + mkdirSync(subFolderPath); + issuesRepaired++; + } + } + } + // TODO: Validate the files in the file store + this.logInfo(`File Store Validated Successfully${!firstCreation && issuesRepaired > 0 ? `. Repaired ${issuesRepaired} issues.` : " with no issues."}`); + + this.tempPath = join(this.path, "temp"); + if (existsSync(this.tempPath)) { + rmSync(this.tempPath, { recursive: true }); + } + mkdirSync(this.tempPath); + this.logInfo(`Created temp working folder at "${this.tempPath}"`); + + this.logInfo(`Ready!`); + } + + private getFilePath(hash: string) { + return join(this.path, hash[0], `${hash[0]}${hash[1]}`, hash); + } + + public AddFile(contents: Buffer | string) { + return new Promise(async (resolve, reject) => { + const hasher = createHash("sha1"); + hasher.setEncoding("hex"); + hasher.write(contents); + hasher.end(); + const hash: string = hasher.read(); + + if (await this.FileExists(hash)) { + return resolve(hash); + } + + writeFile(this.getFilePath(hash), contents, (err) => { + if (err) { + return reject(err); + } + + resolve(hash); + }) + }); + } + + public AddFromStream(stream: Stream) { + return new Promise(async (resolve, reject) => { + const hasher = createHash("sha1"); + hasher.setEncoding("hex"); + + const tempFilePath = join(this.tempPath, randomBytes(16).toString("base64url")); + const tempFile = createWriteStream(tempFilePath); + stream.pipe(tempFile); + stream.pipe(hasher); + tempFile.on("close", () => { + const hash: string = hasher.read(); + rename(tempFilePath, this.getFilePath(hash), (err) => { + if (err) { + return reject(err); + } + + this.logInfo(`Stored file as ${hash}`); + resolve(hash); + }); + }); + }); + } + + public FileExists(hash: string) { + return new Promise((resolve, reject) => { + stat(this.getFilePath(hash), (err, _stat) => { + if (err) { + if (err.code === "ENOENT") { + resolve(false); + } else { + reject(err); + } + } else { + resolve(true); + } + }); + }); + } +} \ No newline at end of file diff --git a/objects/Session.ts b/objects/Session.ts index 010ff5f..3fc97b6 100644 --- a/objects/Session.ts +++ b/objects/Session.ts @@ -25,7 +25,7 @@ export default abstract class Session { validPeriod.setTime(validPeriod.getTime() + Config.session.validity); const key = randomBytes(Config.session.length).toString("hex"); - Session.Sessions.set(key, new SessionUser(user.Id, user.UserType, validPeriod)); + Session.Sessions.set(key, new SessionUser(user.Id, user.Username, user.UserType, validPeriod)); res.setCookie("EHP_SESSION", key, { path: "/", diff --git a/objects/SessionUser.ts b/objects/SessionUser.ts index 3371ea3..a4fb9d3 100644 --- a/objects/SessionUser.ts +++ b/objects/SessionUser.ts @@ -1,12 +1,14 @@ import UserType from "../enums/UserType"; export default class SessionUser { - public readonly userId:number; - public readonly userType:UserType; - public readonly validityPeriod:Date; + public readonly userId: number; + public readonly username: string; + public readonly userType: UserType; + public readonly validityPeriod: Date; - constructor(userId:number, userType: UserType, validityPeriod:Date) { + constructor(userId:number, username: string, userType: UserType, validityPeriod:Date) { this.userId = userId; + this.username = username; this.userType = userType; this.validityPeriod = validityPeriod; } diff --git a/repos/ImageRepo.ts b/repos/ImageRepo.ts new file mode 100644 index 0000000..dd5be67 --- /dev/null +++ b/repos/ImageRepo.ts @@ -0,0 +1,61 @@ +import Image from "../entities/Image"; +import Database from "../objects/Database"; +import RepoBase from "./RepoBase"; + +export default abstract class ImageRepo { + public static async SelectAll() { + const dbImage = await Database.Instance.query("SELECT * FROM Image WHERE IsDeleted = 0"); + const images = new Array(); + + for (const row of dbImage) { + const image = new Image(); + PopulateImageFromDB(image, row); + images.push(image); + } + + return images; + } + + public static async SelectById(id:number) { + const dbImage = await Database.Instance.query("SELECT * FROM Image WHERE Id = ? LIMIT 1", [id]); + if (dbImage == null || dbImage.length === 0) { + return null; + } else { + const image = new Image(); + PopulateImageFromDB(image, dbImage[0]); + return image; + } + } + + public static async InsertUpdate(image: Image) { + if (image.Id === Number.MIN_VALUE) { + image.Id = (await Database.Instance.query("INSERT Image (UserId, DomainId, FileName, ImageTag, ImageType, Hash, FileSize, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ + image.UserId, image.DomainId, image.FileName, image.ImageTag, image.Hash, image.FileSize, image.CreatedByUserId, image.CreatedDatetime.getTime(), image.LastModifiedByUserId ?? null, image.LastModifiedDatetime?.getTime() ?? null, image.DeletedByUserId ?? null, image.DeletedDatetime?.getTime() ?? null, Number(image.IsDeleted) + ]))[0]["Id"]; + } else { + await Database.Instance.query(`UPDATE Image SET UserId = ?, DomainId = ?, FileName = ?, ImageTag = ?, ImageType = ?, Hash = ?, FileSize = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ + image.UserId, image.DomainId, image.FileName, image.ImageTag, image.Hash, image.FileSize, image.CreatedByUserId, image.CreatedDatetime.getTime(), image.LastModifiedByUserId ?? null, image.LastModifiedDatetime?.getTime() ?? null, image.DeletedByUserId ?? null, image.DeletedDatetime?.getTime() ?? null, Number(image.IsDeleted), image.Id + ]); + } + + return image; + } +} + +function PopulateImageFromDB(image: Image, dbImage: any) { + image.Id = dbImage.Id; + image.UserId = dbImage.UserId; + image.DomainId = dbImage.DomainId; + image.FileName = dbImage.FileName; + image.ImageTag = dbImage.ImageTag; + image.ImageType = dbImage.ImageType; + image.Hash = dbImage.Hash; + image.FileSize = dbImage.FileSize; + image.CreatedByUserId = dbImage.CreatedByUserId; + image.CreatedDatetime = new Date(dbImage.CreatedDatetime); + image.LastModifiedByUserId = dbImage.LastModifiedByUserId; + image.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbImage.LastModifiedDatetime); + image.DeletedByUserId = dbImage.DeletedByUserId; + image.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbImage.DeletedDatetime); + image.IsDeleted = dbImage.IsDeleted[0] === 1; +} \ No newline at end of file diff --git a/repos/UserRepo.ts b/repos/UserRepo.ts index 591b88a..6695067 100644 --- a/repos/UserRepo.ts +++ b/repos/UserRepo.ts @@ -51,12 +51,12 @@ export default abstract class UserRepo { public static async InsertUpdate(user:User) { if (user.Id === Number.MIN_VALUE) { - user.Id = (await Database.Instance.query("INSERT User (UserTypeId, Username, PasswordHash, PasswordSalt, ApiKey, UploadKey, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ - user.UserType, user.Username, user.PasswordHash, user.PasswordSalt, user.ApiKey, user.UploadKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted) + user.Id = (await Database.Instance.query("INSERT User (UserTypeId, Username, EmailAddress, PasswordHash, PasswordSalt, ApiKey, UploadKey, Verified, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ + user.UserType, user.Username, user.EmailAddress, user.PasswordHash, user.PasswordSalt, user.ApiKey, user.UploadKey, Number(user.Verified), user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted) ]))[0]["Id"]; } else { - await Database.Instance.query(`UPDATE User SET UserTypeId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ - user.UserType, user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id + await Database.Instance.query(`UPDATE User SET UserTypeId = ?, Username = ?, EmailAddress = ?, PasswordHash = ?, PasswordSalt = ?, ApiKey = ?, UploadKey = ?, Verified = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ + user.UserType, user.Username, user.EmailAddress, user.PasswordHash, user.PasswordSalt, user.ApiKey, user.UploadKey, Number(user.Verified), user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id ]); } @@ -68,8 +68,12 @@ function PopulateUserFromDB(user:User, dbUser:any) { user.Id = dbUser.Id; user.UserType = dbUser.UserTypeId; user.Username = dbUser.Username; + user.EmailAddress = dbUser.EmailAddress; user.PasswordHash = dbUser.PasswordHash; user.PasswordSalt = dbUser.PasswordSalt; + user.ApiKey = dbUser.ApiKey; + user.UploadKey = dbUser.UploadKey; + user.Verified = dbUser.Verified[0] === 1; user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedDatetime = new Date(dbUser.CreatedDatetime); user.LastModifiedByUserId = dbUser.LastModifiedByUserId; diff --git a/services/UserService.ts b/services/UserService.ts index a2f5e47..6790e3f 100644 --- a/services/UserService.ts +++ b/services/UserService.ts @@ -50,7 +50,7 @@ export default abstract class UserService { } } - public static async CreateUser(currentUserId:number, username:string, password:string) { + public static async CreateUser(currentUserId: number, username: string, email: string, password: string) { try { const existingCheck = await UserRepo.SelectByUsername(username); if (existingCheck) { @@ -60,6 +60,7 @@ export default abstract class UserService { const user = new User(); user.UserType = UserType.User; user.Username = username; + user.EmailAddress = email; user.PasswordSalt = PasswordUtility.GenerateSalt(); user.PasswordHash = await PasswordUtility.HashPassword(user.PasswordSalt, password); user.CreatedByUserId = currentUserId; diff --git a/utilities/ConsoleUtility.ts b/utilities/ConsoleUtility.ts index 926399e..ca8349f 100644 --- a/utilities/ConsoleUtility.ts +++ b/utilities/ConsoleUtility.ts @@ -1,4 +1,5 @@ import { green, yellow, red, gray } from "dyetty"; +import { Console } from "hsconsole"; export default abstract class ConsoleUtility { public static StatusColor(statusCode: number) { diff --git a/views/account/login.ejs b/views/account/login.ejs new file mode 100644 index 0000000..a25fddd --- /dev/null +++ b/views/account/login.ejs @@ -0,0 +1,27 @@ +<%- include("../base/header", { title: "Login", session }) %> + +
+
+
+

EUS Login

+ <% if (typeof(message) === "string") { %> + + <% } %> +
+ " > + " required /> + +
+ +
+ +
+
+
+
+
+
+ +<%- include("../base/footer") %> \ No newline at end of file diff --git a/views/account/register.ejs b/views/account/register.ejs new file mode 100644 index 0000000..323b43f --- /dev/null +++ b/views/account/register.ejs @@ -0,0 +1,35 @@ +<%- include("../base/header", { title: "Register", session }) %> + +
+
+
+

EUS Registration

+ <% if (typeof(message) === "string") { %> + + <% } %> +
+ " > +
+ + " required autocomplete="new-password" /> +
+
+ " required autocomplete="new-password" /> + " required autocomplete="new-password" /> +
+ + +
+ +
+ +
+
+
+
+
+
+ +<%- include("../base/footer") %> \ No newline at end of file