mvc the whole web side of multiprobe

This commit is contained in:
Holly Stubbs 2024-09-19 00:41:40 +01:00
parent 2dab36d2ec
commit fecd4ab892
Signed by: tgpholly
GPG Key ID: B8583C4B7D18119E
34 changed files with 614 additions and 409 deletions

View File

@ -0,0 +1,60 @@
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("<", "&lt;").replaceAll(">", "&gt;");
await UserService.CreateUser(0, username, registerViewModel.password);
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");
}
}

View File

@ -0,0 +1,47 @@
import ApiLoginModel from "../models/api/ApiLoginModel";
import Config from "../objects/Config";
import UserService from "../services/UserService";
import Controller from "./Controller";
// for Git server lookup
let cachedVersion = "";
let cacheExpiry = 0;
export default class ApiController extends Controller {
public async Login_Post_AllowAnonymous(apiLoginModel: ApiLoginModel) {
this.res.header("access-control-allow-origin", "*");
if (typeof(apiLoginModel.username) !== "string" || typeof(apiLoginModel.password) !== "string") {
return this.badRequest();
}
const user = await UserService.AuthenticateUser(apiLoginModel.username, apiLoginModel.password);
if (user) {
return this.ok(user.APIKey);
}
return this.unauthorised("Username or Password incorrect");
}
public async Version_Post_AllowAnonymous() {
this.res.header("access-control-allow-origin", "*");
if (Date.now() < cacheExpiry) {
this.ok(cachedVersion);
} else {
const response = await fetch(`http://${Config.githost}/tgpholly/t00-multiuser/raw/branch/master/client/Terminal-00-Multiuser.user.js?${Date.now()}`);
if (response.status === 200) {
const content = await response.text();
if (content.includes("@version")) {
cachedVersion = content.split("@version")[1].split("\n")[0].trim().split(".").join("");
cacheExpiry = Date.now() + 30000;
return this.ok(cachedVersion);
} else {
return this.ok("0");
}
} else {
return this.ok("0");
}
}
}
}

View File

@ -0,0 +1,83 @@
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { Console } from "hsconsole";
import Session from "../objects/Session";
import SessionUser from "../objects/SessionUser";
import RequestCtx from "../objects/RequestCtx";
// prepare for ts-ignore :3
// TODO: figure out some runtime field / type checking so
// can auto badRequest on missing stuff.
export default abstract class Controller {
public static FastifyInstance:FastifyInstance;
public constructor() {
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
const controllerName = this.constructor.name.replace("Controller", "").toLowerCase();
for (const method of methods) {
if (method === "constructor" || method[0] !== method[0].toUpperCase()) { // * Anything that starts with lowercase we'll consider "private"
continue;
}
const params = method.split("_");
const methodNameRaw = params.splice(0, 1)[0]
const methodName = methodNameRaw.toLowerCase();
const doAuth = !params.includes("AllowAnonymous");
// @ts-ignore
const controllerRequestHandler = this[method];
const requestHandler = (req:FastifyRequest, res:FastifyReply) => {
let session = Session.CheckValiditiy(req.cookies);
if (doAuth && session === undefined) {
return res.redirect(`/account/login?returnTo=${encodeURIComponent(req.url)}`);
}
const requestCtx = new RequestCtx(req, res, controllerName, methodName, session);
controllerRequestHandler.bind(requestCtx)(req.method === "GET" ? req.query : req.body);
}
let funcMethods:Array<string> = [];
for (const param of params) {
if (param === "Get" || param === "Post" || param === "Put") {
funcMethods.push(param);
// @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}`);
if (methodName === "index") {
// @ts-ignore
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}/${methodName}`, requestHandler);
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}/${methodName}" as ${param}`);
// @ts-ignore
Controller.FastifyInstance[param.toLowerCase()](`/${controllerName}`, requestHandler);
Console.printInfo(`Registered ${this.constructor.name}.${method} to "/${controllerName}" as ${param}`);
}
}
}
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);
}
}
}
}
// not real, these RequestCtx so they autocomplete :)
// yeah, i know. this is terrible.
// Fields
// @ts-ignore
public session:SessionUser;
// @ts-ignore
public req: FastifyRequest;
// @ts-ignore
public res: FastifyReply;
// Methods
view(view?:string | Object, model?: Object) {}
redirectToAction(action:string, controller?:string) {}
ok(message?:string) {}
badRequest(message?:string) {}
unauthorised(message?:string) {}
}

View File

