mvc the whole web side of multiprobe
This commit is contained in:
parent
2dab36d2ec
commit
fecd4ab892
34 changed files with 614 additions and 409 deletions
60
server/controller/AccountController.ts
Normal file
60
server/controller/AccountController.ts
Normal 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("<", "<").replaceAll(">", ">");
|
||||
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");
|
||||
}
|
||||
}
|
47
server/controller/ApiController.ts
Normal file
47
server/controller/ApiController.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
83
server/controller/Controller.ts
Normal file
83
server/controller/Controller.ts
Normal 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) {}
|
||||
}
|
28
server/controller/HomeController.ts
Normal file
28
server/controller/HomeController.ts
Normal 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();
|
||||
}
|
||||
}
|
82
server/controller/PartyController.ts
Normal file
82
server/controller/PartyController.ts
Normal 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");
|
||||
}
|
||||
}
|
294
server/index.ts
294
server/index.ts
|
@ -12,17 +12,15 @@ import { MessageType } from "./enums/MessageType";
|
|||
import Database from "./objects/Database";
|
||||
import { Console } from "hsconsole";
|
||||
import UserService from "./services/UserService";
|
||||
import UsernameData from "./interfaces/UsernameData";
|
||||
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 Party from "./entities/Party";
|
||||
import SimpleProm from "simple-prom";
|
||||
import Gauge from "simple-prom/lib/objects/Gauge";
|
||||
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()}`);
|
||||
|
||||
|
@ -52,30 +50,12 @@ dataOut.setHelpText("Data sent by the server in bytes");
|
|||
|
||||
// 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({
|
||||
logger: false
|
||||
});
|
||||
|
||||
fastify.register(FastifyView, {
|
||||
engine: {
|
||||
ejs: EJS
|
||||
}
|
||||
});
|
||||
|
||||
fastify.register(FastifyView, { engine: { ejs: EJS } });
|
||||
fastify.register(FastifyFormBody);
|
||||
|
||||
fastify.register(FastifyCookie, {
|
||||
secret: Config.session.secret,
|
||||
parseOptions: {
|
||||
|
@ -88,260 +68,11 @@ fastify.setNotFoundHandler(async (req, res) => {
|
|||
return res.status(404).view("templates/404.ejs", { });
|
||||
});
|
||||
|
||||
function validateSession(cookies:{ [cookieName: string]: string | undefined }) {
|
||||
if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") {
|
||||
const key = FastifyCookie.unsign(cookies["MP_SESSION"], Config.session.secret);
|
||||
if (key.valid && sessions.has(key.value ?? "badkey")) {
|
||||
return sessions.get(key.value ?? "badkey");
|
||||
}
|
||||
}
|
||||
|
||||
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("<", "<").replaceAll(">", ">");
|
||||
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");
|
||||
}
|
||||
}
|
||||
});
|
||||
Controller.FastifyInstance = fastify;
|
||||
new HomeController();
|
||||
new AccountController();
|
||||
new PartyController();
|
||||
new ApiController();
|
||||
|
||||
// Websocket stuff
|
||||
|
||||
|
@ -580,7 +311,6 @@ function shutdown() {
|
|||
Console.printInfo("Shutting down...");
|
||||
websocketServer.close(async () => {
|
||||
await fastify.close();
|
||||
clearInterval(sessionExpiryInterval);
|
||||
clearInterval(afkInterval);
|
||||
|
||||
Console.cleanup();
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export default interface CreateEditPartyData {
|
||||
partyName?:string;
|
||||
partyRef?:string;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default interface IdData {
|
||||
id?: string
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export default interface JoinPartyData {
|
||||
partyRef: string
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export default interface UsernameData {
|
||||
username?: string,
|
||||
password?: string
|
||||
}
|
5
server/models/account/LoginViewModel.ts
Normal file
5
server/models/account/LoginViewModel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default interface LoginViewModel {
|
||||
message?: string,
|
||||
username: string,
|
||||
password: string
|
||||
}
|
5
server/models/account/RegisterViewModel.ts
Normal file
5
server/models/account/RegisterViewModel.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default interface RegisterViewModel {
|
||||
message?: string,
|
||||
username: string,
|
||||
password: string
|
||||
}
|
4
server/models/api/ApiLoginModel.ts
Normal file
4
server/models/api/ApiLoginModel.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default interface ApiLoginModel {
|
||||
username: string,
|
||||
password: string
|
||||
}
|
9
server/models/home/HomeViewModel.ts
Normal file
9
server/models/home/HomeViewModel.ts
Normal 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
|
||||
}
|
7
server/models/party/CreateEditPartyViewModel.ts
Normal file
7
server/models/party/CreateEditPartyViewModel.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default interface CreateEditPartyViewModel {
|
||||
message?:string
|
||||
|
||||
id?: string
|
||||
name: string,
|
||||
partyRef: string,
|
||||
}
|
4
server/models/party/JoinPartyViewModel.ts
Normal file
4
server/models/party/JoinPartyViewModel.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default interface JoinPartyViewModel {
|
||||
message?: string
|
||||
partyRef: string
|
||||
}
|
3
server/models/party/LeavePartyModel.ts
Normal file
3
server/models/party/LeavePartyModel.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default interface LeavePartyModel {
|
||||
id: string
|
||||
}
|
3
server/models/party/SetActivePartyModel.ts
Normal file
3
server/models/party/SetActivePartyModel.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default interface SetActivePartyModel {
|
||||
id: string
|
||||
}
|
60
server/objects/RequestCtx.ts
Normal file
60
server/objects/RequestCtx.ts
Normal 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
55
server/objects/Session.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Database from "../objects/Database";
|
||||
import Party from "../objects/Party";
|
||||
import User from "../objects/User";
|
||||
import Party from "../entities/Party";
|
||||
import User from "../entities/User";
|
||||
import RepoBase from "./RepoBase";
|
||||
|
||||
export default class PartyRepo {
|
||||
|
@ -65,5 +65,5 @@ function populatePartyFromDB(party:Party, dbParty:any) {
|
|||
party.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.LastModifiedDatetime);
|
||||
party.DeletedByUserId = dbParty.DeletedByUserId;
|
||||
party.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbParty.DeletedDatetime);
|
||||
party.IsDeleted = dbParty.IsDeleted;
|
||||
party.IsDeleted = dbParty.IsDeleted === 1;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import Database from "../objects/Database";
|
||||
import UserParty from "../objects/UserParty";
|
||||
import UserParty from "../entities/UserParty";
|
||||
import RepoBase from "./RepoBase";
|
||||
|
||||
export default class UserPartyRepo {
|
||||
|
@ -82,5 +82,5 @@ function populateUserPartyFromDB(userParty:UserParty, dbUserParty:any) {
|
|||
userParty.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.LastModifiedDatetime);
|
||||
userParty.DeletedByUserId = dbUserParty.DeletedByUserId;
|
||||
userParty.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.DeletedDatetime);
|
||||
userParty.IsDeleted = dbUserParty.IsDeleted;
|
||||
userParty.IsDeleted = dbUserParty.IsDeleted === 1;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import Database from "../objects/Database";
|
||||
import User from "../objects/User";
|
||||
import User from "../entities/User";
|
||||
import RepoBase from "./RepoBase";
|
||||
|
||||
export default class UserRepo {
|
||||
|
@ -61,5 +61,5 @@ function populateUserFromDB(user:User, dbUser:any) {
|
|||
user.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
|
||||
user.DeletedByUserId = dbUser.DeletedByUserId;
|
||||
user.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
|
||||
user.IsDeleted = dbUser.IsDeleted;
|
||||
user.IsDeleted = dbUser.IsDeleted === 1;
|
||||
}
|
|
@ -1,13 +1,31 @@
|
|||
import { Console } from "hsconsole";
|
||||
import User from "../objects/User";
|
||||
import User from "../entities/User";
|
||||
import PartyRepo from "../repos/PartyRepo";
|
||||
import UserRepo from "../repos/UserRepo";
|
||||
import PasswordUtility from "../utilities/PasswordUtility";
|
||||
import Party from "../objects/Party";
|
||||
import UserParty from "../objects/UserParty";
|
||||
import Party from "../entities/Party";
|
||||
import UserParty from "../entities/UserParty";
|
||||
import UserPartyRepo from "../repos/UserPartyRepo";
|
||||
|
||||
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) {
|
||||
try {
|
||||
return await UserRepo.selectById(id);
|
||||
|
@ -169,4 +187,24 @@ export default class UserService {
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +1,27 @@
|
|||
<%- include("../base/header", { title: "Login" }) %>
|
||||
<h1 class="text-center mb-5">Login</h1>
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="col-4"></div>
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<input class="form-control" type="text" name="username" maxlength="32" required />
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input class="form-control" type="password" name="password" required />
|
||||
</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 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="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 class="col-4"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<%- include("../base/footer") %>
|
|
@ -1,27 +1,27 @@
|
|||
<%- include("../base/header", { title: "Register" }) %>
|
||||
<h1 class="text-center mb-5">Register</h1>
|
||||
<form method="post">
|
||||
<div class="row">
|
||||
<div class="col-4"></div>
|
||||
<div class="col-4">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<input class="form-control" type="text" name="username" maxlength="32" required />
|
||||
</div>
|
||||
<div class="form-group mt-3">
|
||||
<label class="form-label">Password</label>
|
||||
<input class="form-control" type="password" name="password" minlength="8" autocomplete="new-password" required />
|
||||
</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 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="col d-flex justify-content-center">
|
||||
<a class="align-self-center" href="/account/login">I already 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 class="col-4"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<%- include("../base/footer") %>
|
|
@ -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="col text-center pb-5 lg-sm-0">
|
||||
<h1>Welcome back <%= user.Username %>!</h1>
|
||||
|
@ -51,4 +51,4 @@
|
|||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<%- include("base/footer") %>
|
||||
<%- include("../base/footer") %>
|
|
@ -1,8 +1,8 @@
|
|||
<%- include("base/header", { title: "Home" }) %>
|
||||
<%- include("../base/header", { title: "Home" }) %>
|
||||
<h1><b>MultiProbe</b></h1>
|
||||
<h3>A way to explore <a href="https://angusnicneven.com">Terminal 00</a> with friends.</h3>
|
||||
<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-secondary btn-lg" href="/account/login">Login</a>
|
||||
</div>
|
||||
<%- include("base/footer") %>
|
||||
<%- include("../base/footer") %>
|
32
server/templates/party/createEdit.ejs
Normal file
32
server/templates/party/createEdit.ejs
Normal 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") %>
|
|
@ -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") %>
|
|
@ -1,26 +1,24 @@
|
|||
<%- include("../base/header", { title: "Join Party", userId: session.userId }) %>
|
||||
<h1 class="text-center mb-5">Join Party</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="d-flex justify-content-center">
|
||||
<div class="card my-auto" style="width: 25rem;">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-center">Join Party</h5>
|
||||
<% if (typeof(message) === "string") { %>
|
||||
<div class="alert alert-danger text-center" role="alert"><%= message %></div>
|
||||
<% } %>
|
||||
|
||||
<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>
|
||||
<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="Join" />
|
||||
<a class="btn btn-danger ms-2" href="/">Cancel</a>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<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>
|
||||
<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="Join" />
|
||||
<a class="btn btn-danger ms-2" href="/">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-4"></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<%- include("../base/footer") %>
|
Loading…
Reference in a new issue