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 SessionUser from "./objects/SessionUser";
import PasswordUtility from "./utilities/PasswordUtility";
import CreateEditPartyData from "./interfaces/CreateEditPartyData";
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 }) {
if ("MP_SESSION" in cookies && typeof(cookies["MP_SESSION"]) === "string") {
@ -69,13 +72,15 @@ function validateSession(cookies:{ [cookieName: string]: string | undefined }) {
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);
//const groups = await UserService.GetUserParties(session.userId);
const parties = await UserService.GetUserParties(session.userId);
if (user) {
return res.view("templates/home.ejs", { user, parties: [] });
return res.view("templates/home.ejs", { user, parties });
}
return res.view("templates/index.ejs", { });
@ -96,8 +101,22 @@ fastify.get("/account/register", async (req, res) => {
return res.view("templates/account/register.ejs", { });
});
fastify.setNotFoundHandler(async (req, res) => {
return res.status(404).view("templates/404.ejs", { });
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", { });
});
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
@ -159,6 +178,31 @@ fastify.post("/account/login", async (req, res) => {
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
const websocketServer = new WebSocketServer({
@ -295,4 +339,4 @@ function shutdown() {
process.on("SIGQUIT", 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 {
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) {
return null;
} 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) {
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) {
return null;
} else {
@ -26,14 +42,14 @@ export default class PartyRepo {
}
}
public static async insertUpdate(user:User) {
if (user.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT Party (Username, PasswordHash, PasswordSalt, 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)
public static async insertUpdate(party:Party) {
if (party.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT Party (Name, PartyRef, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
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 {
await Database.Instance.query(`UPDATE Party SET Username = ?, PasswordHash = ?, PasswordSalt = ?, 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
await Database.Instance.query(`UPDATE Party SET Name = ?, PartyRef = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ?, WHERE 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 UserRepo from "../repos/UserRepo";
import PasswordUtility from "../utilities/PasswordUtility";
import Party from "../objects/Party";
import UserParty from "../objects/UserParty";
import UserPartyRepo from "../repos/UserPartyRepo";
export default class UserService {
public static async GetUser(id:number) {
@ -10,6 +13,7 @@ export default class UserService {
return await UserRepo.selectById(id);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
@ -18,6 +22,16 @@ export default class UserService {
return await UserRepo.selectByUsername(username);
} catch (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);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
@ -34,6 +49,7 @@ export default class UserService {
return await PartyRepo.selectByPartyRef(partyRef);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
@ -49,6 +65,35 @@ export default class UserService {
await UserRepo.insertUpdate(user);
} catch (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>
</div>
<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="/account/username">Join 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="/party/join">Join Party</a>
</div>
</div>
</div>
@ -21,9 +21,26 @@
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Code</th>
<th scope="col"></th>
</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>
<% } else { %>
<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") %>