@ -0,0 +1,28 @@
import { FastifyReply, FastifyRequest } from "fastify";
import Controller from "./Controller";
import UserService from "../services/UserService";
import HomeViewModel from "../models/home/HomeViewModel";
export default class HomeController extends Controller {
public async Index_Get_AllowAnonymous() {
if (this.session) {
const user = await UserService.GetUser(this.session.userId);
if (!user) {
return this.unauthorised();
}
const parties = await UserService.GetUserParties(this.session.userId);
const activeUserParty = await UserService.GetActiveParty(this.session.userId);
const homeViewModel: HomeViewModel = {
user,
parties,
activeUserParty
};
return this.view("home", homeViewModel);
}
return this.view();
}
}

View File

@ -0,0 +1,82 @@
import CreateEditPartyViewModel from "../models/party/CreateEditPartyViewModel";
import JoinPartyViewModel from "../models/party/JoinPartyViewModel";
import LeavePartyModel from "../models/party/LeavePartyModel";
import SetActivePartyModel from "../models/party/SetActivePartyModel";
import UserService from "../services/UserService";
import Controller from "./Controller";
export default class PartyController extends Controller {
public async Create_Get() {
return this.view("createEdit");
}
public async Create_Post(createEditPartyViewModel: CreateEditPartyViewModel) {
if (typeof(createEditPartyViewModel.name) !== "string" || typeof(createEditPartyViewModel.partyRef) !== "string") {
return this.badRequest();
}
const party = await UserService.GetPartyByPartyRef(createEditPartyViewModel.partyRef);
if (party) {
createEditPartyViewModel.message = "That Party ID is already taken!";
return this.view("createEdit", createEditPartyViewModel);
}
await UserService.CreateParty(this.session.userId, createEditPartyViewModel.name, createEditPartyViewModel.partyRef);
return this.redirectToAction("index", "home");
}
public async Join_Get() {
return this.view();
}
public async Join_Post(joinPartyViewModel: JoinPartyViewModel) {
if (typeof(joinPartyViewModel.partyRef) !== "string") {
return this.badRequest();
}
const party = await UserService.GetPartyByPartyRef(joinPartyViewModel.partyRef);
if (!party) {
joinPartyViewModel.message = "That Join Code / Party ID is invalid.";
return this.view(joinPartyViewModel);
}
const userPartyExisting = await UserService.GetUserPartyForUser(this.session.userId, party.Id);
if (userPartyExisting) {
joinPartyViewModel.message = "You are already in this party.";
return this.view(joinPartyViewModel);
}
await UserService.AddUserToParty(this.session.userId, party.Id);
return this.redirectToAction("index", "home");
}
public async Leave_Get(leavePartyModel: LeavePartyModel) {
const partyId = parseInt(leavePartyModel.id ?? "-1");
if (typeof(leavePartyModel.id) !== "string" || isNaN(partyId)) {
return this.badRequest();
}
await UserService.LeaveParty(this.session.userId, partyId);
return this.redirectToAction("index", "home");
}
public async SetActive_Get(setActivePartyModel: SetActivePartyModel) {
const partyId = parseInt(setActivePartyModel.id ?? "-1");
if (typeof(setActivePartyModel.id) !== "string" || isNaN(partyId)) {
return this.badRequest();
}
await UserService.SetActiveParty(this.session.userId, partyId);
return this.redirectToAction("index", "home");
}
public async Deactivate_Get() {
await UserService.DeactivateCurrentParty(this.session.userId);
return this.redirectToAction("index", "home");
}
}

View File

