finish up the admin side for now

This commit is contained in:
Holly Stubbs 2024-09-28 14:31:02 +01:00
parent b8913702ae
commit 23a206ce99
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
27 changed files with 455 additions and 30 deletions

View file

@ -1,9 +1,17 @@
import AdminBadgesViewModel from "../models/admin/AdminBadgesViewModel"; import AdminBadgesViewModel from "../models/admin/AdminBadgesViewModel";
import AdminBadgeViewModel from "../models/admin/AdminBadgeViewModel"; import AdminBadgeViewModel from "../models/admin/AdminBadgeViewModel";
import AdminDeleteBadgeModel from "../models/admin/AdminDeleteBadgeModel"; import AdminDeleteBadgeModel from "../models/admin/AdminDeleteBadgeModel";
import AdminDeletePartyModel from "../models/admin/AdminDeletePartyModel";
import AdminExpireSessionModel from "../models/admin/AdminExpireSessionModel";
import AdminIndexViewModel from "../models/admin/AdminIndexViewModel"; import AdminIndexViewModel from "../models/admin/AdminIndexViewModel";
import AdminPartiesViewModel from "../models/admin/AdminPartiesViewModel"; import AdminPartiesViewModel from "../models/admin/AdminPartiesViewModel";
import AdminPartyViewModel from "../models/admin/AdminPartyViewModel";
import AdminUsersViewModel from "../models/admin/AdminUsersViewModel"; import AdminUsersViewModel from "../models/admin/AdminUsersViewModel";
import AdminUserViewModel from "../models/admin/AdminUserViewModel";
import AdminWebSessionsViewModel from "../models/admin/AdminWebSessionsViewModel";
import AdminWsSessionsViewModel from "../models/admin/AdminWsSessionsViewModel";
import Session from "../objects/Session";
import WsData from "../objects/WsData";
import BadgeService from "../services/BadgeService"; import BadgeService from "../services/BadgeService";
import PartyService from "../services/PartyService"; import PartyService from "../services/PartyService";
import UserService from "../services/UserService"; import UserService from "../services/UserService";
@ -13,7 +21,8 @@ export default class AdminController_Auth$Admin extends Controller {
public async Index_Get() { public async Index_Get() {
const adminIndexViewModel: AdminIndexViewModel = { const adminIndexViewModel: AdminIndexViewModel = {
userCount: await UserService.GetUserCount(), userCount: await UserService.GetUserCount(),
partyCount: await PartyService.GetPartyCount() partyCount: await PartyService.GetPartyCount(),
badgeCount: await BadgeService.GetBadgeCount()
}; };
return this.view(adminIndexViewModel); return this.view(adminIndexViewModel);
@ -27,6 +36,33 @@ export default class AdminController_Auth$Admin extends Controller {
return this.view(adminUsersViewModel); return this.view(adminUsersViewModel);
} }
public async User_Get(adminUserViewModel: AdminUserViewModel) {
const user = adminUserViewModel.id ? await UserService.GetUser(parseInt(adminUserViewModel.id)) : null;
if (typeof(adminUserViewModel.id) !== "undefined" && user) {
adminUserViewModel.username = user.Username;
adminUserViewModel.userLevel = String(user.UserLevel);
} else {
adminUserViewModel.username = "";
adminUserViewModel.userLevel = "0";
}
return this.view(adminUserViewModel);
}
public async User_Post(adminUserViewModel: AdminUserViewModel) {
if (typeof(adminUserViewModel.id) === "undefined") {
return this.badRequest();
}
const user = await UserService.SaveUser(this.session.userId, parseInt(adminUserViewModel.id), adminUserViewModel.username ?? "", parseInt(adminUserViewModel.userLevel ?? ""));
if (!user) {
adminUserViewModel.message = "A user with that username already exists.";
return this.view(adminUserViewModel);
}
return this.redirectToAction("users");
}
public async Parties_Get() { public async Parties_Get() {
const adminPartiesViewModel: AdminPartiesViewModel = { const adminPartiesViewModel: AdminPartiesViewModel = {
parties: await PartyService.GetAll() parties: await PartyService.GetAll()
@ -35,6 +71,40 @@ export default class AdminController_Auth$Admin extends Controller {
return this.view(adminPartiesViewModel); return this.view(adminPartiesViewModel);
} }
public async Party_Get(adminPartyViewModel: AdminPartyViewModel) {
const party = adminPartyViewModel.id ? await PartyService.GetParty(parseInt(adminPartyViewModel.id)) : null;
if (typeof(adminPartyViewModel.id) !== "undefined" && party) {
adminPartyViewModel.partyRef = party.PartyRef;
adminPartyViewModel.name = party.Name;
} else {
adminPartyViewModel.partyRef = "";
adminPartyViewModel.name = "";
}
return this.view(adminPartyViewModel);
}
public async Party_Post(adminPartyViewModel: AdminPartyViewModel) {
if (typeof(adminPartyViewModel.id) === "undefined") {
return this.badRequest();
}
await PartyService.SaveParty(this.session.userId, parseInt(adminPartyViewModel.id), adminPartyViewModel.name ?? "", adminPartyViewModel.partyRef ?? "");
return this.redirectToAction("parties");
}
public async DeleteParty_Get(adminDeletePartyModel: AdminDeletePartyModel) {
if (typeof(adminDeletePartyModel.id) === "undefined" || typeof(adminDeletePartyModel.id) !== "string") {
return this.badRequest();
}
const partyId = parseInt(adminDeletePartyModel.id);
await PartyService.DeletePartyAdmin(this.session.userId, partyId);
return this.redirectToAction("parties");
}
public async Badges_Get() { public async Badges_Get() {
const adminBadgesViewModel: AdminBadgesViewModel = { const adminBadgesViewModel: AdminBadgesViewModel = {
badges: await BadgeService.LoadAll() badges: await BadgeService.LoadAll()
@ -80,4 +150,30 @@ export default class AdminController_Auth$Admin extends Controller {
return this.redirectToAction("badges"); return this.redirectToAction("badges");
} }
public async WebSessions_Get() {
const adminWebSessionsViewModel: AdminWebSessionsViewModel = {
sessions: Session.Sessions
};
return this.view(adminWebSessionsViewModel);
}
public async ExpireSession_Get(adminExpireSessionModel: AdminExpireSessionModel) {
if (typeof(adminExpireSessionModel.key) === "undefined" || typeof(adminExpireSessionModel.key) !== "string") {
return this.badRequest();
}
Session.Sessions.remove(adminExpireSessionModel.key);
return this.redirectToAction("websessions");
}
public async WsSessions_Get() {
const adminWsSessionsViewModel: AdminWsSessionsViewModel = {
users: WsData.users
};
return this.view(adminWsSessionsViewModel);
}
} }

View file

@ -49,10 +49,12 @@ import ApiController from "./controller/ApiController";
import PartyService from "./services/PartyService"; import PartyService from "./services/PartyService";
import AdminController_Auth$Admin from "./controller/AdminController"; import AdminController_Auth$Admin from "./controller/AdminController";
import { join } from "path"; import { join } from "path";
import WsData from "./objects/WsData";
Console.customHeader(`MultiProbe server started at ${new Date()}`); Console.customHeader(`MultiProbe server started at ${new Date()}`);
const users = new FunkyArray<string, RemoteUser>(); const users = new FunkyArray<string, RemoteUser>();
WsData.users = users;
new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name); new Database(Config.database.address, Config.database.port, Config.database.username, Config.database.password, Config.database.name);

View file

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

View file

@ -0,0 +1,3 @@
export default interface AdminExpireSessionModel {
key?:string
}

View file

@ -1,4 +1,5 @@
export default interface AdminIndexViewModel { export default interface AdminIndexViewModel {
userCount: number, userCount: number,
partyCount: number partyCount: number,
badgeCount: number
} }

View file

@ -0,0 +1,5 @@
export default interface AdminPartyViewModel {
id?: string,
partyRef?: string,
name?: string
}

View file

@ -0,0 +1,7 @@
export default interface AdminUserViewModel {
message?:string,
id?: string,
username?: string,
userLevel?: string
}

View file

@ -0,0 +1,6 @@
import FunkyArray from "funky-array";
import SessionUser from "../../objects/SessionUser";
export default interface AdminWebSessionsViewModel {
sessions: FunkyArray<string, SessionUser>
}

View file

@ -0,0 +1,6 @@
import FunkyArray from "funky-array";
import RemoteUser from "../../objects/RemoteUser";
export default interface AdminWsSessionsViewModel {
users: FunkyArray<string, RemoteUser>
}

View file

@ -37,7 +37,7 @@ export default abstract class Session {
validPeriod.setTime(validPeriod.getTime() + Config.session.validity); validPeriod.setTime(validPeriod.getTime() + Config.session.validity);
const key = randomBytes(Config.session.length).toString("hex"); const key = randomBytes(Config.session.length).toString("hex");
Session.Sessions.set(key, new SessionUser(user.Id, user.UserLevel, validPeriod)); Session.Sessions.set(key, new SessionUser(user.Id, user.Username, user.UserLevel, validPeriod));
res.setCookie("MP_SESSION", key, { res.setCookie("MP_SESSION", key, {
path: "/", path: "/",

View file

@ -2,11 +2,13 @@ import { UserLevel } from "../enums/UserLevel";
export default class SessionUser { export default class SessionUser {
public readonly userId:number; public readonly userId:number;
public readonly username:string;
public readonly userLevel:UserLevel; public readonly userLevel:UserLevel;
public readonly validityPeriod:Date; public readonly validityPeriod:Date;
constructor(userId:number, userLevel:UserLevel, validityPeriod:Date) { constructor(userId:number, username:string, userLevel:UserLevel, validityPeriod:Date) {
this.userId = userId; this.userId = userId;
this.username = username;
this.userLevel = userLevel; this.userLevel = userLevel;
this.validityPeriod = validityPeriod; this.validityPeriod = validityPeriod;
} }

6
server/objects/WsData.ts Normal file
View file

@ -0,0 +1,6 @@
import FunkyArray from "funky-array";
import RemoteUser from "./RemoteUser";
export default abstract class WsData {
public static users: FunkyArray<string, RemoteUser>;
}

View file

@ -27,6 +27,12 @@ export default abstract class BadgeRepo {
} }
} }
public static async selectBadgeCount() {
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `Badge` WHERE IsDeleted = 0 LIMIT 1");
return countResult[0]["COUNT(Id)"];
}
public static async insertUpdate(badge:Badge) { public static async insertUpdate(badge:Badge) {
if (badge.Id === Number.MIN_VALUE) { if (badge.Id === Number.MIN_VALUE) {
badge.Id = (await Database.Instance.query("INSERT Badge (Name, Description, ImageUrl, ForUrl, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ badge.Id = (await Database.Instance.query("INSERT Badge (Name, Description, ImageUrl, ForUrl, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [

View file

@ -56,21 +56,23 @@ export default class PartyRepo {
} }
public static async selectPartyCount() { public static async selectPartyCount() {
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `Party` LIMIT 1"); const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `Party` WHERE IsDeleted = 0 LIMIT 1");
return countResult[0]["COUNT(Id)"]; return countResult[0]["COUNT(Id)"];
} }
public static async insertUpdate(party:Party) { public static async insertUpdate(party:Party) {
if (party.Id === Number.MIN_VALUE) { if (party.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT Party (Name, PartyRef, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [ party.Id = (await Database.Instance.query("INSERT Party (Name, PartyRef, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING 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.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)
]); ]))[0]["Id"];
} else { } else {
await Database.Instance.query("UPDATE Party SET Name = ?, PartyRef = ?, 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 = ?", [
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 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
]); ]);
} }
return party;
} }
} }

View file

@ -60,14 +60,16 @@ export default class UserPartyRepo {
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, IsActive, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ userParty.Id = (await Database.Instance.query("INSERT UserParty (UserId, PartyId, IsActive, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING 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.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)
]); ]))[0]["Id"];
} else { } else {
await Database.Instance.query(`UPDATE UserParty SET UserId = ?, PartyId = ?, IsActive = ?, 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, 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 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
]); ]);
} }
return userParty;
} }
} }

View file

@ -50,21 +50,23 @@ export default class UserRepo {
} }
public static async selectUserCount() { public static async selectUserCount() {
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `User` LIMIT 1"); const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `User` WHERE IsDeleted = 0 LIMIT 1");
return countResult[0]["COUNT(Id)"]; return countResult[0]["COUNT(Id)"];
} }
public static async insertUpdate(user:User) { public static async insertUpdate(user:User) {
if (user.Id === Number.MIN_VALUE) { if (user.Id === Number.MIN_VALUE) {
await Database.Instance.query("INSERT User (UserLevelId, Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [ user.Id = (await Database.Instance.query("INSERT User (UserLevelId, Username, PasswordHash, PasswordSalt, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
user.UserLevel, 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.UserLevel, 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)
]); ]))[0]["Id"];
} else { } else {
await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id
]); ]);
} }
return user;
} }
} }

View file

@ -57,4 +57,13 @@ export default abstract class BadgeService {
return await BadgeRepo.insertUpdate(badge); return await BadgeRepo.insertUpdate(badge);
} }
public static async GetBadgeCount() {
try {
return await BadgeRepo.selectBadgeCount();
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
} }

View file

@ -7,26 +7,44 @@ import UserPartyRepo from "../repos/UserPartyRepo";
export default abstract class PartyService { export default abstract class PartyService {
public static async CreateParty(currentUserId:number, name:string, partyRef:string) { public static async CreateParty(currentUserId:number, name:string, partyRef:string) {
try { try {
const party = new Party(); let party = new Party();
party.Name = name; party.Name = name;
party.PartyRef = partyRef; party.PartyRef = partyRef;
party.CreatedByUserId = currentUserId; party.CreatedByUserId = currentUserId;
party.CreatedDatetime = new Date(); party.CreatedDatetime = new Date();
await PartyRepo.insertUpdate(party); party = await PartyRepo.insertUpdate(party);
const newParty = await PartyRepo.selectByPartyRef(partyRef);
if (!newParty) {
throw "This shouldn't happen";
}
const userParty = new UserParty(); const userParty = new UserParty();
userParty.UserId = currentUserId; userParty.UserId = currentUserId;
userParty.PartyId = newParty.Id; userParty.PartyId = party.Id;
userParty.CreatedByUserId = currentUserId; userParty.CreatedByUserId = currentUserId;
userParty.CreatedDatetime = new Date(); userParty.CreatedDatetime = new Date();
await UserPartyRepo.insertUpdate(userParty); await UserPartyRepo.insertUpdate(userParty);
return party;
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async SaveParty(currentUserId:number, id:number | undefined, name:string, partyRef:string) {
try {
let party = id ? await PartyRepo.selectById(id) : null;
if (!party) {
party = new Party();
party.CreatedByUserId = currentUserId;
party.CreatedDatetime = new Date();
} else {
party.LastModifiedByUserId = currentUserId;
party.LastModifiedDatetime = new Date();
}
party.Name = name;
party.PartyRef = partyRef;
return await PartyRepo.insertUpdate(party);
} catch (e) { } catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`); Console.printError(`MultiProbe server service error:\n${e}`);
throw e; throw e;
@ -76,7 +94,25 @@ export default abstract class PartyService {
return null; return null;
} }
console.log(party); party.DeletedByUserId = currentUserId;
party.DeletedDatetime = new Date();
party.IsDeleted = true;
await PartyRepo.insertUpdate(party);
return party;
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async DeletePartyAdmin(currentUserId:number, partyId:number) {
try {
const party = await PartyRepo.selectById(partyId);
if (!party) {
return null;
}
party.DeletedByUserId = currentUserId; party.DeletedByUserId = currentUserId;
party.DeletedDatetime = new Date(); party.DeletedDatetime = new Date();

View file

@ -4,6 +4,7 @@ import UserRepo from "../repos/UserRepo";
import PasswordUtility from "../utilities/PasswordUtility"; import PasswordUtility from "../utilities/PasswordUtility";
import UserParty from "../entities/UserParty"; import UserParty from "../entities/UserParty";
import UserPartyRepo from "../repos/UserPartyRepo"; import UserPartyRepo from "../repos/UserPartyRepo";
import { UserLevel } from "../enums/UserLevel";
export default class UserService { export default class UserService {
public static async AuthenticateUser(username:string, password:string) { public static async AuthenticateUser(username:string, password:string) {
@ -201,4 +202,31 @@ export default class UserService {
throw e; throw e;
} }
} }
public static async SaveUser(currentUserId:number, id:number | undefined, username:string, userLevel:UserLevel) {
try {
const existingCheck = id ? null : await UserRepo.selectByUsername(username);
if (existingCheck) {
return null;
}
let user = id ? await UserRepo.selectById(id) : null;
if (!user) {
user = new User();
user.CreatedByUserId = currentUserId;
user.CreatedDatetime = new Date();
} else {
user.LastModifiedByUserId = currentUserId;
user.LastModifiedDatetime = new Date();
}
user.Username = username;
user.UserLevel = userLevel;
return await UserRepo.insertUpdate(user);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
} }

View file

@ -37,7 +37,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h4 class="card-title">Badges</h5> <h4 class="card-title">Badges</h5>
<h3 class="card-text mb-0"><%= 0 %></h3> <h3 class="card-text mb-0"><%= badgeCount %></h3>
</div> </div>
</div> </div>
</div> </div>
@ -48,6 +48,8 @@
<a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/users">Manage Users</a> <a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/users">Manage Users</a>
<a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/parties">Manage Parties</a> <a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/parties">Manage Parties</a>
<a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/badges">Manage Badges</a> <a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/badges">Manage Badges</a>
<a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/websessions">Web Sessions</a>
<a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/wssessions">Websocket Sessions</a>
</div> </div>
</div> </div>
<%- include("../base/footer") %> <%- include("../base/footer") %>

View file

@ -15,6 +15,9 @@
<div class="col"> <div class="col">
<h1>Party Management</h1> <h1>Party Management</h1>
</div> </div>
<div class="col-auto">
<a class="btn btn-primary btn-lg me-2" href="/admin/party">Add Party</a>
</div>
</div> </div>
<div class="row my-5"> <div class="row my-5">
@ -33,8 +36,8 @@
<td><%= party.PartyRef %></td> <td><%= party.PartyRef %></td>
<td><%= party.Name %></td> <td><%= party.Name %></td>
<td class="text-end text-nowrap align-middle"> <td class="text-end text-nowrap align-middle">
<a class="btn btn-sm btn-primary" href="/admin/user?id=<%= party.Id %>">Edit</a> <a class="btn btn-sm btn-primary" href="/admin/party?id=<%= party.Id %>">Edit</a>
<a class="btn btn-sm btn-danger" href="/admin/userdelete?id=<%= party.Id %>">Delete</a> <a class="btn btn-sm btn-danger" href="/admin/deleteparty?id=<%= party.Id %>" onclick="return confirm(`Are you sure you want to delete '<%= party.Name %>'?`)">Delete</a>
</td> </td>
</tr> </tr>
<% } %> <% } %>

View file

@ -0,0 +1,43 @@
<%- include("../base/header", { title: typeof(id) === "undefined" || id.trim().length === 0 ? "Add Party" : `Edit ${name}`, userId: session.userId, isAdmin: true }) %>
<div class="row">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
<li class="breadcrumb-item"><a href="/admin/parties">Party Management</a></li>
<li class="breadcrumb-item active"><a><%= typeof(id) === "undefined" || id.trim().length === 0 ? "Add Party" : `Edit ${name}` %></a></li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col">
<h1><%= typeof(id) === "undefined" || id.trim().length === 0 ? "Add Party" : `Edit ${name}` %></h1>
</div>
</div>
<form method="post" class="needs-validation" novalidate>
<input type="hidden" name="id" value="<%= typeof(id) === "undefined" ? "" : id %>" />
<div class="row my-5 row-cols-1 row-cols-sm-2">
<div class="col col-sm-3 mb-3">
<label for="partyRef" class="form-label">Party Ref</label>
<input class="form-control" id="partyRef" name="partyRef" value="<%= typeof(partyRef) === "undefined" ? "" : partyRef %>" required maxlength="5" />
</div>
<div class="col col-sm-9 col-sm-9">
<label for="name" class="form-label">Name</label>
<input class="form-control" id="name" name="name" value="<%= typeof(name) === "undefined" ? "" : name %>" required maxlength="255" />
</div>
</div>
<div class="row mb-3">
<div class="col text-center">
<button type="submit" class="btn btn-primary">Save</button>
<a type="submit" class="btn btn-danger" href="/admin/parties">Cancel</a>
</div>
</div>
</form>
<%- include("../base/footer") %>

View file

@ -0,0 +1,65 @@
<%- include("../base/header", { title: typeof(id) === "undefined" || id.trim().length === 0 ? "Add User" : `Edit ${username}`, userId: session.userId, isAdmin: true }) %>
<div class="row">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
<li class="breadcrumb-item"><a href="/admin/users">User Management</a></li>
<li class="breadcrumb-item active"><a><%= typeof(id) === "undefined" ? "Add User" : `Edit ${username}` %></a></li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col">
<h1><%= typeof(id) === "undefined" || id.trim().length === 0 ? "Add User" : `Edit ${username}` %></h1>
</div>
</div>
<% if (typeof(message) === "string") { %>
<div class="alert alert-danger text-center mt-5" role="alert"><%= message %></div>
<% } %>
<form method="post" class="needs-validation" novalidate>
<input type="hidden" name="id" value="<%= typeof(id) === "undefined" || id.trim().length === 0 ? "" : id %>" />
<div class="row mt-5 mb-3">
<div class="col">
<label for="username" class="form-label">Username</label>
<input class="form-control" id="username" name="username" value="<%= typeof(username) === "undefined" ? "" : username %>" required maxlength="255" autocomplete="one-time-code" />
</div>
</div>
<div class="row mb-5">
<div class="col">
<label for="name" class="form-label">Permissions</label>
<select class="form-select" name="userLevel" required>
<option value="" disabled <%= userLevel === "0" ? "selected" : "" %>>Please select...</option>
<option value="10" <%= userLevel === "10" ? "selected" : "" %>>User</option>
<option value="999" <%= userLevel === "999" ? "selected" : "" %>>Admin</option>
</select>
</div>
</div>
<div class="row mb-3">
<div class="col text-center">
<button type="submit" class="btn btn-primary">Save</button>
<a type="submit" class="btn btn-danger" href="/admin/users">Cancel</a>
</div>
</div>
</form>
<script>
const imageImg = document.querySelector("#imageImg");
const imageUrl = document.querySelector("#imageUrl");
imageUrl.addEventListener("change", () => {
if (imageUrl.value.trim() === ""){
imageImg.src = "/img/missing.png";
} else {
imageImg.src = imageUrl.value;
}
});
</script>
<%- include("../base/footer") %>

View file

@ -15,6 +15,9 @@
<div class="col"> <div class="col">
<h1>User Management</h1> <h1>User Management</h1>
</div> </div>
<div class="col-auto">
<a class="btn btn-primary btn-lg me-2" href="/admin/user">Add User</a>
</div>
</div> </div>
<div class="row my-5"> <div class="row my-5">

View file

@ -0,0 +1,45 @@
<%- include("../base/header", { title: "Web Sessions", userId: session.userId, isAdmin: true }) %>
<div class="row">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
<li class="breadcrumb-item active"><a>Web Sessions</a></li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col">
<h1>Web Sessions</h1>
</div>
</div>
<div class="row my-5">
<div class="col">
<table class="table table-striped">
<thead>
<th class="align-middle text-center">User Id</th>
<th class="align-middle text-center">Username</th>
<th class="align-middle text-center">Expires At</th>
<th>&nbsp;</th>
</thead>
<tbody><%
const keys = sessions._getKeys();
for (const key of keys) {
const session = sessions.get(key); %>
<tr>
<td class="align-middle text-center"><%= session.userId %></td>
<td class="align-middle text-center"><%= session.username %></td>
<td class="align-middle text-center"><%= session.validityPeriod.toString().split("GMT")[0] %></td>
<td class="text-end text-nowrap align-middle">
<a class="btn btn-sm btn-danger" href="/admin/expiresession?key=<%= key %>">Expire</a>
</td>
</tr>
<% } %></tbody>
</table>
</div>
</div>
<%- include("../base/footer") %>

View file

@ -0,0 +1,43 @@
<%- include("../base/header", { title: "Websocket Sessions", userId: session.userId, isAdmin: true }) %>
<div class="row">
<div class="col">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/admin">Admin</a></li>
<li class="breadcrumb-item active"><a>Websocket Sessions</a></li>
</ol>
</nav>
</div>
</div>
<div class="row">
<div class="col">
<h1>Websocket Sessions</h1>
</div>
</div>
<div class="row my-5">
<div class="col">
<table class="table table-striped">
<thead>
<th class="align-middle text-center">#</th>
<th class="align-middle text-center">Username</th>
<th class="align-middle text-center">Current Location</th>
<th class="align-middle text-center">Is AFK?</th>
</thead>
<tbody><%
const keys = users._getKeys();
for (const key of keys) {
const session = users.get(key); %>
<tr>
<td class="align-middle text-center"><%= session.id %></td>
<td class="align-middle text-center"><%= session.username %></td>
<td class="align-middle text-center"><a href="https://<%= session.rawURL %>">https://<%= session.rawURL %></a></td>
<td class="align-middle text-center"><%= session.isAfk ? "Yes" : "No" %></td>
</tr>
<% } %></tbody>
</table>
</div>
</div>
<%- include("../base/footer") %>

View file

@ -20,17 +20,16 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg bg-body-tertiary"> <nav class="navbar navbar-expand bg-body-tertiary">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
MultiProbe<%= typeof(isAdmin) === "undefined" ? "" : " Admin" %> MultiProbe<%= typeof(isAdmin) === "undefined" ? "" : " Admin" %>
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0"> <ul class="navbar-nav me-auto">
<div class="nav-item">
<a class="nav-link" href="https://git.eusv.net/tgpholly/t00-multiuser" target="_blank">Source Code</a>
</div>
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
<% if (typeof(userId) !== "undefined") { %> <% if (typeof(userId) !== "undefined") { %>