diff --git a/server/controller/AdminController.ts b/server/controller/AdminController.ts index 367329a..8cee842 100644 --- a/server/controller/AdminController.ts +++ b/server/controller/AdminController.ts @@ -20,6 +20,10 @@ import UserService from "../services/UserService"; import Controller from "./Controller"; import UserBadgeListItem from "../entities/list/UserBadgeListItem"; import AdminRemoveUserBadgeModel from "../models/admin/AdminRemoveUserBadgeModel"; +import OverrideService from "../services/OverrideService"; +import AdminOverridesViewModel from "../models/admin/AdminOverridesViewModel"; +import AdminOverrideViewModel from "../models/admin/AdminOverrideViewModel"; +import OverrideType from "../enums/OverrideType"; export default class AdminController_Auth$Admin extends Controller { public async Index_Get() { @@ -215,4 +219,39 @@ export default class AdminController_Auth$Admin extends Controller { return this.redirectToAction("userbadges"); } + + public async Overrides_Get() { + const adminOverridesViewModel: AdminOverridesViewModel = { + overrides: await OverrideService.GetOverrides() + } + + return this.view(adminOverridesViewModel); + } + + public async Override_Get(adminOverrideViewModel: AdminOverrideViewModel) { + const override = adminOverrideViewModel.id ? await OverrideService.GetOverride(parseInt(adminOverrideViewModel.id)) : null; + if (typeof(adminOverrideViewModel.id) !== "undefined" && override) { + adminOverrideViewModel.overrideTypeId = override.OverrideType.toString(); + adminOverrideViewModel.name = override.Name; + adminOverrideViewModel.forUrl = override.ForURL; + adminOverrideViewModel.cssQuery = override.CSSQuery; + } else { + adminOverrideViewModel.overrideTypeId = OverrideType.Unknown.toString(); + adminOverrideViewModel.name = ""; + adminOverrideViewModel.forUrl = ""; + adminOverrideViewModel.cssQuery = ""; + } + + return this.view(adminOverrideViewModel); + } + + public async Override_Post(adminOverrideViewModel: AdminOverrideViewModel) { + if (typeof(adminOverrideViewModel.id) === "undefined") { + return this.badRequest(); + } + + await OverrideService.SaveOverride(this.session.userId, parseInt(adminOverrideViewModel.id), parseInt(adminOverrideViewModel.overrideTypeId), adminOverrideViewModel.name, adminOverrideViewModel.forUrl, adminOverrideViewModel.cssQuery); + + return this.redirectToAction("badges"); + } } \ No newline at end of file diff --git a/server/controller/ApiController.ts b/server/controller/ApiController.ts index bf2f31b..5458378 100644 --- a/server/controller/ApiController.ts +++ b/server/controller/ApiController.ts @@ -63,4 +63,8 @@ export default class ApiController extends Controller { }); } } + + public async GetOverrides_Get_AllowAnonymous() { + return this.ok("test"); + } } \ No newline at end of file diff --git a/server/entities/Override.ts b/server/entities/Override.ts new file mode 100644 index 0000000..7a66d40 --- /dev/null +++ b/server/entities/Override.ts @@ -0,0 +1,16 @@ +import OverrideType from "../enums/OverrideType"; + +export default class Override { + public Id: number = Number.MIN_VALUE; + public OverrideType: OverrideType = OverrideType.Unknown; + public Name: string = ""; + public ForURL: string = ""; + public CSSQuery: string = ""; + public CreatedByUserId: number = Number.MIN_VALUE; + public CreatedDatetime: Date = new Date(); + public LastModifiedByUserId?: number; + public LastModifiedDatetime?: Date; + public DeletedByUserId?: number; + public DeletedDatetime?: Date; + public IsDeleted: boolean = false; +} \ No newline at end of file diff --git a/server/entities/OverrideField.ts b/server/entities/OverrideField.ts new file mode 100644 index 0000000..d0106d3 --- /dev/null +++ b/server/entities/OverrideField.ts @@ -0,0 +1,15 @@ +import OverrideType from "../enums/OverrideType"; + +export default class OverrideField { + public Id: number = Number.MIN_VALUE; + public OverrideId: number = Number.MIN_VALUE; + public FieldName: string = ""; + public FieldValue: string = ""; + public CreatedByUserId: number = Number.MIN_VALUE; + public CreatedDatetime: Date = new Date(); + public LastModifiedByUserId?: number; + public LastModifiedDatetime?: Date; + public DeletedByUserId?: number; + public DeletedDatetime?: Date; + public IsDeleted: boolean = false; +} \ No newline at end of file diff --git a/server/enums/OverrideType.ts b/server/enums/OverrideType.ts new file mode 100644 index 0000000..d131233 --- /dev/null +++ b/server/enums/OverrideType.ts @@ -0,0 +1,31 @@ +enum OverrideType { + Unknown = 0, + AddHtml = 10, + AddLink = 20, + ReplaceText = 30, + ReplaceImage = 40, + AddStyles = 50, + ReplaceStyles = 60 +} + +// @ts-ignore +OverrideType.ToDescription = (overrideType: OverrideType) => { + switch (overrideType) { + case OverrideType.Unknown: + return "Please Select..."; + case OverrideType.AddHtml: + return "Add HTML"; + case OverrideType.AddLink: + return "Add Link"; + case OverrideType.ReplaceText: + return "Replace Text"; + case OverrideType.ReplaceImage: + return "Replace Image"; + case OverrideType.AddStyles: + return "Add Styles"; + case OverrideType.ReplaceStyles: + return "Replace Styles"; + } +}; + +export default OverrideType; \ No newline at end of file diff --git a/server/models/admin/AdminOverrideViewModel.ts b/server/models/admin/AdminOverrideViewModel.ts new file mode 100644 index 0000000..10c5781 --- /dev/null +++ b/server/models/admin/AdminOverrideViewModel.ts @@ -0,0 +1,9 @@ +import OverrideType from "../../enums/OverrideType"; + +export default interface AdminOverrideViewModel { + id?: string; + overrideTypeId: string; + name: string; + forUrl: string; + cssQuery: string; +} \ No newline at end of file diff --git a/server/models/admin/AdminOverridesViewModel.ts b/server/models/admin/AdminOverridesViewModel.ts new file mode 100644 index 0000000..68e4d83 --- /dev/null +++ b/server/models/admin/AdminOverridesViewModel.ts @@ -0,0 +1,5 @@ +import Override from "../../entities/Override"; + +export default interface AdminOverridesViewModel { + overrides: Override[] +} \ No newline at end of file diff --git a/server/objects/Database.ts b/server/objects/Database.ts index 6f93de3..b8240c5 100644 --- a/server/objects/Database.ts +++ b/server/objects/Database.ts @@ -1,7 +1,7 @@ import { Console } from "hsconsole"; import { createPool, Pool, RowDataPacket } from "mysql2"; -export type DBInDataType = string | number | null | undefined; +export type DBInDataType = string | number | Date | null | undefined; export default class Database { private connectionPool:Pool; diff --git a/server/objects/RequestCtx.ts b/server/objects/RequestCtx.ts index 5607fe3..68f0103 100644 --- a/server/objects/RequestCtx.ts +++ b/server/objects/RequestCtx.ts @@ -1,6 +1,8 @@ import { FastifyReply, FastifyRequest } from "fastify"; import SessionUser from "./SessionUser"; import { UserLevel } from "../enums/UserLevel"; +import SelectUtility from "../utilities/SelectUtility"; +import OverrideType from "../enums/OverrideType"; export default class RequestCtx { public controllerName:string; @@ -30,8 +32,14 @@ export default class RequestCtx { } // @ts-ignore inject session viewModel["session"] = this.session; + // @ts-ignore inject enums viewModel["UserLevel"] = UserLevel; + // @ts-ignore + viewModel["OverrideType"] = OverrideType; + + // @ts-ignore inject methods + viewModel["SelectUtility"] = SelectUtility; return this.res.view(`views/${this.controllerName}/${viewName}.ejs`, viewModel); } diff --git a/server/package-lock.json b/server/package-lock.json index 5988ee6..169eef9 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,29 +9,29 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@fastify/cookie": "^11.0.1", - "@fastify/formbody": "^8.0.1", - "@fastify/static": "^8.0.3", - "@fastify/view": "^10.0.1", + "@fastify/cookie": "^11.0.2", + "@fastify/formbody": "^8.0.2", + "@fastify/static": "^8.1.1", + "@fastify/view": "^11.0.0", "bufferstuff": "^1.8.0", "ejs": "^3.1.10", - "fastify": "^5.2.0", - "hsconsole": "^1.0.2", - "mysql2": "^3.11.5", + "fastify": "^5.3.2", + "hsconsole": "^1.1.0", + "mysql2": "^3.14.0", "simple-prom": "^1.0.1", "ultimate-ws": "^1.0.13" }, "devDependencies": { "@types/ejs": "^3.1.5", - "@types/node": "^22.10.2", + "@types/node": "^22.14.0", "@vercel/ncc": "^0.38.3", - "check-outdated": "^2.12.0", + "check-outdated": "^2.13.0", "node": "^22.12.0", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", - "terser": "^5.37.0", + "terser": "^5.39.0", "ts-node": "^10.9.2", - "typescript": "^5.7.2" + "typescript": "^5.8.3" } }, "node_modules/@cspotcode/source-map-support": { @@ -64,9 +64,19 @@ } }, "node_modules/@fastify/cookie": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.1.tgz", - "integrity": "sha512-n1Ooz4bgQ5LcOlJQboWPfsMNxIrGV0SgU85UkctdpTlCQE0mtA3rlspOPUdqk9ubiiZn053ucnia4DjTquI4/g==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.0.2.tgz", + "integrity": "sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { "cookie": "^1.0.0", @@ -98,15 +108,31 @@ } }, "node_modules/@fastify/formbody": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.1.tgz", - "integrity": "sha512-LPrcadSIK8TrQk510Zdj56fnw7cyHq0/PW0YHGGM8ycGL4X7XAex+FKcwpzB4i5lF9eykc71a4EtcO9AEoByqw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-8.0.2.tgz", + "integrity": "sha512-84v5J2KrkXzjgBpYnaNRPqwgMsmY7ZDjuj0YVuMR3NXCJRCgKEZy/taSP1wUYGn0onfxJpLyRGDLa+NMaDJtnA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { "fast-querystring": "^1.1.2", "fastify-plugin": "^5.0.0" } }, + "node_modules/@fastify/forwarded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", + "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", + "license": "MIT" + }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -116,6 +142,16 @@ "fast-deep-equal": "^3.1.3" } }, + "node_modules/@fastify/proxy-addr": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", + "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@fastify/send": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-3.3.0.tgz", @@ -130,9 +166,19 @@ } }, "node_modules/@fastify/static": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.0.3.tgz", - "integrity": "sha512-GHSoOVDIxEYEeVR5l044bRCuAKDErD/+9VE+Z9fnaTRr+DDz0Avrm4kKai1mHbPx6C0U7BVNthjd/gcMquZZUA==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.1.1.tgz", + "integrity": "sha512-TW9eyVHJLytZNpBlSIqd0bl1giJkEaRaPZG+5AT3L/OBKq9U8D7g/OYmc2NPQZnzPURGhMt3IAWuyVkvd2nOkQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { "@fastify/accept-negotiator": "^2.0.0", @@ -144,9 +190,19 @@ } }, "node_modules/@fastify/view": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@fastify/view/-/view-10.0.1.tgz", - "integrity": "sha512-rXtBN0oVDmoRZAS7lelrCIahf+qFtlMOOas8VPdA7JvrJ9ChcF7e36pIUPU0Vbs3KmHxESUb7XatavUZEe/k5Q==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@fastify/view/-/view-11.0.0.tgz", + "integrity": "sha512-uMNwZoY88uUMP6hidJYHT0/7bXzLrWgo/X5kjKdKPi3bj69kSkuNKmcG5KEU2sDF91n+IiLAnqx3dkldFfz5hQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "MIT", "dependencies": { "fastify-plugin": "^5.0.0", @@ -288,13 +344,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.14.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", + "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@vercel/ncc": { @@ -642,10 +698,11 @@ } }, "node_modules/check-outdated": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/check-outdated/-/check-outdated-2.12.0.tgz", - "integrity": "sha512-kWThJFiqxAE09XSNJLLD4hWNvLhWdxFLKxOHhxB+XhGlZGyeELXP8V6R/dRrZ5vbjmp9VmoTYe0vp6egftKz7Q==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/check-outdated/-/check-outdated-2.13.0.tgz", + "integrity": "sha512-TXmXp/IcnV2WnF8B+LBuFLXy5zNAlv46tdN3O/2ag3Hn+CZrUzIGI/hN0EdXMq9liiD8aWJDI5/ZBqYrqgxbSw==", "dev": true, + "license": "MIT", "bin": { "check-outdated": "check-outdated.js" }, @@ -1136,9 +1193,9 @@ "license": "MIT" }, "node_modules/fastify": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.2.0.tgz", - "integrity": "sha512-3s+Qt5S14Eq5dCpnE0FxTp3z4xKChI83ZnMv+k0FwX+VUoZrgCFoLAxpfdi/vT4y6Mk+g7aAMt9pgXDoZmkefQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.3.2.tgz", + "integrity": "sha512-AIPqBgtqBAwkOkrnwesEE+dOyU30dQ4kh7udxeGVR05CRGwubZx+p2H8P0C4cRnQT0+EPK4VGea2DTL2RtWttg==", "funding": [ { "type": "github", @@ -1154,16 +1211,16 @@ "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", - "process-warning": "^4.0.0", - "proxy-addr": "^2.0.7", + "process-warning": "^5.0.0", "rfdc": "^1.3.1", - "secure-json-parse": "^3.0.1", + "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } @@ -1174,6 +1231,22 @@ "integrity": "sha512-0725fmH/yYi8ugsjszLci+lLnGBK6cG+WSxM7edY2OXJEU7gr2JiGBoieL2h9mhTych1vFsEfXsAsGGDJ/Rd5w==", "license": "MIT" }, + "node_modules/fastify/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -1321,14 +1394,6 @@ "node": ">= 8" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1609,9 +1674,10 @@ "dev": true }, "node_modules/hsconsole": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hsconsole/-/hsconsole-1.0.2.tgz", - "integrity": "sha512-st+jaSpNw3uoIhE5vl2lVN8Op8yQF2FyLRdBG68s8vqjduJdKUGtoEXd8Zxe6du1zzpFHHRcU3zJbAq8BOmYQA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hsconsole/-/hsconsole-1.1.0.tgz", + "integrity": "sha512-nQtnapTLf/d090AloKJkVbf15yXNaISYYHC21cwOLClF7hlJs2rlHF3JLaltspK/O2uhz6WcRMsE+2Yn6D8UEw==", + "license": "MIT", "dependencies": { "dyetty": "^1.0.1" } @@ -1690,11 +1756,12 @@ } }, "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/is-array-buffer": { @@ -2212,9 +2279,9 @@ "dev": true }, "node_modules/mysql2": { - "version": "3.11.5", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.5.tgz", - "integrity": "sha512-0XFu8rUmFN9vC0ME36iBvCUObftiMHItrYFhlCRvFWbLgpNqtC4Br/NmZX1HNCszxT0GGy5QtP+k3Q3eCJPaYA==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.0.tgz", + "integrity": "sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==", "license": "MIT", "dependencies": { "aws-ssl-profiles": "^1.1.1", @@ -2590,18 +2657,6 @@ "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw==", "license": "MIT" }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -2813,9 +2868,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/secure-json-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.1.tgz", - "integrity": "sha512-9QR7G96th4QJ2+dJwvZB+JoXyt8PN+DbEjOr6kL2/JU4KH8Eb2sFdU+gt8EDdzWDWoWH0uocDdfCoFzdVSixUA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz", + "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/semver": { @@ -3254,9 +3319,9 @@ } }, "node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "version": "5.39.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", + "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3446,9 +3511,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3492,9 +3557,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/server/package.json b/server/package.json index 760c176..f075b5d 100644 --- a/server/package.json +++ b/server/package.json @@ -12,28 +12,28 @@ "author": "tgpholly", "license": "MIT", "dependencies": { - "@fastify/cookie": "^11.0.1", - "@fastify/formbody": "^8.0.1", - "@fastify/static": "^8.0.3", - "@fastify/view": "^10.0.1", + "@fastify/cookie": "^11.0.2", + "@fastify/formbody": "^8.0.2", + "@fastify/static": "^8.1.1", + "@fastify/view": "^11.0.0", "bufferstuff": "^1.8.0", "ejs": "^3.1.10", - "fastify": "^5.2.0", - "hsconsole": "^1.0.2", - "mysql2": "^3.11.5", + "fastify": "^5.3.2", + "hsconsole": "^1.1.0", + "mysql2": "^3.14.0", "simple-prom": "^1.0.1", "ultimate-ws": "^1.0.13" }, "devDependencies": { "@types/ejs": "^3.1.5", - "@types/node": "^22.10.2", + "@types/node": "^22.14.0", "@vercel/ncc": "^0.38.3", - "check-outdated": "^2.12.0", + "check-outdated": "^2.13.0", "node": "^22.12.0", "nodemon": "^3.1.9", "npm-run-all": "^4.1.5", - "terser": "^5.37.0", + "terser": "^5.39.0", "ts-node": "^10.9.2", - "typescript": "^5.7.2" + "typescript": "^5.8.3" } } diff --git a/server/repos/OverrideFieldRepo.ts b/server/repos/OverrideFieldRepo.ts new file mode 100644 index 0000000..4901d01 --- /dev/null +++ b/server/repos/OverrideFieldRepo.ts @@ -0,0 +1,69 @@ +import Database from "../objects/Database"; +import OverrideField from "../entities/OverrideField"; + +export default class OverrideFieldRepo { + public static async selectAll() { + const dbOverride = await Database.Instance.query("SELECT * FROM OverrideField WHERE IsDeleted = 0"); + const users = new Array(); + + for (const row of dbOverride) { + const override = new OverrideField(); + populateOverrideFromDB(override, row); + users.push(override); + } + + return users; + } + + public static async selectById(id:number) { + const dbOverride = await Database.Instance.query("SELECT * FROM OverrideField WHERE Id = ? LIMIT 1", [id]); + if (dbOverride == null || dbOverride.length === 0) { + return null; + } else { + const override = new OverrideField(); + populateOverrideFromDB(override, dbOverride[0]); + return override; + } + } + + public static async selectByOverrideId(id: number) { + const dbOverride = await Database.Instance.query("SELECT * FROM OverrideField WHERE IsDeleted = 0 AND OverrideId = ?", [id]); + const users = new Array(); + + for (const row of dbOverride) { + const override = new OverrideField(); + populateOverrideFromDB(override, row); + users.push(override); + } + + return users; + } + + public static async insertUpdate(overrideField:OverrideField) { + if (overrideField.Id === Number.MIN_VALUE) { + overrideField.Id = (await Database.Instance.query("INSERT OverrideField (OverrideId, FieldName, FieldValue, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ + overrideField.OverrideId, overrideField.FieldName, overrideField.FieldValue, overrideField.CreatedByUserId, overrideField.CreatedDatetime.getTime(), overrideField.LastModifiedByUserId ?? null, overrideField.LastModifiedDatetime ?? null, overrideField.DeletedByUserId ?? null, overrideField.DeletedDatetime ?? null, Number(overrideField.IsDeleted) + ]))[0]["Id"]; + } else { + await Database.Instance.query(`UPDATE OverrideField SET OverrideId = ?, FieldName = ?, FieldValue = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ + overrideField.OverrideId, overrideField.FieldName, overrideField.FieldValue, overrideField.CreatedByUserId, overrideField.CreatedDatetime.getTime(), overrideField.LastModifiedByUserId ?? null, overrideField.LastModifiedDatetime ?? null, overrideField.DeletedByUserId ?? null, overrideField.DeletedDatetime ?? null, Number(overrideField.IsDeleted), overrideField.Id + ]); + } + + return overrideField; + } +} + +function populateOverrideFromDB(overrideField:OverrideField, dbOverrideField:any) { + overrideField.Id = dbOverrideField.Id; + overrideField.OverrideId = dbOverrideField.OverrideId; + overrideField.FieldName = dbOverrideField.FieldName; + overrideField.FieldValue = dbOverrideField.FieldValue; + overrideField.CreatedByUserId = dbOverrideField.CreatedByUserId; + overrideField.CreatedDatetime = dbOverrideField.CreatedDatetime; + overrideField.LastModifiedByUserId = dbOverrideField.LastModifiedByUserId; + overrideField.LastModifiedDatetime = dbOverrideField.LastModifiedDatetime; + overrideField.DeletedByUserId = dbOverrideField.DeletedByUserId; + overrideField.DeletedDatetime = dbOverrideField.DeletedDatetime; + overrideField.IsDeleted = dbOverrideField.IsDeleted[0] === 1; +} \ No newline at end of file diff --git a/server/repos/OverrideRepo.ts b/server/repos/OverrideRepo.ts new file mode 100644 index 0000000..72126ce --- /dev/null +++ b/server/repos/OverrideRepo.ts @@ -0,0 +1,57 @@ +import Database from "../objects/Database"; +import Override from "../entities/Override"; + +export default class OverrideRepo { + public static async selectAll() { + const dbOverride = await Database.Instance.query("SELECT * FROM Override WHERE IsDeleted = 0"); + const users = new Array(); + + for (const row of dbOverride) { + const override = new Override(); + populateOverrideFromDB(override, row); + users.push(override); + } + + return users; + } + + public static async selectById(id:number) { + const dbOverride = await Database.Instance.query("SELECT * FROM Override WHERE Id = ? LIMIT 1", [id]); + if (dbOverride == null || dbOverride.length === 0) { + return null; + } else { + const override = new Override(); + populateOverrideFromDB(override, dbOverride[0]); + return override; + } + } + + public static async insertUpdate(override:Override) { + if (override.Id === Number.MIN_VALUE) { + override.Id = (await Database.Instance.query("INSERT Override (OverrideTypeId, Name, ForURL, CSSQuery, CreatedByUserId, CreatedDatetime, LastModifiedByUserId, LastModifiedDatetime, DeletedByUserId, DeletedDatetime, IsDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING Id;", [ + override.OverrideType, override.Name, override.ForURL, override.CSSQuery, override.CreatedByUserId, override.CreatedDatetime.getTime(), override.LastModifiedByUserId ?? null, override.LastModifiedDatetime ?? null, override.DeletedByUserId ?? null, override.DeletedDatetime ?? null, Number(override.IsDeleted) + ]))[0]["Id"]; + } else { + await Database.Instance.query(`UPDATE Override SET OverrideTypeId = ?, Name = ?, ForURL = ?, CSSQuery = ?, CreatedByUserId = ?, CreatedDatetime = ?, LastModifiedByUserId = ?, LastModifiedDatetime = ?, DeletedByUserId = ?, DeletedDatetime = ?, IsDeleted = ? WHERE Id = ?`, [ + override.OverrideType, override.Name, override.ForURL, override.CSSQuery, override.CreatedByUserId, override.CreatedDatetime.getTime(), override.LastModifiedByUserId ?? null, override.LastModifiedDatetime ?? null, override.DeletedByUserId ?? null, override.DeletedDatetime ?? null, Number(override.IsDeleted), override.Id + ]); + } + + return override; + } +} + +function populateOverrideFromDB(override:Override, dbOverride:any) { + override.Id = dbOverride.Id; + override.OverrideType = dbOverride.OverrideTypeId; + override.Name = dbOverride.Name; + override.ForURL = dbOverride.ForURL; + override.CSSQuery = dbOverride.CSSQuery; + override.CreatedByUserId = dbOverride.CreatedByUserId; + override.CreatedDatetime = dbOverride.CreatedDatetime; + override.LastModifiedByUserId = dbOverride.LastModifiedByUserId; + override.LastModifiedDatetime = dbOverride.LastModifiedDatetime; + override.DeletedByUserId = dbOverride.DeletedByUserId; + override.DeletedDatetime = dbOverride.DeletedDatetime; + override.IsDeleted = dbOverride.IsDeleted[0] === 1; +} \ No newline at end of file diff --git a/server/services/OverrideService.ts b/server/services/OverrideService.ts new file mode 100644 index 0000000..4685087 --- /dev/null +++ b/server/services/OverrideService.ts @@ -0,0 +1,95 @@ +import { Console } from "hsconsole"; +import OverrideRepo from "../repos/OverrideRepo"; +import Override from "../entities/Override"; +import OverrideType from "../enums/OverrideType"; +import OverrideFieldRepo from "../repos/OverrideFieldRepo"; +import OverrideField from "../entities/OverrideField"; + +export default abstract class OverrideService { + public static async GetOverride(id: number) { + try { + return await OverrideRepo.selectById(id); + } catch (e) { + Console.printError(`MultiProbe server service error:\n${e}`); + throw e; + } + } + + public static async GetOverrides() { + try { + return await OverrideRepo.selectAll(); + } catch (e) { + Console.printError(`MultiProbe server service error:\n${e}`); + throw e; + } + } + + public static async SaveOverride(currentUserId:number, id:number | undefined, overrideType: OverrideType, name: string, forUrl: string, cssQuery: string) { + try { + let override = id ? await OverrideRepo.selectById(id) : null; + if (override === null) { + override = new Override(); + override.CreatedByUserId = currentUserId; + override.CreatedDatetime = new Date(); + } else { + override.LastModifiedByUserId = currentUserId; + override.LastModifiedDatetime = new Date(); + } + + override.OverrideType = overrideType; + override.Name = name; + override.ForURL = forUrl; + override.CSSQuery = cssQuery; + + override = await OverrideRepo.insertUpdate(override); + + return override; + } catch (e) { + Console.printError(`MultiProbe server service error:\n${e}`); + throw e; + } + } + + public static async GetOverrideField(id: number) { + try { + return await OverrideFieldRepo.selectById(id); + } catch (e) { + Console.printError(`MultiProbe server service error:\n${e}`); + throw e; + } + } + + public static async GetOverrideFields(overrideId: number) { + try { + return await OverrideFieldRepo.selectById(overrideId); + } catch (e) { + Console.printError(`MultiProbe server service error:\n${e}`); + throw e; + } + } + + public static async SaveOverrideField(currentUserId:number, id:number | undefined, overrideId: number, fieldName: string, fieldValue: string) { + try { + let overrideField = id ? await OverrideFieldRepo.selectById(id) : null; + if (overrideField === null) { + overrideField = new OverrideField(); + overrideField.CreatedByUserId = currentUserId; + overrideField.CreatedDatetime = new Date(); + } else { + overrideField.LastModifiedByUserId = currentUserId; + overrideField.LastModifiedDatetime = new Date(); + } + + overrideField.OverrideId = overrideId; + overrideField.FieldName = fieldName; + overrideField.FieldValue = fieldValue; + + overrideField = await OverrideFieldRepo.insertUpdate(overrideField); + + return overrideField; + } catch (e) { + Console.printError(`MultiProbe server service error:\n${e}`); + throw e; + } + } +} \ No newline at end of file diff --git a/server/utilities/SelectUtility.ts b/server/utilities/SelectUtility.ts new file mode 100644 index 0000000..684e9c7 --- /dev/null +++ b/server/utilities/SelectUtility.ts @@ -0,0 +1,17 @@ +import OverrideType from "../enums/OverrideType"; + +export default abstract class SelectUtility { + public static EnumToSelectList(enumToList: any, selected?: any) { + const keys = Object.keys(enumToList); + let output: string = ""; + for (const key of keys) { + const keyN = parseInt(key); + if (!isNaN(keyN)) { + // @ts-ignore + output += ``; + } + } + + return output; + } +} \ No newline at end of file diff --git a/server/views/admin/_addhtml.ejs b/server/views/admin/_addhtml.ejs new file mode 100644 index 0000000..0e31803 --- /dev/null +++ b/server/views/admin/_addhtml.ejs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/server/views/admin/index.ejs b/server/views/admin/index.ejs index 9ca2bfe..860e601 100644 --- a/server/views/admin/index.ejs +++ b/server/views/admin/index.ejs @@ -49,6 +49,7 @@ Manage Parties Manage Badges Manage Unlocked Badges + Manage Overrides Web Sessions Websocket Sessions diff --git a/server/views/admin/override.ejs b/server/views/admin/override.ejs new file mode 100644 index 0000000..fba7b4f --- /dev/null +++ b/server/views/admin/override.ejs @@ -0,0 +1,59 @@ +<%- include("../base/header", { title: typeof(id) === "undefined" ? "Add Override" : `Edit ${name}`, userId: session.userId, isAdmin: true }) %> + + + +
+
+

<%= typeof(id) === "undefined" ? "Add Override" : `Edit ${name}` %>

+
+
+ +
+ " /> + +
+
+ + " required maxlength="255" /> +
+
+
+
+ + +
+
+
+
+ + " required /> +
+
+ + <% if (overrideType === OverrideType.AddHtml) { %> + <%- include("./_addhtml") %> + <% } else { %> +
+ <% } %> + +
+
+ + Cancel +
+
+
+ +<%- include("../base/footer", { apiKey: session.apiKey, username: session.username }) %> \ No newline at end of file diff --git a/server/views/admin/overrides.ejs b/server/views/admin/overrides.ejs new file mode 100644 index 0000000..e1186b8 --- /dev/null +++ b/server/views/admin/overrides.ejs @@ -0,0 +1,45 @@ +<%- include("../base/header", { title: "Badge Management", userId: session.userId, isAdmin: true }) %> + +
+
+ +
+
+ +
+
+

Override Management

+
+ +
+ +
+
+ + + + + + + <% for (const override of overrides) { %> + + + + + <% } %> + +
Name 
<%= override.Name %> + Edit + Delete +
+
+
+ +<%- include("../base/footer", { apiKey: session.apiKey, username: session.username }) %> \ No newline at end of file