@ -12,17 +12,15 @@ import { MessageType } from "./enums/MessageType";
import Database from "./objects/Database"; import Database from "./objects/Database";
import { Console } from "hsconsole"; import { Console } from "hsconsole";
import UserService from "./services/UserService"; import UserService from "./services/UserService";
import UsernameData from "./interfaces/UsernameData"; import Party from "./entities/Party";
import { randomBytes } from "crypto";
import SessionUser from "./objects/SessionUser";
import PasswordUtility from "./utilities/PasswordUtility";
import CreateEditPartyData from "./interfaces/CreateEditPartyData";
import JoinPartyData from "./interfaces/JoinPartyData";
import IdData from "./interfaces/IdData";
import Party from "./objects/Party";
import SimpleProm from "simple-prom"; import SimpleProm from "simple-prom";
import Gauge from "simple-prom/lib/objects/Gauge"; import Gauge from "simple-prom/lib/objects/Gauge";
import Counter from "simple-prom/lib/objects/Counter"; import Counter from "simple-prom/lib/objects/Counter";
import Controller from "./controller/Controller";
import HomeController from "./controller/HomeController";
import AccountController from "./controller/AccountController";
import PartyController from "./controller/PartyController";
import ApiController from "./controller/ApiController";
Console.customHeader(`MultiProbe server started at ${new Date()}`); Console.customHeader(`MultiProbe server started at ${new Date()}`);
@ -52,30 +50,12 @@ dataOut.setHelpText("Data sent by the server in bytes");
// Web stuff // Web stuff
const sessions = new FunkyArray<string, SessionUser>();
const sessionExpiryInterval = setInterval(() => {
const currentTime = Date.now();
for (const key of sessions.keys) {
const session = sessions.get(key);
if (!session || (session && currentTime >= session.validityPeriod.getTime())) {
sessions.remove(key);
}
}
webSessions.Value = sessions.length;
}, 3600000);
const fastify = Fastify({ const fastify = Fastify({
logger: false logger: false
}); });
fastify.register(FastifyView, { fastify.register(FastifyView, { engine: { ejs: EJS } });
engine: {
ejs: EJS
}
});
fastify.register(FastifyFormBody); fastify.register(FastifyFormBody);
fastify.register(FastifyCookie, { fastify.register(FastifyCookie, {
secret: Config.session.secret, secret: Config.session.secret,
parseOptions: { parseOptions: {
@ -88,260 +68,11 @@ fastify.setNotFoundHandler(async (req, res) => {
return res.status(404).view("templates/404.ejs", { }); return res.status(404).view("templates/404.ejs", { });
}); });
function validateSession(cookies:{ [cookieName: string]: string | undefined }) { Controller.FastifyInstance = fastify;
if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") { new HomeController();
const key = FastifyCookie.unsign(cookies["MP_SESSION"], Config.session.secret); new AccountController();
if (key.valid && sessions.has(key.value ?? "badkey")) { new PartyController();
return sessions.get(key.value ?? "badkey"); new ApiController();
}
}
return undefined;
}
// Get Methods
fastify.get("/", async (req, res) => {
let session:SessionUser | undefined;
if (session = validateSession(req.cookies)) {
const user = await UserService.GetUser(session.userId);
if (user) {
const parties = await UserService.GetUserParties(session.userId);
const activeUserParty = await UserService.GetActiveParty(session.userId);
return res.view("templates/home.ejs", { user, parties, activeUserParty });
}
return res.view("templates/index.ejs", { });
}
return res.view("templates/index.ejs", { });
});
fastify.get("/account", async (req, res) => {
return res.redirect(302, "/");
});
fastify.get("/account/login", async (req, res) => {
return res.view("templates/account/login.ejs", { });
});
fastify.get("/account/register", async (req, res) => {
return res.view("templates/account/register.ejs", { });
});
fastify.get("/account/logout", async (req, res) => {
res.clearCookie("MP_SESSION");
return res.redirect(302, "/");
});
fastify.get("/party/create", async (req, res) => {
let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/");
}
return res.view("templates/party/createedit.ejs", { session });
});
fastify.get("/party/join", async (req, res) => {
let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/");
}
return res.view("templates/party/join.ejs", { session });
});
fastify.get("/party/setactive", async (req, res) => {
let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/");
}
const data = req.query as IdData;
const numericId = parseInt(data.id ?? "-1");
if (typeof(data.id) !== "string" || isNaN(numericId)) {
return res.redirect(302, "/");
}
await UserService.SetActiveParty(session.userId, numericId);
return res.redirect(302, "/");
});
fastify.get("/party/deactivate", async (req, res) => {
let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/");
}
await UserService.DeactivateCurrentParty(session.userId);
return res.redirect(302, "/");
});
// Post Methods
fastify.post("/account/register", async (req, res) => {
const data = req.body as UsernameData;
if (typeof(data.username) !== "string" || typeof(data.password) !== "string" || data.username.length > 32 || data.password.length < 8) {
return res.view("templates/account/register.ejs", { });
}
const username = data.username.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
await UserService.CreateUser(0, username, data.password);
const user = await UserService.GetUserByUsername(username);
if (!user) {
return res.view("templates/account/register.ejs", { });
}
const validPeriod = new Date();
validPeriod.setTime(validPeriod.getTime() + Config.session.validity);
const key = randomBytes(Config.session.length).toString("hex");
sessions.set(key, new SessionUser(user.Id, validPeriod));
webSessions.Value = sessions.length;
res.setCookie("MP_SESSION", key, {
path: "/",
signed: true
});
return res.redirect(302, "/");
});
fastify.post("/account/login", async (req, res) => {
const data = req.body as UsernameData;
if (typeof(data.username) !== "string" || typeof(data.password) !== "string" || data.username.length > 32 || data.password.length < 8) {
return res.view("templates/account/login.ejs", { });
}
const user = await UserService.GetUserByUsername(data.username);
if (!user) {
return res.view("templates/account/login.ejs", { });
}
if (await PasswordUtility.ValidatePassword(user.PasswordHash, user.PasswordSalt, data.password)) {
const validPeriod = new Date();
validPeriod.setTime(validPeriod.getTime() + Config.session.validity);
const key = randomBytes(Config.session.length).toString("hex");
sessions.set(key, new SessionUser(user.Id, validPeriod));
webSessions.Value = sessions.length;
res.setCookie("MP_SESSION", key, {
path: "/",
signed: true
});
return res.redirect(302, "/");
}
return res.view("templates/account/login.ejs", { });
});
fastify.post("/party/create", async (req, res) => {
try {
let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/");
}
const data = req.body as CreateEditPartyData;
if (typeof(data.partyName) !== "string" || typeof(data.partyRef) !== "string" || data.partyName.length === 0 || data.partyRef.length === 0) {
return res.view("templates/party/createedit.ejs", { session, partyName: data.partyName ?? "", partyRef: data.partyRef ?? "" });
}
const party = await UserService.GetPartyByPartyRef(data.partyRef)
if (party != null) {
return res.view("templates/party/createedit.ejs", { session, partyName: data.partyName ?? "", partyRef: data.partyRef ?? "", error: "A group with that Party ID already exists" });
}
await UserService.CreateParty(session.userId, data.partyName, data.partyRef);
return res.redirect(302, "/");
} catch (e) {
console.error(e);
}
});
fastify.post("/party/join", async (req, res) => {
try {
let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/");
}
const data = req.body as JoinPartyData;
if (typeof(data.partyRef) !== "string" || data.partyRef.length === 0) {
return res.view("templates/party/join.ejs", { partyRef: data.partyRef ?? "" });
}
const party = await UserService.GetPartyByPartyRef(data.partyRef);
if (party == null) {
return res.view("templates/party/join.ejs", { session, partyRef: data.partyRef ?? "", error: "That Join Code / Party ID is invalid." });
}
const userPartyExisting = await UserService.GetUserPartyForUser(session.userId, party.Id);
if (userPartyExisting != null) {
return res.view("templates/party/join.ejs", { session, partyRef: data.partyRef ?? "", error: "You are already in this group." });
}
await UserService.AddUserToParty(session.userId, party.Id);
return res.redirect(302, "/");
} catch (e) {
console.error(e);
}
});
// API
fastify.post("/api/login", async (req, res) => {
res.header("access-control-allow-origin", "*");
const data = req.body as UsernameData;
if (typeof(data.username) !== "string" || typeof(data.password) !== "string" || data.username.length > 32 || data.password.length < 8) {
return res.status(401).send("Username or Password incorrect");
}
const user = await UserService.GetUserByUsername(data.username);
if (!user) {
return res.status(401).send("Username or Password incorrect");
}
if (await PasswordUtility.ValidatePassword(user.PasswordHash, user.PasswordSalt, data.password)) {
return res.status(200).send(user.APIKey);
}
return res.status(401).send("Username or Password incorrect");
});
let cachedVersion = "";
let cacheExpiry = 0;
fastify.post("/api/version", async (req, res) => {
res.header("access-control-allow-origin", "*");
if (Date.now() < cacheExpiry) {
res.send(cachedVersion);
} else {
const response = await fetch(`http://${Config.githost}/tgpholly/t00-multiuser/raw/branch/master/client/Terminal-00-Multiuser.user.js?${Date.now()}`);
if (response.status === 200) {
const content = await response.text();
if (content.includes("@version")) {
cachedVersion = content.split("@version")[1].split("\n")[0].trim().split(".").join("");
cacheExpiry = Date.now() + 30000;
return res.send(cachedVersion);
} else {
return res.send("0");
}
} else {
return res.send("0");
}
}
});
// Websocket stuff // Websocket stuff
@ -580,7 +311,6 @@ function shutdown() {
Console.printInfo("Shutting down..."); Console.printInfo("Shutting down...");
websocketServer.close(async () => { websocketServer.close(async () => {
await fastify.close(); await fastify.close();
clearInterval(sessionExpiryInterval);
clearInterval(afkInterval); clearInterval(afkInterval);
Console.cleanup(); Console.cleanup();

View File

@ -1,4 +0,0 @@
export default interface CreateEditPartyData {
partyName?:string;
partyRef?:string;
}

View File

@ -1,3 +0,0 @@
export default interface IdData {
id?: string
}

View File

@ -1,3 +0,0 @@
export default interface JoinPartyData {
partyRef: string
}

View File

@ -1,4 +0,0 @@
export default interface UsernameData {
username?: string,
password?: string
}

View File

@ -0,0 +1,5 @@
export default interface LoginViewModel {
message?: string,
username: string,
password: string
}

View File

@ -0,0 +1,5 @@
export default interface RegisterViewModel {
message?: string,
username: string,
password: string
}

View File

@ -0,0 +1,4 @@
export default interface ApiLoginModel {
username: string,
password: string
}

View File

@ -0,0 +1,9 @@
import Party from "../../entities/Party";
import User from "../../entities/User";
import UserParty from "../../entities/UserParty";
export default interface HomeViewModel {
user: User,
parties: Array<Party>,
activeUserParty: UserParty | null
}

View File

@ -0,0 +1,7 @@
export default interface CreateEditPartyViewModel {
message?:string
id?: string
name: string,
partyRef: string,
}

View File

@ -0,0 +1,4 @@
export default interface JoinPartyViewModel {
message?: string
partyRef: string
}

View File

@ -0,0 +1,3 @@
export default interface LeavePartyModel {
id: string
}

View File

@ -0,0 +1,3 @@
export default interface SetActivePartyModel {
id: string
}

View File

@ -0,0 +1,60 @@
import { FastifyReply, FastifyRequest } from "fastify";
import SessionUser from "./SessionUser";
export default class RequestCtx {
public controllerName:string;
public actionName:string;
public session?:SessionUser;
public req: FastifyRequest;
public res: FastifyReply;
public constructor(req: FastifyRequest, res: FastifyReply, controllerName:string, actionName:string, sessionUser?:SessionUser) {
this.session = sessionUser;
this.req = req;
this.res = res;
this.controllerName = controllerName;
this.actionName = actionName;
}
view(view?:string | Object, model?: Object) {
let viewName: string = this.actionName;
let viewModel: Object = {};
if (typeof(view) === "string") {
viewName = view;
} else if (typeof(view) === "object") {
viewModel = view;
}
if (typeof(model) === "object") {
viewModel = model;
}
// @ts-ignore inject session
viewModel["session"] = this.session;
return this.res.view(`templates/${this.controllerName}/${viewName}.ejs`, viewModel);
}
// TODO: query params
redirectToAction(action:string, controller?:string) {
const controllerName = controller ?? this.controllerName;
if (action === "index") {
if (controllerName === "home") {
return this.res.redirect(302, `/`);
} else {
return this.res.redirect(302, `/${controllerName}`);
}
} else {
return this.res.redirect(302, `/${controllerName}/${action}`);
}
}
ok(message?:string) {
return this.res.status(200).send(message ?? "");
}
badRequest(message?:string) {
return this.res.status(400).send(message ?? "");
}
unauthorised(message?:string) {
return this.res.status(401).send(message ?? "");
}
}

55
server/objects/Session.ts Normal file
View File

@ -0,0 +1,55 @@
import Config from "./Config";
import FastifyCookie from "@fastify/cookie";
import FunkyArray from "funky-array";
import SessionUser from "./SessionUser";
import { FastifyReply, FastifyRequest } from "fastify";
import User from "../entities/User";
import { randomBytes } from "crypto";
type Cookies = { [cookieName: string]: string | undefined }
export default abstract class Session {
public static Sessions = new FunkyArray<string, SessionUser>();
public static SessionExpiryInterval = setInterval(() => {
const currentTime = Date.now();
for (const key of Session.Sessions.keys) {
const session = Session.Sessions.get(key);
if (!session || (session && currentTime >= session.validityPeriod.getTime())) {
Session.Sessions.remove(key);
}
}
}, 3600000);
public static AssignUserSession(res:FastifyReply, user:User) {
const validPeriod = new Date();
validPeriod.setTime(validPeriod.getTime() + Config.session.validity);
const key = randomBytes(Config.session.length).toString("hex");
Session.Sessions.set(key, new SessionUser(user.Id, validPeriod));
res.setCookie("MP_SESSION", key, {
path: "/",
signed: true
});
}
public static Clear(cookies:Cookies, res:FastifyReply) {
if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") {
const key:unknown = FastifyCookie.unsign(cookies["MP_SESSION"], Config.session.secret);
Session.Sessions.remove(key as string);
res.clearCookie("MP_SESSION");
}
}
public static CheckValiditiy(cookies:Cookies) {
if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") {
const key = FastifyCookie.unsign(cookies["MP_SESSION"], Config.session.secret);
if (key.valid && Session.Sessions.has(key.value ?? "badkey")) {
return Session.Sessions.get(key.value ?? "badkey");
}
}
return undefined;
}
}

View File

@ -1,6 +1,6 @@
import Database from "../objects/Database"; import Database from "../objects/Database";
import Party from "../objects/Party"; import Party from "../entities/Party";
import User from "../objects/User"; import User from "../entities/User";
import RepoBase from "./RepoBase"; import RepoBase from "./RepoBase";
export default class PartyRepo { export default class PartyRepo {
@ -65,5 +65,5 @@ function populatePartyFromDB(party:Party, dbParty:any) {
party.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.LastModifiedDatetime); party.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.LastModifiedDatetime);
party.DeletedByUserId = dbParty.DeletedByUserId; party.DeletedByUserId = dbParty.DeletedByUserId;
party.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.DeletedDatetime); party.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.DeletedDatetime);
party.IsDeleted = dbParty.IsDeleted; party.IsDeleted = dbParty.IsDeleted === 1;
} }

