diff --git a/core/webroutes/banTemplates/getBanTemplates.ts b/core/webroutes/banTemplates/getBanTemplates.ts index 8b733c72a..8f79dffc5 100644 --- a/core/webroutes/banTemplates/getBanTemplates.ts +++ b/core/webroutes/banTemplates/getBanTemplates.ts @@ -26,7 +26,8 @@ export const getBanTemplatesImpl = (ctx: AuthedCtx): BanTemplatesDataType[] => { return []; } - const filteredTemplates = (savedTemplates as unknown[]).filter((template): template is BanTemplatesDataType => { + //Filtering valid & unique templates + const filteredTemplates = (savedTemplates as unknown[]).filter((template, index): template is BanTemplatesDataType => { const isValid = BanTemplatesDataSchema.safeParse(template); if (!isValid.success) { console.error( @@ -35,6 +36,14 @@ export const getBanTemplatesImpl = (ctx: AuthedCtx): BanTemplatesDataType[] => { ); return false; } + const isUnique = savedTemplates.findIndex((t) => t.id === isValid.data.id) === index; + if (!isUnique) { + console.error( + 'Duplicate ban template id:', + isValid.data.id + ); + return false; + } return true; }); diff --git a/core/webroutes/banTemplates/saveBanTemplates.ts b/core/webroutes/banTemplates/saveBanTemplates.ts index a5827d3e5..f3d44fe38 100644 --- a/core/webroutes/banTemplates/saveBanTemplates.ts +++ b/core/webroutes/banTemplates/saveBanTemplates.ts @@ -35,9 +35,14 @@ export default async function SaveBanTemplates(ctx: AuthedCtx) { } const banTemplates = schemaRes.data; + //Dropping duplicates + const filteredBanTemplates = banTemplates.filter((template, index) => { + return banTemplates.findIndex((t) => t.id === template.id) === index; + }); + //Preparing & saving config try { - ctx.txAdmin.configVault.saveProfile('banTemplates', banTemplates); + ctx.txAdmin.configVault.saveProfile('banTemplates', filteredBanTemplates); } catch (error) { console.warn(`[${ctx.admin.name}] Error changing banTemplates settings.`); console.verbose.dir(error); diff --git a/core/webroutes/banTemplates/utils.ts b/core/webroutes/banTemplates/utils.ts index aa11018af..b02348a31 100644 --- a/core/webroutes/banTemplates/utils.ts +++ b/core/webroutes/banTemplates/utils.ts @@ -12,8 +12,8 @@ export type BanDurationType = z.infer; export const BanTemplatesDataSchema = z.object({ - id: z.string().min(1), - reason: z.string().min(3), + id: z.string().length(22), //nanoid fixed at 22 chars + reason: z.string().min(3).max(2048), //should be way less, but just in case duration: BanDurationTypeSchema, }); export type BanTemplatesDataType = z.infer; diff --git a/docs/dev_notes.md b/docs/dev_notes.md index 869350c03..cdce6f8fc 100644 --- a/docs/dev_notes.md +++ b/docs/dev_notes.md @@ -15,16 +15,16 @@ - [ ] remove all "blur" as that is slow as hell for browsers with hw acceleration disabled ## Highlights -- [ ] pre-configured ban/warn reasons with new perm to lock admins to only use them? +- [x] pre-configured ban/warn reasons with new perm to lock admins to only use them? - [x] apply new ban scheme to the web player modal - [x] apply new ban scheme to the NUI - - [ ] checklist: + - [x] checklist: - [x] light mode - [x] multiline - [x] mobile - - [ ] dialog input sanitization - - [ ] better random id (no random id? stable-hash?) - - [ ] settings enforce unique id + - [x] dialog input sanitization + - [x] better random id (no random id? stable-hash?) + - [x] settings enforce unique id - [ ] NEW PAGE: Dashboard - [ ] new performance chart - [ ] number callouts from legacy players page diff --git a/package-lock.json b/package-lock.json index 45bcb8e73..93a59738d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14650,6 +14650,7 @@ "jotai": "^2.8.0", "jotai-effect": "^1.0.0", "lucide-react": "^0.368.0", + "nanoid": "^5.0.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", @@ -15023,6 +15024,23 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "panel/node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "panel/node_modules/rollup": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz", diff --git a/panel/package.json b/panel/package.json index 32c18c456..8892999fc 100644 --- a/panel/package.json +++ b/panel/package.json @@ -47,6 +47,7 @@ "jotai": "^2.8.0", "jotai-effect": "^1.0.0", "lucide-react": "^0.368.0", + "nanoid": "^5.0.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", diff --git a/panel/src/layout/PlayerModal/PlayerBanTab.tsx b/panel/src/layout/PlayerModal/PlayerBanTab.tsx index 5c51f65c3..5a4169938 100644 --- a/panel/src/layout/PlayerModal/PlayerBanTab.tsx +++ b/panel/src/layout/PlayerModal/PlayerBanTab.tsx @@ -101,7 +101,11 @@ export default function PlayerBanTab({ playerRef, banTemplates }: PlayerBanTabPr ? template.reason.slice(0, maxReasonSize - 3) + '...' : template.reason; return ( - + {duration} {reason} @@ -117,6 +121,14 @@ export default function PlayerBanTab({ playerRef, banTemplates }: PlayerBanTabPr Reason
+ - + {!banTemplates.length ? (
You do not have any template configured.
@@ -160,14 +172,6 @@ export default function PlayerBanTab({ playerRef, banTemplates }: PlayerBanTabPr ) : null} -
diff --git a/panel/src/pages/BanTemplates/BanTemplatesInputDialog.tsx b/panel/src/pages/BanTemplates/BanTemplatesInputDialog.tsx index 59c911b35..6eaae6d64 100644 --- a/panel/src/pages/BanTemplates/BanTemplatesInputDialog.tsx +++ b/panel/src/pages/BanTemplates/BanTemplatesInputDialog.tsx @@ -8,6 +8,7 @@ import { AutosizeTextAreaRef, AutosizeTextarea } from "@/components/ui/autosize- import { BanTemplatesInputData } from "./BanTemplatesPage"; import { BanDurationType } from "@shared/otherTypes"; import { banDurationToString } from "@/lib/utils"; +import { txToast } from "@/components/TxToaster"; //Default dropdown options const dropdownOptions = [ @@ -62,11 +63,20 @@ export default function BanTemplatesInputDialog({ e.preventDefault(); const form = e.currentTarget; const id = reasonData?.id || null; - const reason = form.reason.value; + const reason = form.reason.value.trim(); + form.reason.value = reason; //just to make sure the field is also trimmed + if (reason.length < 3) { + form.reason.focus(); + return txToast.warning('Reason must be at least 3 characters long'); + } let duration: BanDurationType; if (selectedDuration === 'permanent') { duration = 'permanent'; } else if (selectedDuration === 'custom') { + if (form.durationMultiplier.value <= 0) { + form.durationMultiplier.focus(); + return txToast.warning('Custom duration must be a positive number'); + } duration = { value: parseInt(form.durationMultiplier.value), unit: customUnits as 'hours' | 'days' | 'weeks' | 'months' @@ -100,6 +110,7 @@ export default function BanTemplatesInputDialog({ defaultValue={initialReason} ref={reasonRef} maxHeight={160} + minLength={3} autoFocus required onChangeCapture={(e) => { @@ -142,6 +153,8 @@ export default function BanTemplatesInputDialog({ defaultValue={initialCustomValue} disabled={selectedDuration !== 'custom'} ref={customMultiplierRef} + min={1} + max={99} required />