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 AdminBadgeViewModel from "../models/admin/AdminBadgeViewModel";
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 AdminPartiesViewModel from "../models/admin/AdminPartiesViewModel";
import AdminPartyViewModel from "../models/admin/AdminPartyViewModel";
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 PartyService from "../services/PartyService";
import UserService from "../services/UserService";
@ -13,7 +21,8 @@ export default class AdminController_Auth$Admin extends Controller {
public async Index_Get() {
const adminIndexViewModel: AdminIndexViewModel = {
userCount: await UserService.GetUserCount(),
partyCount: await PartyService.GetPartyCount()
partyCount: await PartyService.GetPartyCount(),
badgeCount: await BadgeService.GetBadgeCount()
};
return this.view(adminIndexViewModel);
@ -27,6 +36,33 @@ export default class AdminController_Auth$Admin extends Controller {
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() {
const adminPartiesViewModel: AdminPartiesViewModel = {
parties: await PartyService.GetAll()
@ -35,6 +71,40 @@ export default class AdminController_Auth$Admin extends Controller {
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() {
const adminBadgesViewModel: AdminBadgesViewModel = {
badges: await BadgeService.LoadAll()
@ -80,4 +150,30 @@ export default class AdminController_Auth$Admin extends Controller {
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 AdminController_Auth$Admin from "./controller/AdminController";
import { join } from "path";
import WsData from "./objects/WsData";
Console.customHeader(`MultiProbe server started at ${new Date()}`);
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);

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 {
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);
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, {
path: "/",

View file

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

View file

@ -56,21 +56,23 @@ export default class PartyRepo {
}
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)"];
}
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.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)
]);
]))[0]["Id"];
} else {
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
]);
}
return party;
}
}

View file

@ -60,14 +60,16 @@ export default class UserPartyRepo {
public static async insertUpdate(userParty:UserParty) {
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)
]);
]))[0]["Id"];
} else {
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
]);
}
return userParty;
}
}

View file

@ -50,21 +50,23 @@ export default class UserRepo {
}
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)"];
}
public static async insertUpdate(user:User) {
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)
]);
]))[0]["Id"];
} else {
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
]);
}
return user;
}
}

View file

@ -57,4 +57,13 @@ export default abstract class BadgeService {
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 {
public static async CreateParty(currentUserId:number, name:string, partyRef:string) {
try {
const party = new Party();
let 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";
}
party = await PartyRepo.insertUpdate(party);
const userParty = new UserParty();
userParty.UserId = currentUserId;
userParty.PartyId = newParty.Id;
userParty.PartyId = party.Id;
userParty.CreatedByUserId = currentUserId;
userParty.CreatedDatetime = new Date();
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) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
@ -76,7 +94,25 @@ export default abstract class PartyService {
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.DeletedDatetime = new Date();

View file

@ -4,6 +4,7 @@ import UserRepo from "../repos/UserRepo";
import PasswordUtility from "../utilities/PasswordUtility";
import UserParty from "../entities/UserParty";
import UserPartyRepo from "../repos/UserPartyRepo";
import { UserLevel } from "../enums/UserLevel";
export default class UserService {
public static async AuthenticateUser(username:string, password:string) {
@ -201,4 +202,31 @@ export default class UserService {
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-body">
<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>
@ -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/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/websessions">Web Sessions</a>
<a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/wssessions">Websocket Sessions</a>
</div>
</div>
<%- include("../base/footer") %>

View file

@ -15,6 +15,9 @@
<div class="col">
<h1>Party Management</h1>
</div>
<div class="col-auto">
<a class="btn btn-primary btn-lg me-2" href="/admin/party">Add Party</a>
</div>
</div>
<div class="row my-5">
@ -33,8 +36,8 @@
<td><%= party.PartyRef %></td>
<td><%= party.Name %></td>
<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-danger" href="/admin/userdelete?id=<%= party.Id %>">Delete</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/deleteparty?id=<%= party.Id %>" onclick="return confirm(`Are you sure you want to delete '<%= party.Name %>'?`)">Delete</a>
</td>
</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">
<h1>User Management</h1>
</div>
<div class="col-auto">
<a class="btn btn-primary btn-lg me-2" href="/admin/user">Add User</a>
</div>
</div>
<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>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<nav class="navbar navbar-expand bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="/">
MultiProbe<%= typeof(isAdmin) === "undefined" ? "" : " Admin" %>
</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">
<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 class="navbar-nav">
<% if (typeof(userId) !== "undefined") { %>