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==
// @name MultiProbe
// @namespace https://*.angusnicneven.com/*
// @version 20241009.0
// @version 20241012.0
// @description Probe with friends!
// @author tgpholly
// @match https://*.angusnicneven.com/*
@ -55,7 +55,7 @@ console.log("[MP] MultiProbe init");
'use strict';
// 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(".", ""));
if (!continueRunningScript) {
@ -799,6 +799,7 @@ mp_button {
.writeShortString(apiKey)
.writeString(currentPage)
.toBuffer());
pingStartTime = performance.now();
keepAliveInterval = setInterval(() => {
pingStartTime = performance.now();
ws.send(keepAlivePacket);

View file

@ -6,23 +6,28 @@ 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 AdminUserBadgesViewModel from "../models/admin/AdminUserBadgesViewModel";
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 FunkyArray from "funky-array";
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";
import Controller from "./Controller";
import UserBadgeListItem from "../entities/list/UserBadgeListItem";
import AdminRemoveUserBadgeModel from "../models/admin/AdminRemoveUserBadgeModel";
export default class AdminController_Auth$Admin extends Controller {
public async Index_Get() {
const adminIndexViewModel: AdminIndexViewModel = {
userCount: await UserService.GetUserCount(),
partyCount: await PartyService.GetPartyCount(),
badgeCount: await BadgeService.GetBadgeCount()
badgeCount: await BadgeService.GetBadgeCount(),
userBadgeCount: await BadgeService.GetUnlockedBadgeCount()
};
return this.view(adminIndexViewModel);
@ -178,4 +183,34 @@ export default class AdminController_Auth$Admin extends Controller {
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,25 +10,12 @@ export default class Party {
public DeletedDatetime?:Date;
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) {
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.PartyRef = "";
this.Name = "";
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
this.IsDeleted = false;
}
public constructor() {
this.Id = Number.MIN_VALUE;
this.PartyRef = "";
this.Name = "";
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
this.IsDeleted = false;
}
}

View file

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

View file

@ -11,27 +11,13 @@ export default class UserParty {
public DeletedDatetime?:Date;
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) {
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.UserId = Number.MIN_VALUE;
this.PartyId = Number.MIN_VALUE;
this.IsActive = false;
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
this.IsDeleted = false;
}
public constructor() {
this.Id = Number.MIN_VALUE;
this.UserId = Number.MIN_VALUE;
this.PartyId = Number.MIN_VALUE;
this.IsActive = false;
this.CreatedByUserId = Number.MIN_VALUE;
this.CreatedDatetime = new Date(0);
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 {
userCount: 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 {
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>();
for (const row of dbUser) {
for (const row of dbBadge) {
const badge = new Badge();
populateBadgeFromDB(badge, row);
badges.push(badge);
@ -28,9 +28,22 @@ 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");
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) {

View file

@ -58,7 +58,7 @@ export default class PartyRepo {
public static async selectPartyCount() {
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) {

View file

@ -1,3 +1,4 @@
import UserBadgeListItem from "../entities/list/UserBadgeListItem";
import UserBadge from "../entities/UserBadge";
import Database from "../objects/Database";
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) {
const dbUserBadge = await Database.Instance.query("SELECT * FROM UserBadge WHERE UserId = ? AND IsDeleted = 0", [ userId ]);
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) {
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;", [
@ -53,15 +86,22 @@ export default abstract class UserBadgeRepo {
}
}
function populateUserBadgeFromDB(userBadge:UserBadge, dbUser:any) {
userBadge.Id = dbUser.Id;
userBadge.UserId = dbUser.UserId;
userBadge.BadgeId = dbUser.BadgeId;
userBadge.CreatedByUserId = dbUser.CreatedByUserId;
userBadge.CreatedDatetime = new Date(dbUser.CreatedDatetime);
userBadge.LastModifiedByUserId = dbUser.LastModifiedByUserId;
userBadge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.LastModifiedDatetime);
userBadge.DeletedByUserId = dbUser.DeletedByUserId;
userBadge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbUser.DeletedDatetime);
userBadge.IsDeleted = dbUser.IsDeleted[0] === 1;
function populateUserBadgeFromDB(userBadge:UserBadge, dbBadge:any) {
userBadge.Id = dbBadge.Id;
userBadge.UserId = dbBadge.UserId;
userBadge.BadgeId = dbBadge.BadgeId;
userBadge.CreatedByUserId = dbBadge.CreatedByUserId;
userBadge.CreatedDatetime = new Date(dbBadge.CreatedDatetime);
userBadge.LastModifiedByUserId = dbBadge.LastModifiedByUserId;
userBadge.LastModifiedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.LastModifiedDatetime);
userBadge.DeletedByUserId = dbBadge.DeletedByUserId;
userBadge.DeletedDatetime = RepoBase.convertNullableDatetimeIntToDate(dbBadge.DeletedDatetime);
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() {
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) {
@ -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)
]))[0]["Id"];
} else {
await Database.Instance.query(`UPDATE User SET UserLevelId = ?, Username = ?, PasswordHash = ?, PasswordSalt = ?, APIKey = ?, HasUsedClient = ?, 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
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.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.APIKey = dbUser.APIKey;
user.HasUsedClient = dbUser.HasUsedClient[0] === 1;
user.DameCount = dbUser.DameCount;
user.CreatedByUserId = dbUser.CreatedByUserId;
user.CreatedDatetime = new Date(dbUser.CreatedDatetime);
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 BadgeRepo from "../repos/BadgeRepo";
import BadgeCache from "../objects/BadgeCache";
import UserBadgeRepo from "../repos/UserBadgeRepo";
export default abstract class BadgeService {
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) {
const badge = await BadgeRepo.selectById(id);
if (badge == null) {
@ -72,4 +82,31 @@ export default abstract class BadgeService {
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) {
try {
const user = await UserRepo.selectById(currentUserId);

View file

@ -26,7 +26,6 @@
<thead>
<th>&nbsp;</th>
<th>Name</th>
<th>For URL</th>
<th style="white-space:nowrap">Secret</th>
<th>&nbsp;</th>
</thead>
@ -35,7 +34,6 @@
<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"><%= 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="text-end text-nowrap align-middle">
<a class="btn btn-sm btn-primary" href="/admin/badge?id=<%= badge.Id %>">Edit</a>
@ -47,4 +45,5 @@
</table>
</div>
</div>
<%- include("../base/footer") %>

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"><%= badgeCount %></h3>
<h3 class="card-text mb-0"><%= badgeCount %><small style="font-size:.75rem" class="ps-2"><%= userBadgeCount %> unlocked</small></h3>
</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/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/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/wssessions">Websocket Sessions</a>
</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) { %>
<% const badge = badgeById.get(badgeKey); const unlockedBadge = unlockedBadgesById.get(badgeKey); %>
<% 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 %>">
</span>
<% } else if (!badge.IsSecret) { %>