WIP: Request Logging
This commit is contained in:
parent
c4cd41c03c
commit
d343acc9e5
23 changed files with 505 additions and 24 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
node_modules/
|
||||
build/
|
||||
logs/
|
||||
config.json
|
65
controllers/AccountController.ts
Normal file
65
controllers/AccountController.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import LoginViewModel from "../models/account/LoginViewModel";
|
||||
import RegisterViewModel from "../models/account/RegisterViewModel";
|
||||
import Session from "../objects/Session";
|
||||
import UserService from "../services/UserService";
|
||||
import Controller from "./Controller";
|
||||
|
||||
export default class AccountController extends Controller {
|
||||
public async Login_Get_AllowAnonymous() {
|
||||
return this.view();
|
||||
}
|
||||
|
||||
public async Login_Post_AllowAnonymous(loginViewModel: LoginViewModel) {
|
||||
if (typeof(loginViewModel.username) !== "string" || typeof(loginViewModel.password) !== "string") {
|
||||
return this.badRequest();
|
||||
}
|
||||
|
||||
const user = await UserService.AuthenticateUser(loginViewModel.username, loginViewModel.password);
|
||||
if (!user) {
|
||||
loginViewModel.password = "";
|
||||
loginViewModel.message = "Username or Password is incorrect";
|
||||
|
||||
return this.view(loginViewModel);
|
||||
}
|
||||
|
||||
Session.AssignUserSession(this.res, user);
|
||||
|
||||
return this.redirectToAction("index", "home");
|
||||
}
|
||||
|
||||
public async Register_Get_AllowAnonymous() {
|
||||
return this.view();
|
||||
}
|
||||
|
||||
public async Register_Post_AllowAnonymous(registerViewModel: RegisterViewModel) {
|
||||
if (typeof(registerViewModel.username) !== "string" || typeof(registerViewModel.password) !== "string") {
|
||||
return this.badRequest();
|
||||
}
|
||||
|
||||
const username = registerViewModel.username.replaceAll("<", "<").replaceAll(">", ">");
|
||||
if (!await UserService.CreateUser(0, username, registerViewModel.password)) {
|
||||
registerViewModel.password = "";
|
||||
registerViewModel.message = "Sorry! That username is already taken.";
|
||||
|
||||
return this.view(registerViewModel);
|
||||
}
|
||||
|
||||
const user = await UserService.GetUserByUsername(username);
|
||||
if (!user) {
|
||||
registerViewModel.password = "";
|
||||
registerViewModel.message = "Failed to create your account, please try again later.";
|
||||
|
||||
return this.view(registerViewModel);
|
||||
}
|
||||
|
||||
Session.AssignUserSession(this.res, user);
|
||||
|
||||
return this.redirectToAction("index", "home");
|
||||
}
|
||||
|
||||
public async Logout_Get_AllowAnonymous() {
|
||||
Session.Clear(this.req.cookies, this.res);
|
||||
|
||||
return this.redirectToAction("index", "home");
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ import UserType from "../enums/UserType";
|
|||
// can auto badRequest on missing stuff.
|
||||
export default abstract class Controller {
|
||||
public static FastifyInstance:FastifyInstance;
|
||||
public static RegisteredPaths:Array<string> = [];
|
||||
|
||||
public constructor() {
|
||||
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
|
||||
|
@ -20,10 +21,10 @@ export default abstract class Controller {
|
|||
|
||||
for (const prop of rawControllerParts) {
|
||||
if (prop.startsWith("Auth")) {
|
||||
const userLevel = prop.split("$")[1];
|
||||
const userType = prop.split("$")[1];
|
||||
// @ts-ignore
|
||||
controllerAuthLevels.push(UserLevel[userLevel]);
|
||||
Console.printInfo(`Set Auth level requirement for ${this.constructor.name} to ${userLevel}`);
|
||||
controllerAuthLevels.push(UserType[userType]);
|
||||
Console.printInfo(`Set Auth level requirement for ${this.constructor.name} to ${userType}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,6 +42,7 @@ export default abstract class Controller {
|
|||
// @ts-ignore
|
||||
const controllerRequestHandler = this[method];
|
||||
const requestHandler = (req:FastifyRequest, res:FastifyReply) => {
|
||||
const requestStartTime = Date.now();
|
||||
let session = Session.CheckValiditiy(req.cookies);
|
||||
if (doAuth && session === undefined) {
|
||||
return res.redirect(`/account/login?returnTo=${encodeURIComponent(req.url)}`);
|
||||
|
@ -90,37 +92,46 @@ export default abstract class Controller {
|
|||
// @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}`);
|
||||
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}`);
|
||||
Controller.RegisteredPaths.push(`/${controllerName}/${methodName}`);
|
||||
// @ts-ignore
|
||||
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}`, requestHandler);
|
||||
Console.printInfo(`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}`);
|
||||
Controller.RegisteredPaths.push(`/${methodName}`);
|
||||
}
|
||||
} else if (param.startsWith("Auth")) {
|
||||
const nameWithMethod = `${controllerName}_${methodName}_${thisMethodHttpMethod}`;
|
||||
const userLevel = param.split("$")[1];
|
||||
const userType = param.split("$")[1];
|
||||
if (!(nameWithMethod in actionAuthLevels)) {
|
||||
actionAuthLevels[nameWithMethod] = [];
|
||||
}
|
||||
// @ts-ignore
|
||||
actionAuthLevels[nameWithMethod].push(UserLevel[userLevel]);
|
||||
Console.printInfo(`Set Auth level requirement for ${this.constructor.name}.${method} to ${userLevel}`);
|
||||
actionAuthLevels[nameWithMethod].push(UserType[userType]);
|
||||
Console.printInfo(`Set Auth level requirement for ${this.constructor.name}.${method} to ${userType}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (controllerName === "home" && methodName === "index") {
|
||||
for (const httpMethod of funcMethods) {
|
||||
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/" as ${httpMethod}`);
|
||||
// @ts-ignore
|
||||
Controller.FastifyInstance[httpMethod.toLowerCase()](`/`, requestHandler);
|
||||
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/" as ${httpMethod}`);
|
||||
Controller.RegisteredPaths.push(`/`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not real, these RequestCtx so they autocomplete :)
|
||||
// not real, these should mirror RequestCtx so they autocomplete :)
|
||||
// yeah, i know. this is terrible.
|
||||
|
||||
// Fields
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
import Controller from "./Controller";
|
||||
|
||||
export default class HomeController extends Controller {
|
||||
public AllowAnonymous_Index() {
|
||||
public Index_Get_AllowAnonymous() {
|
||||
return this.view();
|
||||
}
|
||||
|
||||
public Upload_Post_AllowAnonymous() {
|
||||
console.log(this.req.headers.authorization);
|
||||
console.log(this.req.body);
|
||||
|
||||
return this.ok();
|
||||
}
|
||||
}
|
19
entities/User.ts
Normal file
19
entities/User.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import UserType from "../enums/UserType";
|
||||
|
||||
export default class User {
|
||||
public Id: number = Number.MIN_VALUE;
|
||||
public UserType: UserType = UserType.Unknown;
|
||||
public Username: string = "";
|
||||
public EmailAddress: string = "";
|
||||
public PasswordHash: string = "";
|
||||
public PasswordSalt: string = "";
|
||||
public ApiKey: string = "";
|
||||
public UploadKey: string = "";
|
||||
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;
|
||||
}
|
33
index.ts
33
index.ts
|
@ -11,6 +11,9 @@ import Controller from "./controllers/Controller";
|
|||
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 ConsoleUtility from "./utilities/ConsoleUtility";
|
||||
|
||||
Console.customHeader(`EUS server started at ${new Date()}`);
|
||||
|
||||
|
@ -38,16 +41,42 @@ fastify.register(FastifyCookie, {
|
|||
|
||||
fastify.register(FastifyStatic, {
|
||||
root: join(__dirname, "wwwroot"),
|
||||
preCompressed: true
|
||||
//prefix: `${Config.ports.web}/static/`
|
||||
});
|
||||
|
||||
fastify.setNotFoundHandler(async (_req, res) => {
|
||||
return res.status(404).view("views/404.ejs", { });
|
||||
fastify.addHook("preValidation", (req, res, done) => {
|
||||
// @ts-ignore
|
||||
req.startTime = Date.now();
|
||||
|
||||
// * Take usual controller path if this path is registered.
|
||||
if (Controller.RegisteredPaths.includes(req.url)) {
|
||||
// @ts-ignore
|
||||
req.logType = cyan("CONTROLLER");
|
||||
return done();
|
||||
} else {
|
||||
// @ts-ignore
|
||||
req.logType = magenta(" STATIC ");
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.addHook("onSend", (req, res, _payload, done) => {
|
||||
// @ts-ignore
|
||||
Console.printInfo(`[ ${req.logType} ] [ ${ConsoleUtility.StatusColor(res.statusCode)} ] [ ${blue(`${Date.now() - req.startTime}ms`)} ] > ${req.url}`);
|
||||
|
||||
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);
|
||||
|
||||
Controller.FastifyInstance = fastify;
|
||||
new AccountController();
|
||||
new HomeController();
|
||||
|
||||
fastify.listen({ port: Config.ports.web, host: "127.0.0.1" }, (err, address) => {
|
||||
|
|
5
models/account/LoginViewModel.ts
Normal file
5
models/account/LoginViewModel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default interface LoginViewModel {
|
||||
message?: string,
|
||||
username: string,
|
||||
password: string
|
||||
}
|
5
models/account/RegisterViewModel.ts
Normal file
5
models/account/RegisterViewModel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default interface RegisterViewModel {
|
||||
message?: string,
|
||||
username: string,
|
||||
password: string
|
||||
}
|
|
@ -5,6 +5,8 @@ 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;
|
||||
}
|
||||
|
||||
interface IPorts {
|
||||
|
@ -23,4 +25,23 @@ interface ISession {
|
|||
secret: string,
|
||||
validity: number,
|
||||
length: number
|
||||
}
|
||||
|
||||
interface IControllers {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface ISignup {
|
||||
enabled: boolean,
|
||||
key: string | null
|
||||
}
|
||||
|
||||
interface IPbkdf2 {
|
||||
itterations: number,
|
||||
keylength: number
|
||||
}
|
||||
|
||||
interface IAccounts {
|
||||
signup: ISignup,
|
||||
pbkdf2: IPbkdf2
|
||||
}
|
|
@ -31,7 +31,7 @@ export default class RequestCtx {
|
|||
// @ts-ignore inject session
|
||||
viewModel["session"] = this.session;
|
||||
// @ts-ignore inject enums
|
||||
viewModel["UserLevel"] = UserLevel;
|
||||
viewModel["UserType"] = UserType;
|
||||
return this.res.view(`views/${this.controllerName}/${viewName}.ejs`, viewModel);
|
||||
}
|
||||
|
||||
|
|
|
@ -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, validPeriod));
|
||||
Session.Sessions.set(key, new SessionUser(user.Id, user.UserType, validPeriod));
|
||||
|
||||
res.setCookie("EHP_SESSION", key, {
|
||||
path: "/",
|
||||
|
|
17
package-lock.json
generated
17
package-lock.json
generated
|
@ -14,15 +14,16 @@
|
|||
"@fastify/multipart": "^9.0.1",
|
||||
"@fastify/static": "^8.0.3",
|
||||
"@fastify/view": "^10.0.1",
|
||||
"dyetty": "^1.0.1",
|
||||
"ejs": "^3.1.10",
|
||||
"fastify": "^5.2.0",
|
||||
"funky-array": "^1.0.0",
|
||||
"hsconsole": "^1.0.2",
|
||||
"hsconsole": "^1.1.0",
|
||||
"mysql2": "^3.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/node": "^22.10.4",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"check-outdated": "^2.12.0",
|
||||
"nodemon": "^3.1.9",
|
||||
|
@ -266,9 +267,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
||||
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
|
||||
"version": "22.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.4.tgz",
|
||||
"integrity": "sha512-99l6wv4HEzBQhvaU/UGoeBoCK61SCROQaCCGyQSgX2tEQ3rKkNZ2S7CEWnS/4s1LV+8ODdK21UeyR1fHP2mXug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -988,9 +989,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/hsconsole": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hsconsole/-/hsconsole-1.0.2.tgz",
|
||||
"integrity": "sha512-st+jaSpNw3uoIhE5vl2lVN8Op8yQF2FyLRdBG68s8vqjduJdKUGtoEXd8Zxe6du1zzpFHHRcU3zJbAq8BOmYQA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/hsconsole/-/hsconsole-1.1.0.tgz",
|
||||
"integrity": "sha512-nQtnapTLf/d090AloKJkVbf15yXNaISYYHC21cwOLClF7hlJs2rlHF3JLaltspK/O2uhz6WcRMsE+2Yn6D8UEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dyetty": "^1.0.1"
|
||||
|
|
|
@ -13,11 +13,11 @@
|
|||
"scripts": {
|
||||
"updateCheck": "check-outdated",
|
||||
"dev": "nodemon --watch './**/*.ts' index.ts",
|
||||
"build": "tsx --build --clean"
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/node": "^22.10.4",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"check-outdated": "^2.12.0",
|
||||
"nodemon": "^3.1.9",
|
||||
|
@ -30,10 +30,11 @@
|
|||
"@fastify/multipart": "^9.0.1",
|
||||
"@fastify/static": "^8.0.3",
|
||||
"@fastify/view": "^10.0.1",
|
||||
"dyetty": "^1.0.1",
|
||||
"ejs": "^3.1.10",
|
||||
"fastify": "^5.2.0",
|
||||
"funky-array": "^1.0.0",
|
||||
"hsconsole": "^1.0.2",
|
||||
"hsconsole": "^1.1.0",
|
||||
"mysql2": "^3.12.0"
|
||||
}
|
||||
}
|
||||
|
|
5
repos/RepoBase.ts
Normal file
5
repos/RepoBase.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default class RepoBase {
|
||||
public static convertNullableDatetimeIntToDate(dateTimeInt?:number) {
|
||||
return dateTimeInt ? new Date(dateTimeInt) : undefined;
|
||||
}
|
||||
}
|
80
repos/UserRepo.ts
Normal file
80
repos/UserRepo.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import Database from "../objects/Database";
|
||||
import RepoBase from "./RepoBase";
|
||||
import User from "../entities/User";
|
||||
|
||||
export default abstract class UserRepo {
|
||||
public static async SelectAll() {
|
||||
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE IsDeleted = 0");
|
||||
const users = new Array<User>();
|
||||
|
||||
for (const row of dbUser) {
|
||||
const user = new User();
|
||||
PopulateUserFromDB(user, row);
|
||||
users.push(user);
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
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) {
|
||||
return null;
|
||||
} else {
|
||||
const user = new User();
|
||||
PopulateUserFromDB(user, dbUser[0]);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
public static async SelectByUsername(username:string) {
|
||||
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE Username = ? LIMIT 1", [username]);
|
||||
if (dbUser == null || dbUser.length === 0) {
|
||||
return null;
|
||||
} else {
|
||||
const user = new User();
|
||||
PopulateUserFromDB(user, dbUser[0]);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
public static async SelectByEmailAddress(emailAddress:string) {
|
||||
const dbUser = await Database.Instance.query("SELECT * FROM User WHERE EmailAddress = ? LIMIT 1", [emailAddress]);
|
||||
if (dbUser == null || dbUser.length === 0) {
|
||||
return null;
|
||||
} else {
|
||||
const user = new User();
|
||||
PopulateUserFromDB(user, dbUser[0]);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
]))[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
|
||||
]);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
function PopulateUserFromDB(user:User, dbUser:any) {
|
||||
user.Id = dbUser.Id;
|
||||
user.UserType = dbUser.UserTypeId;
|
||||
user.Username = dbUser.Username;
|
||||
user.PasswordHash = dbUser.PasswordHash;
|
||||
user.PasswordSalt = dbUser.PasswordSalt;
|
||||
user.CreatedByUserId = dbUser.CreatedByUserId;
|
||||
user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
|
||||
user.LastModifiedByUserId = dbUser.LastModifiedByUserId;
|
||||
user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
|
||||
user.DeletedByUserId = dbUser.DeletedByUserId;
|
||||
user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
|
||||
user.IsDeleted = dbUser.IsDeleted[0] === 1;
|
||||
}
|
76
services/UserService.ts
Normal file
76
services/UserService.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { Console } from "hsconsole";
|
||||
import UserRepo from "../repos/UserRepo";
|
||||
import PasswordUtility from "../utilities/PasswordUtility";
|
||||
import UserType from "../enums/UserType";
|
||||
import User from "../entities/User";
|
||||
|
||||
export default abstract class UserService {
|
||||
public static async AuthenticateUser(username:string, password:string) {
|
||||
try {
|
||||
const user = await UserRepo.SelectByUsername(username);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (await PasswordUtility.ValidatePassword(user.PasswordHash, user.PasswordSalt, password)) {
|
||||
return user;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
Console.printError(`EUS server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async GetUser(id:number) {
|
||||
try {
|
||||
return await UserRepo.SelectById(id);
|
||||
} catch (e) {
|
||||
Console.printError(`EUS server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async GetAll() {
|
||||
try {
|
||||
return await UserRepo.SelectAll();
|
||||
} catch (e) {
|
||||
Console.printError(`EUS server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async GetUserByUsername(username:string) {
|
||||
try {
|
||||
return await UserRepo.SelectByUsername(username);
|
||||
} catch (e) {
|
||||
Console.printError(`EUS server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static async CreateUser(currentUserId:number, username:string, password:string) {
|
||||
try {
|
||||
const existingCheck = await UserRepo.SelectByUsername(username);
|
||||
if (existingCheck) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = new User();
|
||||
user.UserType = UserType.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);
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
Console.printError(`EUS server service error:\n${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
15
utilities/ConsoleUtility.ts
Normal file
15
utilities/ConsoleUtility.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { green, yellow, red, gray } from "dyetty";
|
||||
|
||||
export default abstract class ConsoleUtility {
|
||||
public static StatusColor(statusCode: number) {
|
||||
if (statusCode < 300) {
|
||||
return `${green(statusCode.toString())}`;
|
||||
} else if (statusCode >= 300 && statusCode < 400) {
|
||||
return `${yellow(statusCode.toString())}`;
|
||||
} else if (statusCode >= 400 && statusCode < 600) {
|
||||
return `${red(statusCode.toString())}`;
|
||||
} else {
|
||||
return `${gray(statusCode.toString())}`;
|
||||
}
|
||||
}
|
||||
}
|
36
utilities/PasswordUtility.ts
Normal file
36
utilities/PasswordUtility.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { pbkdf2, randomBytes } from "crypto";
|
||||
import Config from "../objects/Config";
|
||||
|
||||
export default abstract class PasswordUtility {
|
||||
public static ValidatePassword(hash:string, salt:string, password:string) {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
pbkdf2(password, salt, Config.accounts.pbkdf2.itterations, Config.accounts.pbkdf2.keylength, "sha512", (err, derivedKey) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
} else {
|
||||
if (derivedKey.toString("hex") !== hash) {
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
return resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static HashPassword(salt:string, password:string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
pbkdf2(password, salt, Config.accounts.pbkdf2.itterations, Config.accounts.pbkdf2.keylength, "sha512", (err, derivedKey) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
} else {
|
||||
return resolve(derivedKey.toString("hex"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static GenerateSalt() {
|
||||
return randomBytes(Config.accounts.pbkdf2.keylength).toString("hex");
|
||||
}
|
||||
}
|
5
views/404.ejs
Normal file
5
views/404.ejs
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%- include("./base/header", { title: "404", session }) %>
|
||||
|
||||
<h1>404</h1>
|
||||
|
||||
<%- include("./base/footer") %>
|
36
views/base/footer.ejs
Normal file
36
views/base/footer.ejs
Normal file
|
@ -0,0 +1,36 @@
|
|||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/cookieconsent/3.1.1/cookieconsent.min.js" integrity="sha512-yXXqOFjdjHNH1GND+1EO0jbvvebABpzGKD66djnUfiKlYME5HGMUJHoCaeE4D5PTG2YsSJf6dwqyUUvQvS0vaA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script>
|
||||
(() => {
|
||||
const forms = document.querySelectorAll('.needs-validation')
|
||||
|
||||
Array.from(forms).forEach(form => {
|
||||
form.addEventListener('submit', event => {
|
||||
if (!form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
form.classList.add('was-validated');
|
||||
}, false);
|
||||
});
|
||||
})();
|
||||
|
||||
window.cookieconsent.initialise({
|
||||
"palette": {
|
||||
"popup": {
|
||||
"background": "#0b5ed7",
|
||||
"text": "#fff"
|
||||
},
|
||||
"button": {
|
||||
"background": "#198754",
|
||||
"text": "#fff"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"message": "This site uses cookies to retain your login, no more, no less.<br>If you do not agree with this use of cookies, please do not use this site."
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
57
views/base/header.ejs
Normal file
57
views/base/header.ejs
Normal file
|
@ -0,0 +1,57 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %> - EUS</title>
|
||||
|
||||
<link rel="preconnect" href="https://rsms.me/">
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
font-family: Inter, sans-serif;
|
||||
--bs-font-sans-serif: "Inter, sans-serif";
|
||||
font-feature-settings: 'liga' 1, 'calt' 1; /* fix for Chrome */
|
||||
}
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root {
|
||||
font-family: InterVariable, sans-serif;
|
||||
--bs-font-sans-serif: "InterVariable, sans-serif";
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" integrity="sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" integrity="sha512-dPXYcDub/aeb08c63jRq/k6GaKccl256JQy/AnOq7CAnEZ9FzSL9wSbcZkMp4R26vBsMLFYH4kQ67/bbV8XaCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cookieconsent/3.1.1/cookieconsent.min.css" integrity="sha512-LQ97camar/lOliT/MqjcQs5kWgy6Qz/cCRzzRzUCfv0fotsCTC9ZHXaPQmJV8Xu/PVALfJZ7BDezl5lW3/qBxg==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js" integrity="sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
EUS<%= typeof(isAdmin) === "undefined" ? "" : " Admin" %>
|
||||
</a>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<div class="nav-item">
|
||||
<a class="nav-link" href="/">Home</a>
|
||||
</div>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<% if (typeof(userId) !== "undefined") { %>
|
||||
<div class="nav-item float-end">
|
||||
<a class="nav-link" href="/account/logout">Logout</a>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="nav-item float-end">
|
||||
<a class="nav-link" href="/account/login">Sign In</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container pt-5">
|
5
views/home/index.ejs
Normal file
5
views/home/index.ejs
Normal file
|
@ -0,0 +1,5 @@
|
|||
<%- include("../base/header", { title: "Home", session }) %>
|
||||
|
||||
|
||||
|
||||
<%- include("../base/footer") %>
|
BIN
wwwroot/favicon.ico
Normal file
BIN
wwwroot/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Loading…
Reference in a new issue