admin updates + client update

This commit is contained in:
Holly Stubbs 2024-10-12 11:52:13 +01:00
parent ee7806da74
commit a798f41ca6
Signed by: tgpholly
GPG key ID: B8583C4B7D18119E
20 changed files with 299 additions and 70 deletions

View file

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name MultiProbe // @name MultiProbe
// @namespace https://*.angusnicneven.com/* // @namespace https://*.angusnicneven.com/*
// @version 20241009.0 // @version 20241012.0
// @description Probe with friends! // @description Probe with friends!
// @author tgpholly // @author tgpholly
// @match https://*.angusnicneven.com/* // @match https://*.angusnicneven.com/*
@ -55,7 +55,7 @@ console.log("[MP] MultiProbe init");
'use strict'; 'use strict';
// Make sure to change the userscript version too!!!!!!!!!! // Make sure to change the userscript version too!!!!!!!!!!
const USERSCRIPT_VERSION_RAW = "20241009.0"; const USERSCRIPT_VERSION_RAW = "20241012.0";
const USERSCRIPT_VERSION = parseInt(USERSCRIPT_VERSION_RAW.replace(".", "")); const USERSCRIPT_VERSION = parseInt(USERSCRIPT_VERSION_RAW.replace(".", ""));
if (!continueRunningScript) { if (!continueRunningScript) {
@ -799,6 +799,7 @@ mp_button {
.writeShortString(apiKey) .writeShortString(apiKey)
.writeString(currentPage) .writeString(currentPage)
.toBuffer()); .toBuffer());
pingStartTime = performance.now();
keepAliveInterval = setInterval(() => { keepAliveInterval = setInterval(() => {
pingStartTime = performance.now(); pingStartTime = performance.now();
ws.send(keepAlivePacket); ws.send(keepAlivePacket);

View file

@ -6,23 +6,28 @@ 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 AdminPartyViewModel from "../models/admin/AdminPartyViewModel";
import AdminUserBadgesViewModel from "../models/admin/AdminUserBadgesViewModel";
import AdminUsersViewModel from "../models/admin/AdminUsersViewModel"; import AdminUsersViewModel from "../models/admin/AdminUsersViewModel";
import AdminUserViewModel from "../models/admin/AdminUserViewModel"; import AdminUserViewModel from "../models/admin/AdminUserViewModel";
import AdminWebSessionsViewModel from "../models/admin/AdminWebSessionsViewModel"; import AdminWebSessionsViewModel from "../models/admin/AdminWebSessionsViewModel";
import AdminWsSessionsViewModel from "../models/admin/AdminWsSessionsViewModel"; import AdminWsSessionsViewModel from "../models/admin/AdminWsSessionsViewModel";
import FunkyArray from "funky-array";
import Session from "../objects/Session"; import Session from "../objects/Session";
import WsData from "../objects/WsData"; 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";
import Controller from "./Controller"; import Controller from "./Controller";
import UserBadgeListItem from "../entities/list/UserBadgeListItem";
import AdminRemoveUserBadgeModel from "../models/admin/AdminRemoveUserBadgeModel";
export default class AdminController_Auth$Admin extends Controller { 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() badgeCount: await BadgeService.GetBadgeCount(),
userBadgeCount: await BadgeService.GetUnlockedBadgeCount()
}; };
return this.view(adminIndexViewModel); return this.view(adminIndexViewModel);
@ -178,4 +183,34 @@ export default class AdminController_Auth$Admin extends Controller {
return this.view(adminWsSessionsViewModel); return this.view(adminWsSessionsViewModel);
} }
public async UserBadges_Get(adminUserBadgesViewModel: AdminUserBadgesViewModel) {
const unlockedBadgeListItems = await BadgeService.GetUnlockedBadgeList(adminUserBadgesViewModel.badgeq, adminUserBadgesViewModel.userq);
const unlockedBadgeByBadgeId = new FunkyArray<number, Array<UserBadgeListItem>>();
let unlockedBadgeByBadgeIdArray: Array<UserBadgeListItem> | undefined;
for (const unlockedBadgeListItem of unlockedBadgeListItems) {
if (!(unlockedBadgeByBadgeIdArray = unlockedBadgeByBadgeId.get(unlockedBadgeListItem.BadgeId))) {
unlockedBadgeByBadgeId.set(unlockedBadgeListItem.BadgeId, unlockedBadgeByBadgeIdArray = new Array<UserBadgeListItem>());
}
unlockedBadgeByBadgeIdArray.push(unlockedBadgeListItem);
}
adminUserBadgesViewModel.badges = await BadgeService.LoadList(adminUserBadgesViewModel.badgeq);
adminUserBadgesViewModel.unlockByBadgeId = unlockedBadgeByBadgeId
return this.view(adminUserBadgesViewModel);
}
public async RemoveUserBadge_Get(adminRemoveUserBadgeModel: AdminRemoveUserBadgeModel) {
if (typeof(adminRemoveUserBadgeModel.id) === "undefined" || typeof(adminRemoveUserBadgeModel.id) !== "string") {
return this.badRequest();
}
const userBadgeId = parseInt(adminRemoveUserBadgeModel.id);
UserService.DeleteUnlockedBadge(this.session.userId, userBadgeId);
return this.redirectToAction("userbadges");
}
} }

View file

@ -10,19 +10,7 @@ export default class Party {
public DeletedDatetime?:Date; public DeletedDatetime?:Date;
public IsDeleted:boolean; public IsDeleted:boolean;
public constructor(id?:number, partyRef?:string, name?:string, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) { public constructor() {
if (typeof(id) == "number" && typeof(partyRef) == "string" && typeof(name) == "string" && 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.PartyRef = partyRef;
this.Name = name;
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.Id = Number.MIN_VALUE;
this.PartyRef = ""; this.PartyRef = "";
this.Name = ""; this.Name = "";
@ -30,5 +18,4 @@ export default class Party {
this.CreatedDatetime = new Date(0); this.CreatedDatetime = new Date(0);
this.IsDeleted = false; this.IsDeleted = false;
} }
}
} }

View file

@ -11,6 +11,7 @@ export default class User {
public PasswordHash:string; public PasswordHash:string;
public APIKey:string; public APIKey:string;
public HasUsedClient:boolean; public HasUsedClient:boolean;
public DameCount:number;
public CreatedByUserId:number; public CreatedByUserId:number;
public CreatedDatetime:Date; public CreatedDatetime:Date;
public LastModifiedByUserId?:number; public LastModifiedByUserId?:number;
@ -27,6 +28,7 @@ export default class User {
this.PasswordSalt = ""; this.PasswordSalt = "";
this.APIKey = ""; this.APIKey = "";
this.HasUsedClient = false; this.HasUsedClient = false;
this.DameCount = 0;
this.CreatedByUserId = Number.MIN_VALUE; this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0); this.CreatedDatetime = new Date(0);
this.IsDeleted = false; this.IsDeleted = false;

View file

@ -11,20 +11,7 @@ export default class UserParty {
public DeletedDatetime?:Date; public DeletedDatetime?:Date;
public IsDeleted:boolean; public IsDeleted:boolean;
public constructor(id?:number, userId?:number, partyId?:number, isActive?:boolean, createdByUserId?:number, createdDateTime?:Date, lastModifiedByUserId?:number, lastModifiedDatetime?:Date, deletedByUserId?:number, deletedDatetime?:Date, isDeleted?:boolean) { public constructor() {
if (typeof(id) == "number" && typeof(userId) == "number" && typeof(partyId) == "number" && typeof(isActive) === "boolean" && typeof(createdByUserId) == "number" && createdDateTime instanceof Date && typeof(lastModifiedByUserId) == "number" && lastModifiedDatetime instanceof Date && typeof(deletedByUserId) == "number" && deletedDatetime instanceof Date && typeof(isDeleted) == "boolean") {
this.Id = id;
this.UserId = userId;
this.PartyId = partyId;
this.IsActive = isActive;
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.Id = Number.MIN_VALUE;
this.UserId = Number.MIN_VALUE; this.UserId = Number.MIN_VALUE;
this.PartyId = Number.MIN_VALUE; this.PartyId = Number.MIN_VALUE;
@ -33,5 +20,4 @@ export default class UserParty {
this.CreatedDatetime = new Date(0); this.CreatedDatetime = new Date(0);
this.IsDeleted = false; this.IsDeleted = false;
} }
}
} }

View file

@ -0,0 +1,13 @@
export default class UserBadgeListItem {
public Id: number;
public BadgeId: number;
public Username: string;
public UnlockDatetime: Date;
public constructor() {
this.Id = Number.MIN_VALUE;
this.BadgeId = Number.MIN_VALUE;
this.Username = "";
this.UnlockDatetime = new Date(0);
}
}

View file

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

View file

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

View file

@ -0,0 +1,11 @@
import Badge from "../../entities/Badge";
import FunkyArray from "funky-array";
import UserBadgeListItem from "../../entities/list/UserBadgeListItem";
export default interface AdminUserBadgesViewModel {
badgeq?: string,
userq?: string,
badges: Array<Badge>,
unlockByBadgeId: FunkyArray<number, Array<UserBadgeListItem>>
}

View file

@ -4,10 +4,10 @@ import RepoBase from "./RepoBase";
export default abstract class BadgeRepo { export default abstract class BadgeRepo {
public static async selectAll() { public static async selectAll() {
const dbUser = await Database.Instance.query("SELECT * FROM Badge WHERE IsDeleted = 0"); const dbBadge = await Database.Instance.query("SELECT * FROM Badge WHERE IsDeleted = 0", []);
const badges = new Array<Badge>(); const badges = new Array<Badge>();
for (const row of dbUser) { for (const row of dbBadge) {
const badge = new Badge(); const badge = new Badge();
populateBadgeFromDB(badge, row); populateBadgeFromDB(badge, row);
badges.push(badge); badges.push(badge);
@ -28,9 +28,22 @@ export default abstract class BadgeRepo {
} }
public static async selectBadgeCount() { public static async selectBadgeCount() {
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `Badge` WHERE IsDeleted = 0 LIMIT 1"); const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `Badge` WHERE IsDeleted = 0 LIMIT 1", []);
return countResult[0]["COUNT(Id)"]; return countResult[0]["COUNT(Id)"] as number;
}
public static async selectList(query?: string) {
const dbBadge = await Database.Instance.query("SELECT * FROM Badge WHERE IsDeleted = 0 AND Name LIKE ?", [ query ? `%${query}%` : "%%" ]);
const badges = new Array<Badge>();
for (const row of dbBadge) {
const badge = new Badge();
populateBadgeFromDB(badge, row);
badges.push(badge);
}
return badges;
} }
public static async insertUpdate(badge:Badge) { public static async insertUpdate(badge:Badge) {

View file

@ -58,7 +58,7 @@ export default class PartyRepo {
public static async selectPartyCount() { public static async selectPartyCount() {
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `Party` WHERE IsDeleted = 0 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)"] as number;
} }
public static async insertUpdate(party:Party) { public static async insertUpdate(party:Party) {

View file

@ -1,3 +1,4 @@
import UserBadgeListItem from "../entities/list/UserBadgeListItem";
import UserBadge from "../entities/UserBadge"; import UserBadge from "../entities/UserBadge";
import Database from "../objects/Database"; import Database from "../objects/Database";
import RepoBase from "./RepoBase"; import RepoBase from "./RepoBase";
@ -14,6 +15,19 @@ export default abstract class UserBadgeRepo {
} }
} }
public static async selectAll() {
const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE IsDeleted = 0", []);
const userBadges = new Array<UserBadge>();
for (const row of dbUserBadge) {
const userBadge = new UserBadge();
populateUserBadgeFromDB(userBadge, row);
userBadges.push(userBadge);
}
return userBadges;
}
public static async selectByUserId(userId:number) { public static async selectByUserId(userId:number) {
const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE UserId = ? AND IsDeleted = 0", [ userId ]); const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE UserId = ? AND IsDeleted = 0", [ userId ]);
const userBadges = new Array<UserBadge>(); const userBadges = new Array<UserBadge>();
@ -38,6 +52,25 @@ export default abstract class UserBadgeRepo {
} }
} }
public static async selectUnlockedBadgeCount() {
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `UserBadge` WHERE IsDeleted = 0 LIMIT 1", []);
return countResult[0]["COUNT(Id)"] as number;
}
public static async selectUnlockList(badgeQuery?: string, userQuery?: string) {
const dbUserBadgeListItems = await Database.Instance.query('SELECT UserBadge.Id AS "Id", Badge.Id AS "BadgeId", `User`.`Username`, UserBadge.CreatedDatetime AS "UnlockDatetime" FROM UserBadge JOIN Badge ON Badge.Id = UserBadge.BadgeId AND Badge.IsDeleted = 0 JOIN `User` ON `User`.Id = UserBadge.UserId AND `User`.IsDeleted = 0 WHERE UserBadge.IsDeleted = 0 AND `User`.`Username` LIKE ? AND Badge.Name LIKE ?;', [ userQuery ? `%${userQuery}%` : "%%", badgeQuery ? `%${badgeQuery}%` : "%%" ]);
const userBadgeListItems = new Array<UserBadgeListItem>();
for (const row of dbUserBadgeListItems) {
const userBadgeListItem = new UserBadgeListItem();
populateUserBadgeListItemFromDB(userBadgeListItem, row);
userBadgeListItems.push(userBadgeListItem);
}
return userBadgeListItems;
}
public static async insertUpdate(userBadge:UserBadge) { public static async insertUpdate(userBadge:UserBadge) {
if (userBadge.Id === Number.MIN_VALUE) { if (userBadge.Id === Number.MIN_VALUE) {
userBadge.Id = (await Database.Instance.query("INSERT UserBadge (UserId, BadgeId, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ userBadge.Id = (await Database.Instance.query("INSERT UserBadge (UserId, BadgeId, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [
@ -53,15 +86,22 @@ export default abstract class UserBadgeRepo {
} }
} }
function populateUserBadgeFromDB(userBadge:UserBadge, dbUser:any) { function populateUserBadgeFromDB(userBadge:UserBadge, dbBadge:any) {
userBadge.Id = dbUser.Id; userBadge.Id = dbBadge.Id;
userBadge.UserId = dbUser.UserId; userBadge.UserId = dbBadge.UserId;
userBadge.BadgeId = dbUser.BadgeId; userBadge.BadgeId = dbBadge.BadgeId;
userBadge.CreatedByUserId = dbUser.CreatedByUserId; userBadge.CreatedByUserId = dbBadge.CreatedByUserId;
userBadge.CreatedDatetime = new Date(dbUser.CreatedDatetime); userBadge.CreatedDatetime = new Date(dbBadge.CreatedDatetime);
userBadge.LastModifiedByUserId = dbUser.LastModifiedByUserId; userBadge.LastModifiedByUserId = dbBadge.LastModifiedByUserId;
userBadge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime); userBadge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.LastModifiedDatetime);
userBadge.DeletedByUserId = dbUser.DeletedByUserId; userBadge.DeletedByUserId = dbBadge.DeletedByUserId;
userBadge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime); userBadge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.DeletedDatetime);
userBadge.IsDeleted = dbUser.IsDeleted[0] === 1; userBadge.IsDeleted = dbBadge.IsDeleted[0] === 1;
}
function populateUserBadgeListItemFromDB(userBadgeListItem:UserBadgeListItem, dbBadgeListItem:any) {
userBadgeListItem.Id = dbBadgeListItem.Id;
userBadgeListItem.BadgeId = dbBadgeListItem.BadgeId;
userBadgeListItem.Username = dbBadgeListItem.Username;
userBadgeListItem.UnlockDatetime = new Date(dbBadgeListItem.UnlockDatetime);
} }

View file

@ -52,7 +52,7 @@ export default class UserRepo {
public static async selectUserCount() { public static async selectUserCount() {
const countResult = await Database.Instance.query("SELECT COUNT(Id) FROM `User` WHERE IsDeleted = 0 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)"] as number;
} }
public static async insertUpdate(user:User) { public static async insertUpdate(user:User) {
@ -61,8 +61,8 @@ export default class UserRepo {
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"]; ]))[0]["Id"];
} else { } else {
await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, HasUsedClient = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, HasUsedClient = ?, DameCount = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [
user.UserLevel, user.Username, user.PasswordHash, user.PasswordSalt, user.APIKey, Number(user.HasUsedClient), 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, Number(user.HasUsedClient), user.DameCount, user.CreatedByUserId, user.CreatedDatetime.getTime(), user.LastModifiedByUserId ?? null, user.LastModifiedDatetime?.getTime() ?? null, user.DeletedByUserId ?? null, user.DeletedDatetime?.getTime() ?? null, Number(user.IsDeleted), user.Id
]); ]);
} }
@ -78,6 +78,7 @@ function populateUserFromDB(user:User, dbUser:any) {
user.PasswordSalt = dbUser.PasswordSalt; user.PasswordSalt = dbUser.PasswordSalt;
user.APIKey = dbUser.APIKey; user.APIKey = dbUser.APIKey;
user.HasUsedClient = dbUser.HasUsedClient[0] === 1; user.HasUsedClient = dbUser.HasUsedClient[0] === 1;
user.DameCount = dbUser.DameCount;
user.CreatedByUserId = dbUser.CreatedByUserId; user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = new Date(dbUser.CreatedDatetime); user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
user.LastModifiedByUserId = dbUser.LastModifiedByUserId; user.LastModifiedByUserId = dbUser.LastModifiedByUserId;

@ -1 +1 @@
Subproject commit 69fb94332deb5dcc1f485d445c042a3f4a7030b0 Subproject commit 7accf5b1c9529490c5713f8dbb9a067cfb2c3d49

View file

@ -2,6 +2,7 @@ import { Console } from "hsconsole";
import Badge from "../entities/Badge"; import Badge from "../entities/Badge";
import BadgeRepo from "../repos/BadgeRepo"; import BadgeRepo from "../repos/BadgeRepo";
import BadgeCache from "../objects/BadgeCache"; import BadgeCache from "../objects/BadgeCache";
import UserBadgeRepo from "../repos/UserBadgeRepo";
export default abstract class BadgeService { export default abstract class BadgeService {
public static async SaveBadge(currentUserId:number, id:number | undefined, name:string, description:string, imageUrl: string, forUrl:string, isSecret: boolean) { public static async SaveBadge(currentUserId:number, id:number | undefined, name:string, description:string, imageUrl: string, forUrl:string, isSecret: boolean) {
@ -51,6 +52,15 @@ export default abstract class BadgeService {
} }
} }
public static async LoadList(query?: string) {
try {
return await BadgeRepo.selectList(query);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async DeleteBadge(currentUserId:number, id: number) { public static async DeleteBadge(currentUserId:number, id: number) {
const badge = await BadgeRepo.selectById(id); const badge = await BadgeRepo.selectById(id);
if (badge == null) { if (badge == null) {
@ -72,4 +82,31 @@ export default abstract class BadgeService {
throw e; throw e;
} }
} }
public static async GetUnlockedBadgeCount() {
try {
return await UserBadgeRepo.selectUnlockedBadgeCount();
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async GetAllUnlockedBadges() {
try {
return await UserBadgeRepo.selectAll();
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async GetUnlockedBadgeList(badgeQuery?: string, userQuery?: string) {
try {
return await UserBadgeRepo.selectUnlockList(badgeQuery, userQuery);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
} }

View file

@ -253,6 +253,44 @@ export default class UserService {
} }
} }
public static async DeleteUnlockedBadge(currentUserId:number, userBadgeId:number) {
try {
let userBadge = await UserBadgeRepo.selectById(userBadgeId);
if (!userBadge) {
return null;
}
userBadge.DeletedByUserId = currentUserId;
userBadge.DeletedDatetime = new Date();
userBadge.IsDeleted = true;
await UserBadgeRepo.insertUpdate(userBadge)
return userBadge;
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async IncrementDameCount(currentUserId:number) {
try {
const user = await UserRepo.selectById(currentUserId);
if (!user) {
return null;
}
user.DameCount++;
user.LastModifiedByUserId = currentUserId;
user.LastModifiedDatetime = new Date();
return await UserRepo.insertUpdate(user);
} catch (e) {
Console.printError(`MultiProbe server service error:\n${e}`);
throw e;
}
}
public static async SaveUserClientLoginFlag(currentUserId:number) { public static async SaveUserClientLoginFlag(currentUserId:number) {
try { try {
const user = await UserRepo.selectById(currentUserId); const user = await UserRepo.selectById(currentUserId);

View file

@ -26,7 +26,6 @@
<thead> <thead>
<th>&nbsp;</th> <th>&nbsp;</th>
<th>Name</th> <th>Name</th>
<th>For URL</th>
<th style="white-space:nowrap">Secret</th> <th style="white-space:nowrap">Secret</th>
<th>&nbsp;</th> <th>&nbsp;</th>
</thead> </thead>
@ -35,7 +34,6 @@
<tr> <tr>
<td class="align-middle"><img style="image-rendering:pixelated" src="<%= badge.ImageUrl.trim().length === 0 ? "/img/missing.png" : badge.ImageUrl %>" width="32" height="32" /></td> <td class="align-middle"><img style="image-rendering:pixelated" src="<%= badge.ImageUrl.trim().length === 0 ? "/img/missing.png" : badge.ImageUrl %>" width="32" height="32" /></td>
<td class="align-middle"><%= badge.Name %></td> <td class="align-middle"><%= badge.Name %></td>
<td class="align-middle"><a href="<%= badge.ForUrl %>"><%= badge.ForUrl %></a></td>
<td class="align-middle text-center"><%= badge.IsSecret ? "Yes" : "No" %></td> <td class="align-middle text-center"><%= badge.IsSecret ? "Yes" : "No" %></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/badge?id=<%= badge.Id %>">Edit</a> <a class="btn btn-sm btn-primary" href="/admin/badge?id=<%= badge.Id %>">Edit</a>
@ -47,4 +45,5 @@
</table> </table>
</div> </div>
</div> </div>
<%- include("../base/footer") %> <%- include("../base/footer") %>

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"><%= badgeCount %></h3> <h3 class="card-text mb-0"><%= badgeCount %><small style="font-size:.75rem" class="ps-2"><%= userBadgeCount %> unlocked</small></h3>
</div> </div>
</div> </div>
</div> </div>
@ -48,6 +48,7 @@
<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/userbadges">Manage Unlocked 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/websessions">Web Sessions</a>
<a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/wssessions">Websocket Sessions</a> <a class="btn btn-primary btn-lg me-2 mb-3" href="/admin/wssessions">Websocket Sessions</a>
</div> </div>

View file

@ -0,0 +1,61 @@
<%- include("../base/header", { title: "Unlocked Badge Management", 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>Unlocked Badge Management</a></li>
</ol>
</nav>
</div>
</div>
<div class="row mb-3">
<div class="col">
<h1>Unlocked Badge Management</h1>
</div>
</div>
<div class="row">
<div class="col">
<form>
<label class="form-label" for="badgeq">Badge Name (fuzzy)</label>
<input class="form-control mb-3" name="badgeq" id="badgeq" value="<%= typeof(badgeq) === "undefined" ? "" : badgeq %>">
<label class="form-label">Username (fuzzy)</label>
<input class="form-control" name="userq" id="userq" value="<%= typeof(userq) === "undefined" ? "" : userq %>">
<input type="submit" hidden />
</form>
</div>
</div>
<div class="row my-5">
<div class="col">
<% for (const badge of badges) { %>
<% const unlockArray = unlockByBadgeId.get(badge.Id) ?? []; %>
<div class="row row-cols-1 mb-3">
<div class="col bg-light-subtle p-3">
<div class="row">
<div class="col-auto align-middle"><img style="image-rendering:pixelated" src="<%= badge.ImageUrl.trim().length === 0 ? "/img/missing.png" : badge.ImageUrl %>" width="32" height="32" /></div>
<div class="col d-flex align-items-center ps-0"><%= badge.Name %></div>
<div class="col-auto d-flex align-items-center pe-4"><%= unlockArray.length %></div>
</div>
</div>
<div class="col bg-dark-subtle">
<% for (const unlock of unlockArray) { %>
<div class="row border-bottom">
<div class="col p-3 d-flex align-items-center">
<%= unlock.Username %>
</div>
<div class="col-auto p-3">
<a class="btn btn-danger" href="/admin/removeuserbadge?id=<%= unlock.Id %>">Remove</a>
</div>
</div>
<% } %>
</div>
</div>
<% } %>
</div>
</div>
<%- include("../base/footer") %>

View file

@ -69,7 +69,7 @@
<% for (const badgeKey of badgeById.keys) { %> <% for (const badgeKey of badgeById.keys) { %>
<% const badge = badgeById.get(badgeKey); const unlockedBadge = unlockedBadgesById.get(badgeKey); %> <% const badge = badgeById.get(badgeKey); const unlockedBadge = unlockedBadgesById.get(badgeKey); %>
<% if (unlockedBadge) { %> <% if (unlockedBadge) { %>
<span class="d-inline-block mb-3" tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="top" data-bs-title="<%= badge.Name %>" data-bs-content="<%= badge.Description %><br><small>Unlocked on <%= unlockedBadge.CreatedDatetime.toString().split(" ").slice(1, 4).join(" ") %></small>"> <span class="d-inline-block mb-3" tabindex="0" data-bs-toggle="popover" data-bs-trigger="hover focus" data-bs-placement="top" data-bs-title="<%= badge.Name %>" data-bs-content="<%= badge.Description.replaceAll("\r", "").replaceAll("\n", "<br>") %><br><%= badge.Id === 13 ? `<small>You've been Dame'd ${user.DameCount} times</small><br>` : "" %><small>Unlocked on <%= unlockedBadge.CreatedDatetime.toString().split(" ").slice(1, 4).join(" ") %></small>">
<img width="32" height="32" src="<%= badge.ImageUrl %>"> <img width="32" height="32" src="<%= badge.ImageUrl %>">
</span> </span>
<% } else if (!badge.IsSecret) { %> <% } else if (!badge.IsSecret) { %>