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/
build/
logs/
images/
config.json

View file

@ -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("<", "&lt;").replaceAll(">", "&gt;");
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("<", "&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.";
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);

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 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<string> = [];
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(`/`);
}
}

View file

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

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 ApiKey: string = "";
public UploadKey: string = "";
public Verified: boolean = false;
public CreatedByUserId: number = Number.MIN_VALUE;
public CreatedDatetime: Date = new Date();
public LastModifiedByUserId?: number;

View file

@ -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()}`);
@ -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");
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.`);
}
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.ports.web, host: "127.0.0.1" }, (err, address) => {
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")}`);
});

View file

@ -1,5 +1,8 @@
export default interface RegisterViewModel {
message?: string,
registerKey: 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());
export default abstract class Config {
public static ports:IPorts = config.ports;
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,

View file

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

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);
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: "/",

View file

@ -2,11 +2,13 @@ import UserType from "../enums/UserType";
export default class SessionUser {
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;
}

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) {
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;

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 {
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;

View file

@ -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) {

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") %>