Fix TypeScript errors and implement furniture import ID integrity with 18+ age verification
- Add termsAccepted and ageVerified columns to User table - Update register schema with new boolean fields - Fix register form age verification checkbox (th -> t) - Fix furni-import spriteId declaration order - Fix batch route variable naming (id -> spriteId) - Fix catalog-audit import path and ensure correct types - Hardened import with per-item id conflict checks - Added audit option for FurnitureData.json spriteId conflicts - Updated tagline to include Leeftijdsvereiste: 18+
This commit is contained in:
1 parent
e5ec3c1f06
commit
e76c530e4f
13 files changed
+267
-104
No files matched your search
@@ -106,6 +106,77 @@ async function acquireDiskLock(lockPath: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Build a map of sprite id → classname from both sections of the furnidata.
|
||||
* If multiple entries share the same id, the first encountered classname wins. */
|
||||
function furniDataIdOwners(
|
||||
data: Record<string, { furnitype: Array<Record<string, unknown>> }>,
|
||||
): Map<number, string> {
|
||||
const owners = new Map<number, string>();
|
||||
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||
for (const e of data[section]?.furnitype ?? []) {
|
||||
const id = Number(e?.id);
|
||||
if (!Number.isFinite(id) || id <= 0) continue;
|
||||
const classname = typeof e?.classname === "string" ? e.classname : "";
|
||||
if (!classname) continue;
|
||||
if (!owners.has(id)) owners.set(id, classname);
|
||||
}
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
|
||||
/** Assert that no entry in `entries` has a spriteId already used by a different classname
|
||||
* in the existing furnidata or within this batch. Throws if a conflict is found. */
|
||||
function assertNoFurniDataIdConflicts(
|
||||
existingOwners: Map<number, string>,
|
||||
entries: Array<{ entry: Record<string, unknown>; itemType: string }>,
|
||||
): void {
|
||||
const conflicts: string[] = [];
|
||||
for (const { entry } of entries) {
|
||||
const id = Number(entry?.id);
|
||||
if (!Number.isFinite(id) || id <= 0) continue;
|
||||
const classname =
|
||||
typeof entry?.classname === "string" ? entry.classname : "";
|
||||
const existing = existingOwners.get(id);
|
||||
if (existing !== undefined && existing !== classname) {
|
||||
conflicts.push(
|
||||
`spriteId ${id} already used by "${existing}" in FurnitureData.json ` +
|
||||
`(attempted "${classname}")`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// reserve the id within this batch so a second entry with the same id + different classname
|
||||
// is also caught (even though the file hasn't been written yet).
|
||||
existingOwners.set(id, classname);
|
||||
}
|
||||
if (conflicts.length > 0) {
|
||||
throw new Error(
|
||||
`FurnitureData spriteId conflicts: ${conflicts.join("; ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if a spriteId is already used by a different classname in the local
|
||||
* FurnitureData.json. Returns the existing classname if there's a conflict,
|
||||
* or null if the spriteId is free or the file couldn't be read. */
|
||||
export async function findFurniDataIdConflict(
|
||||
spriteId: number,
|
||||
classname: string,
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const furniData = await readFurniData();
|
||||
const owners = furniDataIdOwners(
|
||||
furniData as Record<
|
||||
string,
|
||||
{ furnitype: Array<Record<string, unknown>> }
|
||||
>,
|
||||
);
|
||||
const existing = owners.get(spriteId);
|
||||
return existing !== undefined && existing !== classname ? existing : null;
|
||||
} catch {
|
||||
return null; // if file missing/unreadable, treat as no conflict
|
||||
}
|
||||
}
|
||||
|
||||
export async function withFurniDataLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||
let release!: () => void;
|
||||
const acquired = new Promise<void>((r) => {
|
||||
@@ -228,6 +299,11 @@ export async function appendFurniEntry(
|
||||
string,
|
||||
{ furnitype: Array<Record<string, unknown>> }
|
||||
>;
|
||||
|
||||
// Guard: ensure the spriteId is not already used by a different classname in the file.
|
||||
const existingOwners = furniDataIdOwners(furniData);
|
||||
assertNoFurniDataIdConflicts(existingOwners, [{ entry, itemType }]);
|
||||
|
||||
if (itemType === "i") {
|
||||
if (!furniData.wallitemtypes) furniData.wallitemtypes = { furnitype: [] };
|
||||
upsertEntryInSection(furniData.wallitemtypes, entry);
|
||||
@@ -331,6 +407,17 @@ export async function appendFurniEntriesBatch(
|
||||
string,
|
||||
{ furnitype: unknown[] }
|
||||
>;
|
||||
|
||||
// Guard: ensure no spriteId conflicts with existing file entries
|
||||
// or within this batch (same id + different classname).
|
||||
const existingOwners = furniDataIdOwners(
|
||||
furniData as Record<
|
||||
string,
|
||||
{ furnitype: Array<Record<string, unknown>> }
|
||||
>,
|
||||
);
|
||||
assertNoFurniDataIdConflicts(existingOwners, entries);
|
||||
|
||||
for (const { entry, itemType } of entries) {
|
||||
if (itemType === "i") {
|
||||
if (!furniData.wallitemtypes)
|
||||
|
||||
Reference in new issue
Block a user