View File

@ -1,5 +1,5 @@
import Database from "../objects/Database"; import Database from "../objects/Database";
import UserParty from "../objects/UserParty"; import UserParty from "../entities/UserParty";
import RepoBase from "./RepoBase"; import RepoBase from "./RepoBase";
export default class UserPartyRepo { export default class UserPartyRepo {
@ -82,5 +82,5 @@ function populateUserPartyFromDB(userParty:UserParty, dbUserParty:any) {
userParty.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.LastModifiedDatetime); userParty.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.LastModifiedDatetime);
userParty.DeletedByUserId = dbUserParty.DeletedByUserId; userParty.DeletedByUserId = dbUserParty.DeletedByUserId;
userParty.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.DeletedDatetime); userParty.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.DeletedDatetime);
userParty.IsDeleted = dbUserParty.IsDeleted; userParty.IsDeleted = dbUserParty.IsDeleted === 1;
} }

View File

@ -1,5 +1,5 @@
import Database from "../objects/Database"; import Database from "../objects/Database";
import User from "../objects/User"; import User from "../entities/User";
import RepoBase from "./RepoBase"; import RepoBase from "./RepoBase";
export default class UserRepo { export default class UserRepo {
@ -61,5 +61,5 @@ function populateUserFromDB(user:User, dbUser:any) {
user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime); user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
user.DeletedByUserId = dbUser.DeletedByUserId; user.DeletedByUserId = dbUser.DeletedByUserId;
user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime); user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
user.IsDeleted = dbUser.IsDeleted; user.IsDeleted = dbUser.IsDeleted === 1;
} }

