allow joining party, creating party, setting and unsetting active party, add Party ui to client

This commit is contained in:
Holly Stubbs 2024-04-24 00:05:57 +01:00
parent 882f16356c
commit 771a5ca97a
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
9 changed files with 205 additions and 19 deletions

View file

@ -17,6 +17,9 @@ import { randomBytes } from "crypto";
import SessionUser from "./objects/SessionUser"; import SessionUser from "./objects/SessionUser";
import PasswordUtility from "./utilities/PasswordUtility"; import PasswordUtility from "./utilities/PasswordUtility";
import CreateEditPartyData from "./interfaces/CreateEditPartyData"; import CreateEditPartyData from "./interfaces/CreateEditPartyData";
import JoinPartyData from "./interfaces/JoinPartyData";
import UserParty from "./objects/UserParty";
import IdData from "./interfaces/IdData";
Console.customHeader(`MultiProbe server started at ${new Date()}`); Console.customHeader(`MultiProbe server started at ${new Date()}`);
@ -79,8 +82,9 @@ fastify.get("/", async (req, res) => {
if (session = validateSession(req.cookies)) { if (session = validateSession(req.cookies)) {
const user = await UserService.GetUser(session.userId); const user = await UserService.GetUser(session.userId);
const parties = await UserService.GetUserParties(session.userId); const parties = await UserService.GetUserParties(session.userId);
const activeUserParty = await UserService.GetActiveParty(session.userId);
if (user) { if (user) {
return res.view("templates/home.ejs", { user, parties }); return res.view("templates/home.ejs", { user, parties, activeUserParty });
} }
return res.view("templates/index.ejs", { }); return res.view("templates/index.ejs", { });
@ -101,13 +105,19 @@ fastify.get("/account/register", async (req, res) => {
return res.view("templates/account/register.ejs", { }); 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) => { fastify.get("/party/create", async (req, res) => {
let session:SessionUser | undefined; let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) { if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/"); return res.redirect(302, "/");
} }
return res.view("templates/party/createedit.ejs", { }); return res.view("templates/party/createedit.ejs", { session });
}); });
fastify.get("/party/join", async (req, res) => { fastify.get("/party/join", async (req, res) => {
@ -116,7 +126,35 @@ fastify.get("/party/join", async (req, res) => {
return res.redirect(302, "/"); return res.redirect(302, "/");
} }
return res.view("templates/party/join.ejs", { }); 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 // Post Methods
@ -187,12 +225,12 @@ fastify.post("/party/create", async (req, res) => {
const data = req.body as CreateEditPartyData; const data = req.body as CreateEditPartyData;
if (typeof(data.partyName) !== "string" || typeof(data.partyRef) !== "string" || data.partyName.length === 0 || data.partyRef.length === 0) { if (typeof(data.partyName) !== "string" || typeof(data.partyRef) !== "string" || data.partyName.length === 0 || data.partyRef.length === 0) {
return res.view("templates/party/createedit.ejs", { partyName: data.partyName ?? "", partyRef: data.partyRef ?? "" }); return res.view("templates/party/createedit.ejs", { session, partyName: data.partyName ?? "", partyRef: data.partyRef ?? "" });
} }
const party = await UserService.GetPartyByPartyRef(data.partyRef) const party = await UserService.GetPartyByPartyRef(data.partyRef)
if (party != null) { if (party != null) {
return res.view("templates/party/createedit.ejs", { partyName: data.partyName ?? "", partyRef: data.partyRef ?? "", error: "A group with that Party ID already exists" }); 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); await UserService.CreateParty(session.userId, data.partyName, data.partyRef);
@ -203,6 +241,36 @@ fastify.post("/party/create", async (req, res) => {
} }
}); });
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);
}
});
// Websocket stuff // Websocket stuff
const websocketServer = new WebSocketServer({ const websocketServer = new WebSocketServer({

View file

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

View file

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

View file

@ -2,6 +2,7 @@ export default class UserParty {
public Id:number; public Id:number;
public UserId:number; public UserId:number;
public PartyId:number; public PartyId:number;
public IsActive:boolean;
public CreatedByUserId:number; public CreatedByUserId:number;
public CreatedDatetime:Date; public CreatedDatetime:Date;
public LastModifiedByUserId?:number; public LastModifiedByUserId?:number;
@ -10,11 +11,12 @@ export default class UserParty {
public DeletedDatetime?:Date; public DeletedDatetime?:Date;
public IsDeleted:boolean; public IsDeleted:boolean;
public constructor(id?:number, userId?:number, partyId?:number, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) { public constructor(id?:number, userId?:number, partyId?:number, isActive?:boolean, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) {
if (typeof(id) == "number" && typeof(userId) == "number" && typeof(partyId) == "number" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") { if (typeof(id) == "number" && typeof(userId) == "number" && typeof(partyId) == "number" && typeof(isActive) === "boolean" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") {
this.Id = id; this.Id = id;
this.UserId = userId; this.UserId = userId;
this.PartyId = partyId; this.PartyId = partyId;
this.IsActive = isActive;
this.CreatedByUserId = createdByUserId; this.CreatedByUserId = createdByUserId;
this.CreatedDatetime = createdDateTime; this.CreatedDatetime = createdDateTime;
this.LastModifiedByUserId = lastModifiedByUserId; this.LastModifiedByUserId = lastModifiedByUserId;
@ -26,6 +28,7 @@ export default class UserParty {
this.Id = Number.MIN_VALUE; this.Id = Number.MIN_VALUE;
this.UserId = Number.MIN_VALUE; this.UserId = Number.MIN_VALUE;
this.PartyId = Number.MIN_VALUE; this.PartyId = Number.MIN_VALUE;
this.IsActive = false;
this.CreatedByUserId = Number.MIN_VALUE; this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0); this.CreatedDatetime = new Date(0);
this.IsDeleted = false; this.IsDeleted = false;

View file

@ -30,14 +30,42 @@ export default class UserPartyRepo {
} }
} }
public static async selectByUserIdPartyId(userId:number, partyId:number) {
const dbUserParty = await Database.Instance.query("SELECT * FROM UserParty WHERE UserId = ? AND PartyId = ? AND IsDeleted = 0", [userId, partyId]);
if (dbUserParty == null || dbUserParty.length === 0) {
return null;
} else {
const userParty = new UserParty();
populateUserPartyFromDB(userParty, dbUserParty[0]);
return userParty;
}
}
public static async deactivateAll(userId:number) {
await Database.Instance.query("UPDATE UserParty SET IsActive = 0, LastModifiedByUserId = ?, LastModifiedDatetime = ? WHERE UserId = ? AND IsActive = 1", [
userId, Date.now(), userId
]);
}
public static async selectActive(userId:number) {
const dbUserParty = await Database.Instance.query("SELECT * FROM UserParty WHERE UserId = ? AND IsActive = 1 AND IsDeleted = 0 LIMIT 1", [userId]);
if (dbUserParty == null || dbUserParty.length === 0) {
return null;
} else {
const userParty = new UserParty();
populateUserPartyFromDB(userParty, dbUserParty[0]);
return userParty;
}
}
public static async insertUpdate(userParty:UserParty) { public static async insertUpdate(userParty:UserParty) {
if (userParty.Id === Number.MIN_VALUE) { if (userParty.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT UserParty (UserId, PartyId, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [ await Database.Instance.query("INSERT UserParty (UserId, PartyId, IsActive, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
userParty.UserId, userParty.PartyId, userParty.CreatedByUserId, userParty.CreatedDatetime.getTime(), userParty.LastModifiedByUserId ?? null, userParty.LastModifiedDatetime?.getTime() ?? null, userParty.DeletedByUserId ?? null, userParty.DeletedDatetime?.getTime() ?? null, Number(userParty.IsDeleted) userParty.UserId, userParty.PartyId, Number(userParty.IsActive), userParty.CreatedByUserId, userParty.CreatedDatetime.getTime(), userParty.LastModifiedByUserId ?? null, userParty.LastModifiedDatetime?.getTime() ?? null, userParty.DeletedByUserId ?? null, userParty.DeletedDatetime?.getTime() ?? null, Number(userParty.IsDeleted)
]); ]);
} else { } else {
await Database.Instance.query(`UPDATE UserParty SET UserId = ?, PartyId = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [ await Database.Instance.query(`UPDATE UserParty SET UserId = ?, PartyId = ?, IsActive = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
userParty.UserId, userParty.PartyId, userParty.CreatedByUserId, userParty.CreatedDatetime.getTime(), userParty.LastModifiedByUserId ?? null, userParty.LastModifiedDatetime?.getTime() ?? null, userParty.DeletedByUserId ?? null, userParty.DeletedDatetime?.getTime() ?? null, Number(userParty.IsDeleted), userParty.Id userParty.UserId, userParty.PartyId, Number(userParty.IsActive), userParty.CreatedByUserId, userParty.CreatedDatetime.getTime(), userParty.LastModifiedByUserId ?? null, userParty.LastModifiedDatetime?.getTime() ?? null, userParty.DeletedByUserId ?? null, userParty.DeletedDatetime?.getTime() ?? null, Number(userParty.IsDeleted), userParty.Id
]); ]);
} }
} }
@ -47,6 +75,7 @@ function populateUserPartyFromDB(userParty:UserParty, dbUserParty:any) {
userParty.Id = dbUserParty.Id; userParty.Id = dbUserParty.Id;
userParty.UserId = dbUserParty.UserId; userParty.UserId = dbUserParty.UserId;
userParty.PartyId = dbUserParty.PartyId; userParty.PartyId = dbUserParty.PartyId;
userParty.IsActive = dbUserParty.IsActive;
userParty.CreatedByUserId = dbUserParty.CreatedByUserId; userParty.CreatedByUserId = dbUserParty.CreatedByUserId;
userParty.CreatedDatetime = new Date(dbUserParty.CreatedDatetime); userParty.CreatedDatetime = new Date(dbUserParty.CreatedDatetime);
userParty.LastModifiedByUserId = dbUserParty.LastModifiedByUserId; userParty.LastModifiedByUserId = dbUserParty.LastModifiedByUserId;

View file

@ -35,6 +35,15 @@ export default class UserService {
} }
} }
public static async GetUserPartyForUser(userId:number, partyId:number) {
try {
return await UserPartyRepo.selectByUserIdPartyId(userId, partyId);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async GetParty(id:number) { public static async GetParty(id:number) {
try { try {
return await PartyRepo.selectById(id); return await PartyRepo.selectById(id);
@ -96,4 +105,59 @@ export default class UserService {
throw e; throw e;
} }
} }
public static async AddUserToParty(userId:number, partyId:number) {
try {
const userParty = new UserParty();
userParty.UserId = userId;
userParty.PartyId = partyId;
userParty.CreatedByUserId = userId;
userParty.CreatedDatetime = new Date();
await UserPartyRepo.insertUpdate(userParty);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async SetActiveParty(currentUserId:number, partyId:number) {
try {
await UserPartyRepo.deactivateAll(currentUserId);
const userParty = await UserPartyRepo.selectByUserIdPartyId(currentUserId, partyId);
if (!userParty) {
return;
}
userParty.IsActive = true;
userParty.LastModifiedByUserId = currentUserId;
userParty.LastModifiedDatetime = new Date();
await UserPartyRepo.insertUpdate(userParty);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
console.log(e);
throw e;
}
}
public static async DeactivateCurrentParty(currentUserId:number) {
try {
await UserPartyRepo.deactivateAll(currentUserId);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
console.log(e);
throw e;
}
}
public static async GetActiveParty(currentUserId:number) {
try {
return await UserPartyRepo.selectActive(currentUserId);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
} }

View file

@ -5,8 +5,8 @@
<h3>What would you like to do?</h3> <h3>What would you like to do?</h3>
<div class="mt-3"> <div class="mt-3">
<div> <div>
<a class="btn btn-primary btn-lg me-2" href="/account/username">Change Username</a> <a class="btn btn-primary btn-lg me-2 disabled" href="/account/username">Change Username</a>
<a class="btn btn-primary btn-lg me-2" href="/account/password">Change Password</a> <a class="btn btn-primary btn-lg me-2 disabled" href="/account/password">Change Password</a>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<a class="btn btn-primary btn-lg me-2" href="/party/create">Create Party</a> <a class="btn btn-primary btn-lg me-2" href="/party/create">Create Party</a>
@ -31,7 +31,11 @@
<td><%= party.Name %></td> <td><%= party.Name %></td>
<td><%= party.PartyRef %></td> <td><%= party.PartyRef %></td>
<td class="text-end"> <td class="text-end">
<a href="/party/setactive?id=<%= party.Id %>" class="btn btn-sm btn-success me-2">Set Active</a> <% if (activeUserParty && activeUserParty.PartyId === party.Id) { %>
<a href="/party/deactivate" class="btn btn-sm btn-success me-2">Deactivate</a>
<% } else { %>
<a href="/party/setactive?id=<%= party.Id %>" class="btn btn-sm btn-success me-2">Set Current</a>
<% } %>
<% if (party.CreatedByUserId === user.Id) { %> <% if (party.CreatedByUserId === user.Id) { %>
<a class="btn btn-sm btn-danger disabled" title="You may not leave a party if you created it." onclick="alert('You may not leave a party if you created it.')">Leave</a> <a class="btn btn-sm btn-danger disabled" title="You may not leave a party if you created it." onclick="alert('You may not leave a party if you created it.')">Leave</a>
<% } else { %> <% } else { %>

View file

@ -1,4 +1,4 @@
<%- include("../base/header", { title: typeof(party) === "undefined" ? "Create Party" : `Editing ${party.Name}` }) %> <%- include("../base/header", { title: typeof(party) === "undefined" ? "Create Party" : `Editing ${party.Name}`, userId: session.userId }) %>
<% if (typeof(party) === "undefined") { %> <% if (typeof(party) === "undefined") { %>
<h1 class="text-center mb-5">Create Party</h1> <h1 class="text-center mb-5">Create Party</h1>
<% } else { %> <% } else { %>
@ -8,13 +8,19 @@
<div class="row"> <div class="row">
<div class="col-4"></div> <div class="col-4"></div>
<div class="col-4"> <div class="col-4">
<% if (typeof(error) === "string") { %>
<div class="alert alert-danger">
<%= error %>
</div>
<% } %>
<div class="form-group"> <div class="form-group">
<label class="form-label">Party Name</label> <label class="form-label">Party Name</label>
<input class="form-control" type="text" name="partyName" maxlength="64" required /> <input class="form-control" type="text" name="partyName" maxlength="64" value="<%= typeof(partyName) === "undefined" ? "" : partyName %>" required />
</div> </div>
<div class="form-group mt-3"> <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> <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" 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-5">

View file

@ -1,12 +1,18 @@
<%- include("../base/header", { title: "Join Party" }) %> <%- include("../base/header", { title: "Join Party", userId: session.userId }) %>
<h1 class="text-center mb-5">Join Party</h1> <h1 class="text-center mb-5">Join Party</h1>
<form method="post"> <form method="post">
<div class="row"> <div class="row">
<div class="col-4"></div> <div class="col-4"></div>
<div class="col-4"> <div class="col-4">
<% if (typeof(error) === "string") { %>
<div class="alert alert-danger">
<%= error %>
</div>
<% } %>
<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" 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-5">