WIP: Accounts & Uploading

This commit is contained in:
Holly Stubbs 2025-01-05 14:22:18 +00:00
parent d343acc9e5
commit 2c5e40b36f
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
20 changed files with 388 additions and 41 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
node_modules/ node_modules/
build/ build/
logs/ logs/
images/
config.json config.json

View file

@ -1,5 +1,6 @@
import LoginViewModel from "../models/account/LoginViewModel"; import LoginViewModel from "../models/account/LoginViewModel";
import RegisterViewModel from "../models/account/RegisterViewModel"; import RegisterViewModel from "../models/account/RegisterViewModel";
import Config from "../objects/Config";
import Session from "../objects/Session"; import Session from "../objects/Session";
import UserService from "../services/UserService"; import UserService from "../services/UserService";
import Controller from "./Controller"; import Controller from "./Controller";
@ -32,13 +33,22 @@ export default class AccountController extends Controller {
} }
public async Register_Post_AllowAnonymous(registerViewModel: RegisterViewModel) { 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(); return this.badRequest();
} }
const username = registerViewModel.username.replaceAll("<", "&lt;").replaceAll(">", "&gt;"); if (registerViewModel.registerKey !== Config.accounts.signup.key) {
if (!await UserService.CreateUser(0, username, registerViewModel.password)) {
registerViewModel.password = ""; registerViewModel.password = "";
registerViewModel.password2 = "";
registerViewModel.message = "Incorrect Registration Key.";
return this.view(registerViewModel);
}
const username = registerViewModel.username.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
if (!await UserService.CreateUser(0, username, registerViewModel.email.trim(), registerViewModel.password)) {
registerViewModel.password = "";
registerViewModel.password2 = "";
registerViewModel.message = "Sorry! That username is already taken."; registerViewModel.message = "Sorry! That username is already taken.";
return this.view(registerViewModel); return this.view(registerViewModel);
@ -47,6 +57,7 @@ export default class AccountController extends Controller {
const user = await UserService.GetUserByUsername(username); const user = await UserService.GetUserByUsername(username);
if (!user) { if (!user) {
registerViewModel.password = ""; registerViewModel.password = "";
registerViewModel.password2 = "";
registerViewModel.message = "Failed to create your account, please try again later."; registerViewModel.message = "Failed to create your account, please try again later.";
return this.view(registerViewModel); return this.view(registerViewModel);

View file

@ -0,0 +1,5 @@
import Controller from "./Controller";
export default class ApiController extends Controller {
}

View file

@ -4,6 +4,7 @@ import Session from "../objects/Session";
import SessionUser from "../objects/SessionUser"; import SessionUser from "../objects/SessionUser";
import RequestCtx from "../objects/RequestCtx"; import RequestCtx from "../objects/RequestCtx";
import UserType from "../enums/UserType"; import UserType from "../enums/UserType";
import { cyan } from "dyetty";
// prepare for ts-ignore :3 // prepare for ts-ignore :3
// TODO: figure out some runtime field / type checking so // TODO: figure out some runtime field / type checking so
@ -12,6 +13,10 @@ export default abstract class Controller {
public static FastifyInstance:FastifyInstance; public static FastifyInstance:FastifyInstance;
public static RegisteredPaths:Array<string> = []; public static RegisteredPaths:Array<string> = [];
private logInfo(logText: string) {
Console.printInfo(`[ ${cyan("CONTROLLER")} ] ${logText}`);
}
public constructor() { public constructor() {
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
const rawControllerParts = this.constructor.name.split("_"); const rawControllerParts = this.constructor.name.split("_");
@ -24,7 +29,7 @@ export default abstract class Controller {
const userType = prop.split("$")[1]; const userType = prop.split("$")[1];
// @ts-ignore // @ts-ignore
controllerAuthLevels.push(UserType[userType]); 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(); thisMethodHttpMethod = param.toLowerCase();
// @ts-ignore // @ts-ignore
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName === "index" ? "" : methodName}`, requestHandler); 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}`); Controller.RegisteredPaths.push(`/${controllerName}/${methodName === "index" ? "" : methodName}`);
if (methodName === "index") { if (methodName === "index") {
// @ts-ignore // @ts-ignore
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName}`, requestHandler); 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}`); Controller.RegisteredPaths.push(`/${controllerName}/${methodName}`);
// @ts-ignore // @ts-ignore
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}`, requestHandler); 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}`); Controller.RegisteredPaths.push(`/${controllerName}`);
} else if (controllerName === "home") { } else if (controllerName === "home") {
// @ts-ignore // @ts-ignore
Controller.FastifyInstance[param.toLowerCase()](`/${methodName}`, requestHandler); 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}`); Controller.RegisteredPaths.push(`/${methodName}`);
} }
} else if (param.startsWith("Auth")) { } else if (param.startsWith("Auth")) {
@ -116,7 +121,7 @@ export default abstract class Controller {
} }
// @ts-ignore // @ts-ignore
actionAuthLevels[nameWithMethod].push(UserType[userType]); 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) { for (const httpMethod of funcMethods) {
// @ts-ignore // @ts-ignore
Controller.FastifyInstance[httpMethod.toLowerCase()](`/`, requestHandler); 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(`/`); Controller.RegisteredPaths.push(`/`);
} }
} }

View file

@ -1,14 +1,29 @@
import Config from "../objects/Config";
import HashFS from "../objects/HashFS";
import Controller from "./Controller"; import Controller from "./Controller";
import { randomBytes } from "crypto";
export default class HomeController extends Controller { export default class HomeController extends Controller {
public Index_Get_AllowAnonymous() { public Index_Get_AllowAnonymous() {
return this.view(); return this.view();
} }
public Upload_Post_AllowAnonymous() { public async Upload_Post_AllowAnonymous() {
console.log(this.req.headers.authorization); const data = await this.req.file();
console.log(this.req.body); 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();
} }
} }

17
entities/Image.ts Normal file
View file

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

View file

@ -9,6 +9,7 @@ export default class User {
public PasswordSalt: string = ""; public PasswordSalt: string = "";
public ApiKey: string = ""; public ApiKey: string = "";
public UploadKey: string = ""; public UploadKey: string = "";
public Verified: boolean = false;
public CreatedByUserId: number = Number.MIN_VALUE; public CreatedByUserId: number = Number.MIN_VALUE;
public CreatedDatetime: Date = new Date(); public CreatedDatetime: Date = new Date();
public LastModifiedByUserId?: number; public LastModifiedByUserId?: number;

View file

@ -12,8 +12,10 @@ import HomeController from "./controllers/HomeController";
import Database from "./objects/Database"; import Database from "./objects/Database";
import { join } from "path"; import { join } from "path";
import AccountController from "./controllers/AccountController"; 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 ConsoleUtility from "./utilities/ConsoleUtility";
import HashFS from "./objects/HashFS";
import { existsSync, mkdirSync, rmSync } from "fs";
Console.customHeader(`EUS server started at ${new Date()}`); Console.customHeader(`EUS server started at ${new Date()}`);
@ -56,7 +58,7 @@ fastify.addHook("preValidation", (req, res, done) => {
return done(); return done();
} else { } else {
// @ts-ignore // @ts-ignore
req.logType = magenta(" STATIC "); req.logType = magenta("STATIC");
} }
done(); done();
@ -70,20 +72,32 @@ fastify.addHook("onSend", (req, res, _payload, done) => {
}); });
fastify.setNotFoundHandler(async (req, res) => { fastify.setNotFoundHandler(async (req, res) => {
return res.status(404).view("views/404.ejs", { session: null }); 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; if (Config.database.enabled) {
new AccountController(); new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name);
new HomeController(); } 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) { if (err) {
Console.printError(`Error occured while spinning up fastify:\n${err}`); Console.printError(`Error occured while spinning up fastify:\n${err}`);
process.exit(1); 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")}`);
}); });