View File

@ -1,13 +1,31 @@
import { Console } from "hsconsole"; import { Console } from "hsconsole";
import User from "../objects/User"; import User from "../entities/User";
import PartyRepo from "../repos/PartyRepo"; import PartyRepo from "../repos/PartyRepo";
import UserRepo from "../repos/UserRepo"; import UserRepo from "../repos/UserRepo";
import PasswordUtility from "../utilities/PasswordUtility"; import PasswordUtility from "../utilities/PasswordUtility";
import Party from "../objects/Party"; import Party from "../entities/Party";
import UserParty from "../objects/UserParty"; import UserParty from "../entities/UserParty";
import UserPartyRepo from "../repos/UserPartyRepo"; import UserPartyRepo from "../repos/UserPartyRepo";
export default class UserService { export default 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(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async GetUser(id:number) { public static async GetUser(id:number) {
try { try {
return await UserRepo.selectById(id); return await UserRepo.selectById(id);
@ -169,4 +187,24 @@ export default class UserService {
throw e; throw e;
} }
} }
public static async LeaveParty(currentUserId:number, partyId:number) {
try {
const userParty = await UserPartyRepo.selectByUserIdPartyId(currentUserId, partyId);
if (!userParty) {
return null;
}
userParty.DeletedByUserId = currentUserId;
userParty.DeletedDatetime = new Date();
userParty.IsDeleted = true;
await UserPartyRepo.insertUpdate(userParty);
return userParty;
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
} }

View File

@ -1,27 +1,27 @@
<%- include("../base/header", { title: "Login" }) %> <%- include("../base/header", { title: "Login" }) %>
<h1 class="text-center mb-5">Login</h1>
<form method="post"> <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">MultiProbe 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="row">
<div class="col-4"></div> <div class="col d-flex justify-content-center">
<div class="col-4"> <a class="align-self-center" href="/account/register">I don't have an account.</a>
<div class="form-group">
<label class="form-label">Username</label>
<input class="form-control" type="text" name="username" maxlength="32" required />
</div> </div>
<div class="form-group mt-3"> <div class="col-auto me-3">
<label class="form-label">Password</label> <input class="btn btn-primary mx-auto d-block" type="submit" value="Login" />
<input class="form-control" type="password" name="password" required />
</div> </div>
</div>
</form>
</div>
</div>
</div>
<div class="text-center mt-3">
<a href="/account/register">I don't have an account.</a>
</div>
<div class="text-center mt-3">
<input class="btn btn-primary" type="submit" value="Continue" />
</div>
</div>
<div class="col-4"></div>
</div>
</form>
<%- include("../base/footer") %> <%- include("../base/footer") %>

View File

@ -1,27 +1,27 @@
<%- include("../base/header", { title: "Register" }) %> <%- include("../base/header", { title: "Register" }) %>
<h1 class="text-center mb-5">Register</h1>
<form method="post"> <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">MultiProbe 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 %>" >
<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 mb-3" name="password" type="password" placeholder="Password" required autocomplete="new-password" />
<div class="row"> <div class="row">
<div class="col-4"></div> <div class="col d-flex justify-content-center">
<div class="col-4"> <a class="align-self-center" href="/account/login">I already have an account!</a>
<div class="form-group">
<label class="form-label">Username</label>
<input class="form-control" type="text" name="username" maxlength="32" required />
</div> </div>
<div class="form-group mt-3"> <div class="col-auto me-3">
<label class="form-label">Password</label> <input class="btn btn-primary mx-auto d-block" type="submit" value="Register" />
<input class="form-control" type="password" name="password" minlength="8" autocomplete="new-password" required />
</div> </div>
</div>
</form>
</div>
</div>
</div>
<div class="text-center mt-3">
<a href="/account/login">I already have an account!</a>
</div>
<div class="text-center mt-3">
<input class="btn btn-primary" type="submit" value="Continue" />
</div>
</div>
<div class="col-4"></div>
</div>
</form>
<%- include("../base/footer") %> <%- include("../base/footer") %>

View File

@ -1,4 +1,4 @@
<%- include("base/header", { title: "Home", userId: user.Id }) %> <%- include("../base/header", { title: "Home", userId: session.userId }) %>
<div class="row"> <div class="row">
<div class="col text-center pb-5 lg-sm-0"> <div class="col text-center pb-5 lg-sm-0">
<h1>Welcome back <%= user.Username %>!</h1> <h1>Welcome back <%= user.Username %>!</h1>
@ -51,4 +51,4 @@
<% } %> <% } %>
</div> </div>
</div> </div>
<%- include("base/footer") %> <%- include("../base/footer") %>

View File

@ -1,8 +1,8 @@
<%- include("base/header", { title: "Home" }) %> <%- include("../base/header", { title: "Home" }) %>
<h1><b>MultiProbe</b></h1> <h1><b>MultiProbe</b></h1>
<h3>A way to explore <a href="https://angusnicneven.com">Terminal 00</a> with friends.</h3> <h3>A way to explore <a href="https://angusnicneven.com">Terminal 00</a> with friends.</h3>
<div class="mt-3"> <div class="mt-3">
<a type="button" class="btn btn-primary btn-lg me-2" href="/account/register">Register</a> <a type="button" class="btn btn-primary btn-lg me-2" href="/account/register">Register</a>
<a type="button" class="btn btn-secondary btn-lg" href="/account/login">Login</a> <a type="button" class="btn btn-secondary btn-lg" href="/account/login">Login</a>
</div> </div>
<%- include("base/footer") %> <%- include("../base/footer") %>

View File

@ -0,0 +1,32 @@
<%- include("../base/header", { title: typeof(party) === "undefined" ? "Create Party" : `Editing ${name}`, userId: session.userId }) %>
<div class="d-flex justify-content-center">
<div class="card my-auto" style="width: 25rem;">
<div class="card-body">
<% if (typeof(party) === "undefined") { %>
<h4 class="card-title text-center">Create New Party</h5>
<% } else { %>
<h4 class="card-title text-center">Editing <%= name %></h5>
<% } %>
<% if (typeof(message) === "string") { %>
<div class="alert alert-danger text-center" role="alert"><%= message %></div>
<% } %>
<form class="mt-3" method="post">
<div class="form-group">
<label for="name" class="form-label">Party Name</label>
<input class="form-control" type="text" name="name" maxlength="64" value="<%= typeof(name) === "undefined" ? "" : name %>" required />
</div>
<div class="form-group mt-3">
<label for="partyRef" class="form-label">Join Code / Party ID<br><span style="font-size: 10pt;">Pick something nice, e.g. "<b>3EGGS</b>"</span></label>
<input class="form-control" type="text" name="partyRef" minlength="5" maxlength="5" value="<%= typeof(partyRef) === "undefined" ? "" : partyRef %>" required />
</div>
<div class="text-center mt-3">
<input class="btn btn-primary" type="submit" value="Save" />
<a class="btn btn-danger ms-2" href="/">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<%- include("../base/footer") %>

View File

@ -1,34 +0,0 @@
<%- include("../base/header", { title: typeof(party) === "undefined" ? "Create Party" : `Editing ${party.Name}`, userId: session.userId }) %>
<% if (typeof(party) === "undefined") { %>
<h1 class="text-center mb-5">Create Party</h1>
<% } else { %>
<h1 class="text-center mb-5">Editing <%= party.Name %></h1>
<% } %>
<form method="post">
<div class="row">
<div class="col-4"></div>
<div class="col-4">
<% if (typeof(error) === "string") { %>
<div class="alert alert-danger">
<%= error %>
</div>
<% } %>
<div class="form-group">
<label class="form-label">Party Name</label>
<input class="form-control" type="text" name="partyName" maxlength="64" value="<%= typeof(partyName) === "undefined" ? "" : partyName %>" required />
</div>
<div class="form-group mt-3">
<label class="form-label">Join Code / Party ID<br><span style="font-size: 10pt;">Pick something nice, e.g. "<b>3EGGS</b>"</span></label>
<input class="form-control" type="text" name="partyRef" minlength="5" maxlength="5" value="<%= typeof(partyRef) === "undefined" ? "" : partyRef %>" required />
</div>
<div class="text-center mt-5">
<input class="btn btn-primary" type="submit" value="Save" />
<a class="btn btn-danger ms-2" href="/">Cancel</a>
</div>
</div>
<div class="col-4"></div>
</div>
</form>
<%- include("../base/footer") %>

View File

@ -1,26 +1,24 @@
<%- include("../base/header", { title: "Join Party", userId: session.userId }) %> <%- include("../base/header", { title: "Join Party", userId: session.userId }) %>
<h1 class="text-center mb-5">Join Party</h1> <div class="d-flex justify-content-center">
<form method="post"> <div class="card my-auto" style="width: 25rem;">
<div class="row"> <div class="card-body">
<div class="col-4"></div> <h4 class="card-title text-center">Join Party</h5>
<div class="col-4"> <% if (typeof(message) === "string") { %>
<% if (typeof(error) === "string") { %> <div class="alert alert-danger text-center" role="alert"><%= message %></div>
<div class="alert alert-danger">
<%= error %>
</div>
<% } %> <% } %>
<form method="post">
<div class="form-group mt-3"> <div class="form-group mt-3">
<label class="form-label">Join Code / Party ID <span style="font-size: 10pt;">e.g. "<b>3EGGS</b>"</span></label> <label class="form-label">Join Code / Party ID <span style="font-size: 10pt;">e.g. "<b>3EGGS</b>"</span></label>
<input class="form-control" type="text" name="partyRef" minlength="5" maxlength="5" value="<%= typeof(partyRef) === "undefined" ? "" : partyRef %>" required /> <input class="form-control" type="text" name="partyRef" minlength="5" maxlength="5" value="<%= typeof(partyRef) === "undefined" ? "" : partyRef %>" required />
</div> </div>
<div class="text-center mt-5"> <div class="text-center mt-3">
<input class="btn btn-primary" type="submit" value="Join" /> <input class="btn btn-primary" type="submit" value="Join" />
<a class="btn btn-danger ms-2" href="/">Cancel</a> <a class="btn btn-danger ms-2" href="/">Cancel</a>
</div> </div>
</form>
</div> </div>
<div class="col-4"></div>
</div> </div>
</form> </div>
<%- include("../base/footer") %> <%- include("../base/footer") %>