allow creation of parties

This commit is contained in:
Holly Stubbs 2024-04-23 17:01:25 +01:00
parent 1183f0f9b6
commit 882f16356c
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
9 changed files with 282 additions and 17 deletions

View file

@ -16,6 +16,7 @@ import UsernameData from "./interfaces/UsernameData";
import { randomBytes } from "crypto"; 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";
Console.customHeader(`MultiProbe server started at ${new Date()}`); Console.customHeader(`MultiProbe server started at ${new Date()}`);
@ -56,7 +57,9 @@ fastify.register(FastifyCookie, {
} }
}); });
// Get Methods fastify.setNotFoundHandler(async (req, res) => {
return res.status(404).view("templates/404.ejs", { });
});
function validateSession(cookies:{ [cookieName: string]: string | undefined }) { function validateSession(cookies:{ [cookieName: string]: string | undefined }) {
if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") { if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") {
@ -69,13 +72,15 @@ function validateSession(cookies:{ [cookieName: string]: string | undefined }) {
return undefined; return undefined;
} }
// Get Methods
fastify.get("/", async (req, res) => { fastify.get("/", async (req, res) => {
let session:SessionUser | undefined; let session:SessionUser | undefined;
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 groups = await UserService.GetUserParties(session.userId); const parties = await UserService.GetUserParties(session.userId);
if (user) { if (user) {
return res.view("templates/home.ejs", { user, parties: [] }); return res.view("templates/home.ejs", { user, parties });
} }
return res.view("templates/index.ejs", { }); return res.view("templates/index.ejs", { });
@ -96,8 +101,22 @@ fastify.get("/account/register", async (req, res) => {
return res.view("templates/account/register.ejs", { }); return res.view("templates/account/register.ejs", { });
}); });
fastify.setNotFoundHandler(async (req, res) => { fastify.get("/party/create", async (req, res) => {
return res.status(404).view("templates/404.ejs", { }); let session:SessionUser | undefined;
if (!(session = validateSession(req.cookies))) {
return res.redirect(302, "/");
}
return res.view("templates/party/createedit.ejs", { });
});
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", { });
}); });
// Post Methods // Post Methods
@ -159,6 +178,31 @@ fastify.post("/account/login", async (req, res) => {
return res.view("templates/account/login.ejs", { }); 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", { partyName: data.partyName ?? "", partyRef: data.partyRef ?? "" });
}
const party = await UserService.GetPartyByPartyRef(data.partyRef)
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" });
}
await UserService.CreateParty(session.userId, data.partyName, data.partyRef);
return res.redirect(302, "/");
} catch (e) {
console.error(e);
}
});
// Websocket stuff // Websocket stuff
const websocketServer = new WebSocketServer({ const websocketServer = new WebSocketServer({
@ -295,4 +339,4 @@ function shutdown() {
process.on("SIGQUIT", shutdown); process.on("SIGQUIT", shutdown);
process.on("SIGINT", shutdown); process.on("SIGINT", shutdown);
process.on("SIGUSR2", shutdown); //process.on("SIGUSR2", shutdown);

View file

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

View file

@ -0,0 +1,34 @@
export default class UserParty {
public Id:number;
public UserId:number;
public PartyId:number;
public CreatedByUserId:number;
public CreatedDatetime:Date;
public LastModifiedByUserId?:number;
public LastModifiedDatetime?:Date;
public DeletedByUserId?:number;
public DeletedDatetime?:Date;
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) {
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") {
this.Id = id;
this.UserId = userId;
this.PartyId = partyId;
this.CreatedByUserId = createdByUserId;
this.CreatedDatetime = createdDateTime;
this.LastModifiedByUserId = lastModifiedByUserId;
this.LastModifiedDatetime = lastModifiedDatetime;
this.DeletedByUserId = deletedByUserId;
this.DeletedDatetime = deletedDatetime;
this.IsDeleted = isDeleted;
} else {
this.Id = Number.MIN_VALUE;
this.UserId = Number.MIN_VALUE;
this.PartyId = Number.MIN_VALUE;
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
this.IsDeleted = false;
}
}
}

View file

@ -5,7 +5,7 @@ import RepoBase from "./RepoBase";
export default class PartyRepo { export default class PartyRepo {
public static async selectById(id:number) { public static async selectById(id:number) {
const dbParty = await Database.Instance.query("SELECT * FROM Party WHERE Id = ? LIMIT 1", [id]); const dbParty = await Database.Instance.query("SELECT * FROM Party WHERE Id = ? AND IsDeleted = 0 LIMIT 1", [id]);
if (dbParty == null || dbParty.length === 0) { if (dbParty == null || dbParty.length === 0) {
return null; return null;
} else { } else {
@ -15,8 +15,24 @@ export default class PartyRepo {
} }
} }
public static async selectByUserId(userId:number) {
const dbParties = await Database.Instance.query("SELECT Party.* FROM Party JOIN UserParty ON Party.Id = UserParty.PartyId WHERE UserParty.UserId = ? AND UserParty.IsDeleted = 0 AND Party.IsDeleted = 0", [userId]);
const parties = new Array<Party>();
if (dbParties == null || dbParties.length === 0) {
return parties;
} else {
for (const dbParty of dbParties) {
const party = new Party();
populatePartyFromDB(party, dbParty);
parties.push(party);
}
return parties;
}
}
public static async selectByPartyRef(partyRef:string) { public static async selectByPartyRef(partyRef:string) {
const dbParty = await Database.Instance.query("SELECT * FROM Party WHERE PartyRef = ? LIMIT 1", [partyRef]); const dbParty = await Database.Instance.query("SELECT * FROM Party WHERE PartyRef = ? AND IsDeleted = 0 LIMIT 1", [partyRef]);
if (dbParty == null || dbParty.length === 0) { if (dbParty == null || dbParty.length === 0) {
return null; return null;
} else { } else {
@ -26,14 +42,14 @@ export default class PartyRepo {
} }
} }
public static async insertUpdate(user:User) { public static async insertUpdate(party:Party) {
if (user.Id === Number.MIN_VALUE) { if (party.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT Party (Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ await Database.Instance.query("INSERT Party (Name, PartyRef, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted) party.Name, party.PartyRef, party.CreatedByUserId, party.CreatedDatetime.getTime(), party.LastModifiedByUserId ?? null, party.LastModifiedDatetime?.getTime() ?? null, party.DeletedByUserId ?? null, party.DeletedDatetime?.getTime() ?? null, Number(party.IsDeleted)
]); ]);
} else { } else {
await Database.Instance.query(`UPDATE Party SET Username = ?, PasswordHash = ?, PasswordSalt = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [ await Database.Instance.query(`UPDATE Party SET Name = ?, PartyRef = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE Id = ?`, [
user.Username, user.PasswordHash, user.PasswordSalt, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id party.Name, party.PartyRef, party.CreatedByUserId, party.CreatedDatetime.getTime(), party.LastModifiedByUserId ?? null, party.LastModifiedDatetime?.getTime() ?? null, party.DeletedByUserId ?? null, party.DeletedDatetime?.getTime() ?? null, Number(party.IsDeleted), party.Id
]); ]);
} }
} }

View file

@ -0,0 +1,57 @@
import Database from "../objects/Database";
import UserParty from "../objects/UserParty";
import RepoBase from "./RepoBase";
export default class UserPartyRepo {
public static async selectById(id:number) {
const dbUserParty = await Database.Instance.query("SELECT * FROM UserParty WHERE Id = ? AND IsDeleted = 0 LIMIT 1", [id]);
if (dbUserParty == null || dbUserParty.length === 0) {
return null;
} else {
const userParty = new UserParty();
populateUserPartyFromDB(userParty, dbUserParty[0]);
return userParty;
}
}
public static async selectByUserId(userId:number) {
const dbUserParties = await Database.Instance.query("SELECT * FROM UserParty WHERE UserId = ? AND IsDeleted = 0", [userId]);
if (dbUserParties == null || dbUserParties.length === 0) {
return null;
} else {
const userParties = new Array<UserParty>();
for (const dbUserParty of dbUserParties) {
const userParty = new UserParty();
populateUserPartyFromDB(userParty, dbUserParty[0]);
userParties.push(userParty);
}
return userParties;
}
}
public static async insertUpdate(userParty:UserParty) {
if (userParty.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT UserParty (UserId, PartyId, 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)
]);
} else {
await Database.Instance.query(`UPDATE UserParty SET UserId = ?, PartyId = ?, 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
]);
}
}
}
function populateUserPartyFromDB(userParty:UserParty, dbUserParty:any) {
userParty.Id = dbUserParty.Id;
userParty.UserId = dbUserParty.UserId;
userParty.PartyId = dbUserParty.PartyId;
userParty.CreatedByUserId = dbUserParty.CreatedByUserId;
userParty.CreatedDatetime = new Date(dbUserParty.CreatedDatetime);
userParty.LastModifiedByUserId = dbUserParty.LastModifiedByUserId;
userParty.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.LastModifiedDatetime);
userParty.DeletedByUserId = dbUserParty.DeletedByUserId;
userParty.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUserParty.DeletedDatetime);
userParty.IsDeleted = dbUserParty.IsDeleted;
}

View file

@ -3,6 +3,9 @@ import User from "../objects/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 UserParty from "../objects/UserParty";
import UserPartyRepo from "../repos/UserPartyRepo";
export default class UserService { export default class UserService {
public static async GetUser(id:number) { public static async GetUser(id:number) {
@ -10,6 +13,7 @@ export default class UserService {
return await UserRepo.selectById(id); return await UserRepo.selectById(id);
} catch (e) { } catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`); Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
} }
} }
@ -18,6 +22,16 @@ export default class UserService {
return await UserRepo.selectByUsername(username); return await UserRepo.selectByUsername(username);
} catch (e) { } catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`); Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async GetUserParties(userId:number) {
try {
return await PartyRepo.selectByUserId(userId);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
} }
} }
@ -26,6 +40,7 @@ export default class UserService {
return await PartyRepo.selectById(id); return await PartyRepo.selectById(id);
} catch (e) { } catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`); Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
} }
} }
@ -34,6 +49,7 @@ export default class UserService {
return await PartyRepo.selectByPartyRef(partyRef); return await PartyRepo.selectByPartyRef(partyRef);
} catch (e) { } catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`); Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
} }
} }
@ -49,6 +65,35 @@ export default class UserService {
await UserRepo.insertUpdate(user); await UserRepo.insertUpdate(user);
} catch (e) { } catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`); Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async CreateParty(currentUserId:number, name:string, partyRef:string) {
try {
const party = new Party();
party.Name = name;
party.PartyRef = partyRef;
party.CreatedByUserId = currentUserId;
party.CreatedDatetime = new Date();
await PartyRepo.insertUpdate(party);
const newParty = await PartyRepo.selectByPartyRef(partyRef);
if (!newParty) {
throw "This shouldn't happen";
}
const userParty = new UserParty();
userParty.UserId = currentUserId;
userParty.PartyId = newParty.Id;
userParty.CreatedByUserId = currentUserId;
userParty.CreatedDatetime = new Date();
await UserPartyRepo.insertUpdate(userParty);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
} }
} }
} }

View file

@ -9,8 +9,8 @@
<a class="btn btn-primary btn-lg me-2" href="/account/password">Change Password</a> <a class="btn btn-primary btn-lg me-2" 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="/account/password">Create Party</a> <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="/account/username">Join Party</a> <a class="btn btn-primary btn-lg me-2" href="/party/join">Join Party</a>
</div> </div>
</div> </div>
</div> </div>
@ -21,9 +21,26 @@
<thead> <thead>
<tr> <tr>
<th scope="col">Name</th> <th scope="col">Name</th>
<th scope="col">Code</th>
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody>
<% for (const party of parties) { %>
<tr>
<td><%= party.Name %></td>
<td><%= party.PartyRef %></td>
<td class="text-end">
<a href="/party/setactive?id=<%= party.Id %>" class="btn btn-sm btn-success me-2">Set Active</a>
<% 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>
<% } else { %>
<a href="/party/leave?id=<%= party.Id %>" class="btn btn-sm btn-danger" onclick="return confirm(`Are you sure you want to leave '<%= party.Name %>'?`)">Leave</a>
<% } %>
</td>
</tr>
<% } %>
</tbody>
</table> </table>
<% } else { %> <% } else { %>
<div class="alert alert-primary" role="alert">You are not in any parties.</div> <div class="alert alert-primary" role="alert">You are not in any parties.</div>

View file

@ -0,0 +1,28 @@
<%- include("../base/header", { title: typeof(party) === "undefined" ? "Create Party" : `Editing ${party.Name}` }) %>
<% 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">
<div class="form-group">
<label class="form-label">Party Name</label>
<input class="form-control" type="text" name="partyName" maxlength="64" 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" 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

@ -0,0 +1,20 @@
<%- include("../base/header", { title: "Join Party" }) %>
<h1 class="text-center mb-5">Join Party</h1>
<form method="post">
<div class="row">
<div class="col-4"></div>
<div class="col-4">
<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" 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>
</div>
<div class="col-4"></div>
</div>
</form>
<%- include("../base/footer") %>