View file

@ -1,5 +1,8 @@
export default interface RegisterViewModel { export default interface RegisterViewModel {
message?: string, message?: string,
registerKey: string
username: string, username: string,
password: string email: string,
password: string,
password2: string
} }

View file

@ -2,18 +2,21 @@ import { readFileSync } from "fs";
const config = JSON.parse(readFileSync("./config.json").toString()); const config = JSON.parse(readFileSync("./config.json").toString());
export default abstract class Config { export default abstract class Config {
public static ports:IPorts = config.ports; public static instance: string = config.instance;
public static database:IDatabase = config.database; public static hosts: IHosts = config.hosts;
public static session:ISession = config.session; public static database: IDatabase = config.database;
public static controllers:IControllers = config.controllers; public static session: ISession = config.session;
public static accounts:IAccounts = config.accounts; public static controllers: IControllers = config.controllers;
public static accounts: IAccounts = config.accounts;
} }
interface IPorts { interface IHosts {
web: number webHost: string,
webPort: number
} }
interface IDatabase { interface IDatabase {
enabled: boolean,
address: string, address: string,
port: number, port: number,
username: string, username: string,

View file

@ -1,3 +1,4 @@
import { blue } from "dyetty";
import { Console } from "hsconsole"; import { Console } from "hsconsole";
import { createPool, Pool, RowDataPacket } from "mysql2"; import { createPool, Pool, RowDataPacket } from "mysql2";
@ -21,7 +22,7 @@ export default class Database {
database: databaseName 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; Database.Instance = this;
} }

140
objects/HashFS.ts Normal file
View file

@ -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<string, HashFS> = new FunkyArray<string, HashFS>();
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<string>(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<string>(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<boolean>((resolve, reject) => {
stat(this.getFilePath(hash), (err, _stat) => {
if (err) {
if (err.code === "ENOENT") {
resolve(false);
} else {
reject(err);
}
} else {
resolve(true);
}
});
});
}
}

View file

@ -25,7 +25,7 @@ export default abstract class Session {
validPeriod.setTime(validPeriod.getTime() + Config.session.validity); validPeriod.setTime(validPeriod.getTime() + Config.session.validity);
const key = randomBytes(Config.session.length).toString("hex"); 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, { res.setCookie("EHP_SESSION", key, {
path: "/", path: "/",

View file

@ -1,12 +1,14 @@
import UserType from "../enums/UserType"; import UserType from "../enums/UserType";
export default class SessionUser { export default class SessionUser {
public readonly userId:number; public readonly userId: number;
public readonly userType:UserType; public readonly username: string;
public readonly validityPeriod:Date; 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.userId = userId;
this.username = username;
this.userType = userType; this.userType = userType;
this.validityPeriod = validityPeriod; this.validityPeriod = validityPeriod;
} }

61
repos/ImageRepo.ts Normal file
View file

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

View file

@ -51,12 +51,12 @@ export default abstract class UserRepo {
public static async InsertUpdate(user:User) { public static async InsertUpdate(user:User) {
if (user.Id === Number.MIN_VALUE) { 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.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.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.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"]; ]))[0]["Id"];
} else { } else {
await Database.Instance.query(`UPDATE User SET UserTypeId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE 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.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 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.Id = dbUser.Id;
user.UserType = dbUser.UserTypeId; user.UserType = dbUser.UserTypeId;
user.Username = dbUser.Username; user.Username = dbUser.Username;
user.EmailAddress = dbUser.EmailAddress;
user.PasswordHash = dbUser.PasswordHash; user.PasswordHash = dbUser.PasswordHash;
user.PasswordSalt = dbUser.PasswordSalt; user.PasswordSalt = dbUser.PasswordSalt;
user.ApiKey = dbUser.ApiKey;
user.UploadKey = dbUser.UploadKey;
user.Verified = dbUser.Verified[0] === 1;
user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = new Date(dbUser.CreatedDatetime); user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
user.LastModifiedByUserId = dbUser.LastModifiedByUserId; user.LastModifiedByUserId = dbUser.LastModifiedByUserId;

View file

@ -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 { try {
const existingCheck = await UserRepo.SelectByUsername(username); const existingCheck = await UserRepo.SelectByUsername(username);
if (existingCheck) { if (existingCheck) {
@ -60,6 +60,7 @@ export default abstract class UserService {
const user = new User(); const user = new User();
user.UserType = UserType.User; user.UserType = UserType.User;
user.Username = username; user.Username = username;
user.EmailAddress = email;
user.PasswordSalt = PasswordUtility.GenerateSalt(); user.PasswordSalt = PasswordUtility.GenerateSalt();
user.PasswordHash = await PasswordUtility.HashPassword(user.PasswordSalt, password); user.PasswordHash = await PasswordUtility.HashPassword(user.PasswordSalt, password);
user.CreatedByUserId = currentUserId; user.CreatedByUserId = currentUserId;

View file

@ -1,4 +1,5 @@
import { green, yellow, red, gray } from "dyetty"; import { green, yellow, red, gray } from "dyetty";
import { Console } from "hsconsole";
export default abstract class ConsoleUtility { export default abstract class ConsoleUtility {
public static StatusColor(statusCode: number) { public static StatusColor(statusCode: number) {

27
views/account/login.ejs Normal file
View file

@ -0,0 +1,27 @@
<%- include("../base/header", { title: "Login", session }) %>
<div class="d-flex justify-content-center">
<div class="card my-auto" style="width: 25rem;">
<div class="card-body">
<h4 class="card-title text-center">EUS Login</h5>
<% if (typeof(message) === "string") { %>
<div class="alert alert-danger text-center" role="alert"><%= message %></div>
<% } %>
<form action="/account/login" method="POST">
<input type="hidden" name="returnTo" value="<%= typeof(returnTo) === "undefined" ? "" : returnTo %>" >
<input class="form-control mt-3 mb-2" name="username" placeholder="Username" value="<%= typeof(username) === "undefined" ? "" : username %>" required />
<input class="form-control mb-3" name="password" type="password" placeholder="Password" required />
<div class="row">
<div class="col d-flex justify-content-center">
<a class="align-self-center" href="/account/register">I don't have an account.</a>
</div>
<div class="col-auto me-3">
<input class="btn btn-primary mx-auto d-block" type="submit" value="Login" />
</div>
</div>
</form>
</div>
</div>
</div>
<%- include("../base/footer") %>

View file

@ -0,0 +1,35 @@
<%- include("../base/header", { title: "Register", session }) %>
<div class="d-flex justify-content-center">
<div class="card my-auto" style="width: 25rem;">
<div class="card-body">
<h4 class="card-title text-center">EUS Registration</h5>
<% if (typeof(message) === "string") { %>
<div class="alert alert-danger text-center" role="alert"><%= message %></div>
<% } %>
<form action="/account/register" method="POST">
<input type="hidden" name="returnTo" value="<%= typeof(returnTo) === "undefined" ? "" : returnTo %>" >
<div class="input-group mt-3 mb-2">
<span class="input-group-text"><i class="bi bi-key-fill"></i></span>
<input class="form-control" name="registerKey" placeholder="Registration Key" value="<%= typeof(registerKey) === "undefined" ? "" : registerKey %>" required autocomplete="new-password" />
</div>
<hr>
<input class="form-control mt-3 mb-2" name="username" placeholder="Username" value="<%= typeof(username) === "undefined" ? "" : username %>" required autocomplete="new-password" />
<input class="form-control mt-3 mb-2" name="email" type="email" placeholder="Email Address" value="<%= typeof(email) === "undefined" ? "" : email %>" required autocomplete="new-password" />
<hr>
<input class="form-control mb-3" name="password" type="password" placeholder="Password" required autocomplete="new-password" />
<input class="form-control mb-3" name="password2" type="password" placeholder="Confirm Password" required autocomplete="new-password" />
<div class="row">
<div class="col d-flex justify-content-center">
<a class="align-self-center" href="/account/login">I have an account!</a>
</div>
<div class="col-auto me-3">
<input class="btn btn-primary mx-auto d-block" type="submit" value="Register" />
</div>
</div>
</form>
</div>
</div>
</div>
<%- include("../base/footer") %>