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
@@ -52,8 +52,6 @@ const nextConfig: NextConfig = {
|
|||||||
ignoreBuildErrors: false,
|
ignoreBuildErrors: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Enable React Compiler for automatic memoization
|
|
||||||
|
|
||||||
// Compress responses with gzip/brotli
|
// Compress responses with gzip/brotli
|
||||||
compress: true,
|
compress: true,
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -7,7 +7,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "[email protected]",
|
"packageManager": "[email protected]",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbo",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "biome check .",
|
"lint": "biome check .",
|
||||||
@@ -30,8 +30,8 @@
|
|||||||
"@dnd-kit/core": "6.3.1",
|
"@dnd-kit/core": "6.3.1",
|
||||||
"@dnd-kit/sortable": "10.0.0",
|
"@dnd-kit/sortable": "10.0.0",
|
||||||
"@dnd-kit/utilities": "3.2.2",
|
"@dnd-kit/utilities": "3.2.2",
|
||||||
"@hookform/resolvers": "5.7.1",
|
"@hookform/resolvers": "5.8.0",
|
||||||
"@next/bundle-analyzer": "16.3.0",
|
"@next/bundle-analyzer": "16.3.1",
|
||||||
"@tanstack/react-virtual": "3.14.9",
|
"@tanstack/react-virtual": "3.14.9",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"motion": "13.1.0",
|
"motion": "13.1.0",
|
||||||
"music-metadata": "11.14.0",
|
"music-metadata": "11.14.0",
|
||||||
"mysql2": "3.23.3",
|
"mysql2": "3.23.3",
|
||||||
"next": "16.3.0",
|
"next": "16.3.1",
|
||||||
"next-auth": "5.0.0-beta.32",
|
"next-auth": "5.0.0-beta.32",
|
||||||
"next-intl": "4.13.6",
|
"next-intl": "4.13.6",
|
||||||
"otplib": "13.4.1",
|
"otplib": "13.4.1",
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"react": "19.2.8",
|
"react": "19.2.8",
|
||||||
"react-dom": "19.2.8",
|
"react-dom": "19.2.8",
|
||||||
"react-hook-form": "7.85.0",
|
"react-hook-form": "7.85.0",
|
||||||
"resend": "6.19.0",
|
"resend": "6.20.0",
|
||||||
"server-only": "0.0.1",
|
"server-only": "0.0.1",
|
||||||
"sharp": "0.35.3",
|
"sharp": "0.35.3",
|
||||||
"sonner": "2.0.8",
|
"sonner": "2.0.8",
|
||||||
|
|||||||
Generated
+70
-69
@@ -35,11 +35,11 @@ importers:
|
|||||||
specifier: 3.2.2
|
specifier: 3.2.2
|
||||||
version: 3.2.2([email protected])
|
version: 3.2.2([email protected])
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: 5.7.1
|
specifier: 5.8.0
|
||||||
version: 5.7.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
|
version: 5.8.0(@standard-schema/[email protected])([email protected]([email protected]))([email protected])
|
||||||
'@next/bundle-analyzer':
|
'@next/bundle-analyzer':
|
||||||
specifier: 16.3.0
|
specifier: 16.3.1
|
||||||
version: 16.3.0
|
version: 16.3.1
|
||||||
'@tanstack/react-virtual':
|
'@tanstack/react-virtual':
|
||||||
specifier: 3.14.9
|
specifier: 3.14.9
|
||||||
version: 3.14.9([email protected]([email protected]))([email protected])
|
version: 3.14.9([email protected]([email protected]))([email protected])
|
||||||
@@ -95,14 +95,14 @@ importers:
|
|||||||
specifier: 3.23.3
|
specifier: 3.23.3
|
||||||
version: 3.23.3(@types/[email protected])
|
version: 3.23.3(@types/[email protected])
|
||||||
next:
|
next:
|
||||||
specifier: 16.3.0
|
specifier: 16.3.1
|
||||||
version: 16.3.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
version: 16.3.1(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||||
next-auth:
|
next-auth:
|
||||||
specifier: 5.0.0-beta.32
|
specifier: 5.0.0-beta.32
|
||||||
version: 5.0.0-beta.32([email protected].0(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])
|
version: 5.0.0-beta.32([email protected].1(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])
|
||||||
next-intl:
|
next-intl:
|
||||||
specifier: 4.13.6
|
specifier: 4.13.6
|
||||||
version: 4.13.6([email protected].0(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected])
|
version: 4.13.6(@swc/[email protected])([email protected].1(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected])
|
||||||
otplib:
|
otplib:
|
||||||
specifier: 13.4.1
|
specifier: 13.4.1
|
||||||
version: 13.4.1
|
version: 13.4.1
|
||||||
@@ -119,8 +119,8 @@ importers:
|
|||||||
specifier: 7.85.0
|
specifier: 7.85.0
|
||||||
version: 7.85.0([email protected])
|
version: 7.85.0([email protected])
|
||||||
resend:
|
resend:
|
||||||
specifier: 6.19.0
|
specifier: 6.20.0
|
||||||
version: 6.19.0
|
version: 6.20.0
|
||||||
server-only:
|
server-only:
|
||||||
specifier: 0.0.1
|
specifier: 0.0.1
|
||||||
version: 0.0.1
|
version: 0.0.1
|
||||||
@@ -559,8 +559,8 @@ packages:
|
|||||||
'@formatjs/[email protected]':
|
'@formatjs/[email protected]':
|
||||||
resolution: {integrity: sha512-kHEAFOkeJSPNi7c5PaKaRjxcBrJwzzt81ifUu+8uve1EDW/VJl83KsxmqgqNZLzcFEhSliZGvx3+pk/RH0IOmg==}
|
resolution: {integrity: sha512-kHEAFOkeJSPNi7c5PaKaRjxcBrJwzzt81ifUu+8uve1EDW/VJl83KsxmqgqNZLzcFEhSliZGvx3+pk/RH0IOmg==}
|
||||||
|
|
||||||
'@hookform/resolvers@5.7.1':
|
'@hookform/resolvers@5.8.0':
|
||||||
resolution: {integrity: sha512-8wS/P4UDr5sQDe4nFaV51TVyfDPrWgNIXweqG0Bs9Z5LSuzKLb+RQNPvkN2oHM5SRrJyWrVH/F+LOUcFjUyvwQ==}
|
resolution: {integrity: sha512-2m6GvRLmYYK1Fwt093lGMf7db9l/+8pNuAtwoNkpBntJT4xcA5lNthYGWKViOc3z2SuaPD0HjE81pyXmqc1JyA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@sinclair/typebox': '>=0.25.24'
|
'@sinclair/typebox': '>=0.25.24'
|
||||||
'@standard-schema/spec': ^1.0.0
|
'@standard-schema/spec': ^1.0.0
|
||||||
@@ -584,7 +584,7 @@ packages:
|
|||||||
superstruct: '>=0.12.0'
|
superstruct: '>=0.12.0'
|
||||||
typanion: ^3.3.2
|
typanion: ^3.3.2
|
||||||
valibot: '>=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc'
|
valibot: '>=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc'
|
||||||
vest: '>=3.0.0'
|
vest: '>=6.0.0'
|
||||||
yup: ^1.0.0
|
yup: ^1.0.0
|
||||||
zod: ^3.25.0 || ^4.0.0
|
zod: ^3.25.0 || ^4.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
@@ -825,60 +825,60 @@ packages:
|
|||||||
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
|
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
|
||||||
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
|
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-o8OiXKIATcSC6kHm5BIEmaDEDTFUDhpy9POQIbzAQTlb8NWwFhEQheTLFSbyQXxH2ywhjZ/e8EUlbOjj2k22Yg==}
|
resolution: {integrity: sha512-/6XQeYPHM6jF1gTeJ82Mu6yXOHQIFVxzAn3DkPtHDp5v6SDXoCicBMTHloN+2h4WKFCQujSLCgLZHItJ3jN1uw==}
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==}
|
resolution: {integrity: sha512-35G3xwkQUb2oETSDjFXGrVugknoayLFBh7vSE+yAcl9IP2zT9wyGwq7297AYHR11kJld807t5f8AJBs6WBzXsQ==}
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==}
|
resolution: {integrity: sha512-ABMIu2zQ7cnNIHm5ivKGwZwUrm0pAai3yiJ/gK/rF1c1VP9UOnj7XECbMKFdVKp9I9eMYq9NoDs1WXOoowxzJw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==}
|
resolution: {integrity: sha512-gNG21e/UnrroeScbY/QndUEdl0mF1FRibW7BBeYUz/5ABCepjqDdEdgr592vpzMtCn/m7FTjYq3TN4TpyDnutw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==}
|
resolution: {integrity: sha512-6B6Lw016iwNUQuaJoraMMTLh6TwHzFUtxipSScD1F3YyymcrRWkobodRS2ftIOkF5vrs4zNlyUrTC5YZQ9Lz5w==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==}
|
resolution: {integrity: sha512-JUiPXZKK9wOhjf4MgDiH29GZLxfqOesbLtHq2pDxwH/WwscTRV2ToymnOTh1egzaZf0ueUf8T2+CeYTGHjW0Iw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==}
|
resolution: {integrity: sha512-Uog9jsrmIRIL/lfvIp9htmskSNC7JcQsMVucXL2V2YY1y/D9IUN3LPEafqy0zRJ2cIU1SQ0V6F6TlffQ+pLAGg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==}
|
resolution: {integrity: sha512-6yy3FT13KgUFOj5H8bl8w/6nKiJwHIvbtwh1V+1acsu+7y4tJjnemSa6mhsh53BeoVrlozE+fMgZhXH46WmjMA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==}
|
resolution: {integrity: sha512-iOoN1QecUoGNZik536U/vtK43YwgyrCsGIkth52yIkl612n+0C9MjSnJbQAikISpb+WYRooBVhaDlUW7iZoKog==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
resolution: {integrity: sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==}
|
resolution: {integrity: sha512-d/k+PpAriUPaeMJJOG7HUSdqfEX46FEPWU1p3/nm2ACmXhj9hFEWdFODUBIpkuijXYkfL90qZzTqVPRp4BW/hw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -1584,8 +1584,8 @@ packages:
|
|||||||
'@swc/[email protected]':
|
'@swc/[email protected]':
|
||||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||||
|
|
||||||
'@swc/[email protected].15':
|
'@swc/[email protected].23':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
|
||||||
|
|
||||||
'@swc/[email protected]':
|
'@swc/[email protected]':
|
||||||
resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==}
|
resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==}
|
||||||
@@ -2583,8 +2583,8 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
[email protected].0:
|
[email protected].1:
|
||||||
resolution: {integrity: sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==}
|
resolution: {integrity: sha512-hsAp0i7Rh+/dhe7DGIeN2YlpLM1DP4MNxti9EtDMtqcO612X81MvvEj388/oTce9U1EcEIOWDlGq0zRwrBKvuA==}
|
||||||
engines: {node: '>=20.9.0'}
|
engines: {node: '>=20.9.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2758,8 +2758,8 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
|
resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
|
||||||
|
|
||||||
resend@6.19.0:
|
resend@6.20.0:
|
||||||
resolution: {integrity: sha512-JnEdYnd9WyBDIzunsEZtUF8n3cBKM9SlmySaos/7wwi4sEXKG1Zz4M64VFAyk+278erX01Fq2wHQ3p7OIdPriA==}
|
resolution: {integrity: sha512-fXDFt7jVMuka6ruS79DzaQFKPyxOAUtoYUGSl3dTUyOnFK9klmUULxADQRzFFtHfHRipYyy62jYcm4cMKqvtjw==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@react-email/render': '*'
|
'@react-email/render': '*'
|
||||||
@@ -3372,7 +3372,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/fast-memoize': 3.1.7
|
'@formatjs/fast-memoize': 3.1.7
|
||||||
|
|
||||||
'@hookform/resolvers@5.7.1(@standard-schema/[email protected])([email protected]([email protected]))([email protected])':
|
'@hookform/resolvers@5.8.0(@standard-schema/[email protected])([email protected]([email protected]))([email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.85.0([email protected])
|
react-hook-form: 7.85.0([email protected])
|
||||||
@@ -3514,37 +3514,37 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.3
|
'@tybys/wasm-util': 0.10.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
dependencies:
|
dependencies:
|
||||||
webpack-bundle-analyzer: 4.10.1
|
webpack-bundle-analyzer: 4.10.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
'@next/[email protected].0': {}
|
'@next/[email protected].1': {}
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/[email protected].0':
|
'@next/[email protected].1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@noble/[email protected]': {}
|
'@noble/[email protected]': {}
|
||||||
@@ -3985,7 +3985,7 @@ snapshots:
|
|||||||
'@swc/[email protected]':
|
'@swc/[email protected]':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@swc/[email protected]':
|
'@swc/[email protected](@swc/[email protected])':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/counter': 0.1.3
|
'@swc/counter': 0.1.3
|
||||||
'@swc/types': 0.1.28
|
'@swc/types': 0.1.28
|
||||||
@@ -4002,10 +4002,11 @@ snapshots:
|
|||||||
'@swc/core-win32-arm64-msvc': 1.15.47
|
'@swc/core-win32-arm64-msvc': 1.15.47
|
||||||
'@swc/core-win32-ia32-msvc': 1.15.47
|
'@swc/core-win32-ia32-msvc': 1.15.47
|
||||||
'@swc/core-win32-x64-msvc': 1.15.47
|
'@swc/core-win32-x64-msvc': 1.15.47
|
||||||
|
'@swc/helpers': 0.5.23
|
||||||
|
|
||||||
'@swc/[email protected]': {}
|
'@swc/[email protected]': {}
|
||||||
|
|
||||||
'@swc/[email protected].15':
|
'@swc/[email protected].23':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
@@ -4753,22 +4754,22 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected].0(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]):
|
[email protected]([email protected].1(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@auth/core': 0.41.3
|
'@auth/core': 0.41.3
|
||||||
next: 16.3.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
next: 16.3.1(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]([email protected].0(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected]):
|
[email protected](@swc/[email protected])([email protected].1(@types/[email protected])([email protected]([email protected]))([email protected]))([email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/intl-localematcher': 0.8.13
|
'@formatjs/intl-localematcher': 0.8.13
|
||||||
'@parcel/watcher': 2.6.0
|
'@parcel/watcher': 2.6.0
|
||||||
'@swc/core': 1.15.47
|
'@swc/core': 1.15.47(@swc/[email protected])
|
||||||
icu-minify: 4.13.6
|
icu-minify: 4.13.6
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
next: 16.3.0(@types/[email protected])([email protected]([email protected]))([email protected])
|
next: 16.3.1(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||||
next-intl-swc-plugin-extractor: 4.13.6
|
next-intl-swc-plugin-extractor: 4.13.6
|
||||||
po-parser: 2.1.1
|
po-parser: 2.1.1
|
||||||
react: 19.2.8
|
react: 19.2.8
|
||||||
@@ -4778,10 +4779,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
|
|
||||||
[email protected].0(@types/[email protected])([email protected]([email protected]))([email protected]):
|
[email protected].1(@types/[email protected])([email protected]([email protected]))([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.3.0
|
'@next/env': 16.3.1
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.23
|
||||||
baseline-browser-mapping: 2.11.13
|
baseline-browser-mapping: 2.11.13
|
||||||
caniuse-lite: 1.0.30001809
|
caniuse-lite: 1.0.30001809
|
||||||
postcss: 8.5.26
|
postcss: 8.5.26
|
||||||
@@ -4789,14 +4790,14 @@ snapshots:
|
|||||||
react-dom: 19.2.8([email protected])
|
react-dom: 19.2.8([email protected])
|
||||||
styled-jsx: 5.1.6([email protected])
|
styled-jsx: 5.1.6([email protected])
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 16.3.0
|
'@next/swc-darwin-arm64': 16.3.1
|
||||||
'@next/swc-darwin-x64': 16.3.0
|
'@next/swc-darwin-x64': 16.3.1
|
||||||
'@next/swc-linux-arm64-gnu': 16.3.0
|
'@next/swc-linux-arm64-gnu': 16.3.1
|
||||||
'@next/swc-linux-arm64-musl': 16.3.0
|
'@next/swc-linux-arm64-musl': 16.3.1
|
||||||
'@next/swc-linux-x64-gnu': 16.3.0
|
'@next/swc-linux-x64-gnu': 16.3.1
|
||||||
'@next/swc-linux-x64-musl': 16.3.0
|
'@next/swc-linux-x64-musl': 16.3.1
|
||||||
'@next/swc-win32-arm64-msvc': 16.3.0
|
'@next/swc-win32-arm64-msvc': 16.3.1
|
||||||
'@next/swc-win32-x64-msvc': 16.3.0
|
'@next/swc-win32-x64-msvc': 16.3.1
|
||||||
sharp: 0.35.3(@types/[email protected])
|
sharp: 0.35.3(@types/[email protected])
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -5004,7 +5005,7 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
resend@6.19.0:
|
resend@6.20.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
postal-mime: 2.7.5
|
postal-mime: 2.7.5
|
||||||
standardwebhooks: 1.0.0
|
standardwebhooks: 1.0.0
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { redirect } from "next/navigation";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requirePermission } from "@/lib/admin/guard";
|
import { requirePermission } from "@/lib/admin/guard";
|
||||||
import { db, WebsiteAds } from "@/lib/db";
|
import { db, WebsiteAds } from "@/lib/db";
|
||||||
import { formPositiveBigInt } from "@/lib/form-data";
|
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { adminAction } from "@/lib/safe-action";
|
import { adminAction } from "@/lib/safe-action";
|
||||||
@@ -117,28 +116,3 @@ export const deleteAd = adminAction(
|
|||||||
return actionOk();
|
return actionOk();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Legacy form POST delete — kept for compatibility; prefer client deleteAd action. */
|
|
||||||
export async function deleteAdForm(formData: FormData): Promise<void> {
|
|
||||||
const staff = await requirePermission(PERMS.PAGES_EDIT);
|
|
||||||
const id = formPositiveBigInt(formData, "id");
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.delete(WebsiteAds).where(eq(WebsiteAds.id, id));
|
|
||||||
await logStaffActivity({
|
|
||||||
staffId: staff.id,
|
|
||||||
action: "ad_delete",
|
|
||||||
description: `Deleted advertisement #${id}`,
|
|
||||||
targetType: "website_ad",
|
|
||||||
targetId: Number(id),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error("Action failed: deleteAdForm", {
|
|
||||||
action: "deleteAdForm",
|
|
||||||
id: Number(id),
|
|
||||||
error: err instanceof Error ? err.message : "DB error",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
redirect("/admin/ads");
|
|
||||||
}
|
|
||||||
@@ -155,6 +155,7 @@ export function AuditClient() {
|
|||||||
const [applySql, setApplySql] = useState(false);
|
const [applySql, setApplySql] = useState(false);
|
||||||
const [repairFurniData, setRepairFurniData] = useState(false);
|
const [repairFurniData, setRepairFurniData] = useState(false);
|
||||||
const [repairStructure, setRepairStructure] = useState(false);
|
const [repairStructure, setRepairStructure] = useState(false);
|
||||||
|
const [checkFurniDataIds, setCheckFurniDataIds] = useState(false);
|
||||||
|
|
||||||
const [issues, setIssues] = useState<AuditIssue[]>([]);
|
const [issues, setIssues] = useState<AuditIssue[]>([]);
|
||||||
const [missingFromSources, setMissingFromSources] = useState<
|
const [missingFromSources, setMissingFromSources] = useState<
|
||||||
@@ -421,6 +422,13 @@ export function AuditClient() {
|
|||||||
>
|
>
|
||||||
Repair FurnitureData.json (add missing + dedupe)
|
Repair FurnitureData.json (add missing + dedupe)
|
||||||
</AuditCheckbox>
|
</AuditCheckbox>
|
||||||
|
<AuditCheckbox
|
||||||
|
checked={checkFurniDataIds}
|
||||||
|
disabled={loading}
|
||||||
|
onChange={setCheckFurniDataIds}
|
||||||
|
>
|
||||||
|
Check FurnitureData.json for spriteId conflicts
|
||||||
|
</AuditCheckbox>
|
||||||
<AuditCheckbox
|
<AuditCheckbox
|
||||||
checked={repairStructure}
|
checked={repairStructure}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const POST = withAdmin(
|
|||||||
let applySql = false;
|
let applySql = false;
|
||||||
let repairFurniData = false;
|
let repairFurniData = false;
|
||||||
let repairStructure = false;
|
let repairStructure = false;
|
||||||
|
let checkFurniDataIds = false;
|
||||||
try {
|
try {
|
||||||
const body = (await request.json().catch(() => ({}))) as {
|
const body = (await request.json().catch(() => ({}))) as {
|
||||||
repair?: unknown;
|
repair?: unknown;
|
||||||
@@ -26,6 +27,7 @@ export const POST = withAdmin(
|
|||||||
applySql?: unknown;
|
applySql?: unknown;
|
||||||
repairFurniData?: unknown;
|
repairFurniData?: unknown;
|
||||||
repairStructure?: unknown;
|
repairStructure?: unknown;
|
||||||
|
checkFurniDataIds?: unknown;
|
||||||
};
|
};
|
||||||
repair = body.repair === true;
|
repair = body.repair === true;
|
||||||
repairNitros = body.repairNitros === true;
|
repairNitros = body.repairNitros === true;
|
||||||
@@ -33,6 +35,7 @@ export const POST = withAdmin(
|
|||||||
applySql = body.applySql === true;
|
applySql = body.applySql === true;
|
||||||
repairFurniData = body.repairFurniData === true;
|
repairFurniData = body.repairFurniData === true;
|
||||||
repairStructure = body.repairStructure === true;
|
repairStructure = body.repairStructure === true;
|
||||||
|
checkFurniDataIds = body.checkFurniDataIds === true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn("Failed to parse audit request body", { err });
|
logger.warn("Failed to parse audit request body", { err });
|
||||||
}
|
}
|
||||||
@@ -55,6 +58,7 @@ export const POST = withAdmin(
|
|||||||
applySql,
|
applySql,
|
||||||
repairFurniData,
|
repairFurniData,
|
||||||
repairStructure,
|
repairStructure,
|
||||||
|
checkFurniDataIds,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
send({
|
send({
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function RegisterForm({
|
|||||||
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
|
const showCaptcha = captcha.provider !== "none" && !!captcha.siteKey;
|
||||||
const [serverError, formAction, isPending] = useActionState(register, null);
|
const [serverError, formAction, isPending] = useActionState(register, null);
|
||||||
const [termsAccepted, setTermsAccepted] = useState(false);
|
const [termsAccepted, setTermsAccepted] = useState(false);
|
||||||
|
const [ageVerified, setAgeVerified] = useState(false);
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [showConfirm, setShowConfirm] = useState(false);
|
const [showConfirm, setShowConfirm] = useState(false);
|
||||||
@@ -280,7 +281,7 @@ export function RegisterForm({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Terms */}
|
{/* Terms & Age Verification */}
|
||||||
<div
|
<div
|
||||||
className="rounded-xl p-4 flex flex-col gap-3"
|
className="rounded-xl p-4 flex flex-col gap-3"
|
||||||
style={{
|
style={{
|
||||||
@@ -314,6 +315,23 @@ export function RegisterForm({
|
|||||||
{t("termsAccept", { hotel: hotelName })}
|
{t("termsAccept", { hotel: hotelName })}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-3 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="ageVerified"
|
||||||
|
name="ageVerified"
|
||||||
|
checked={ageVerified}
|
||||||
|
onChange={(e) => setAgeVerified(e.target.checked)}
|
||||||
|
className="w-5 h-5 rounded border-2 accent-(--color-primary) shrink-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="ageVerified"
|
||||||
|
className="font-semibold cursor-pointer select-none"
|
||||||
|
style={{ color: "var(--color-text-readable)" }}
|
||||||
|
>
|
||||||
|
{t("ageVerified", { hotel: hotelName })}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Captcha */}
|
{/* Captcha */}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ export const User = mysqlTable("users", {
|
|||||||
homeRoom: int("home_room").notNull().default(0),
|
homeRoom: int("home_room").notNull().default(0),
|
||||||
secretKey: varchar("secret_key", { length: 40 }),
|
secretKey: varchar("secret_key", { length: 40 }),
|
||||||
pincode: varchar("pincode", { length: 11 }),
|
pincode: varchar("pincode", { length: 11 }),
|
||||||
|
termsAccepted: boolean("terms_accepted").notNull().default(false),
|
||||||
|
ageVerified: boolean("age_verified").notNull().default(false),
|
||||||
extraRank: int("extra_rank"),
|
extraRank: int("extra_rank"),
|
||||||
twoFactorSecret: text("two_factor_secret"),
|
twoFactorSecret: text("two_factor_secret"),
|
||||||
twoFactorRecoveryCodes: text("two_factor_recovery_codes"),
|
twoFactorRecoveryCodes: text("two_factor_recovery_codes"),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
repairFurniData,
|
repairFurniData,
|
||||||
repairOrphanedCatalog,
|
repairOrphanedCatalog,
|
||||||
} from "./catalog-repair";
|
} from "./catalog-repair";
|
||||||
|
import { readFurniData } from "./furni-data";
|
||||||
import { ensureDirectories } from "./furni-import";
|
import { ensureDirectories } from "./furni-import";
|
||||||
import {
|
import {
|
||||||
assetNameCandidates,
|
assetNameCandidates,
|
||||||
@@ -118,6 +119,7 @@ export interface AuditSummary {
|
|||||||
duplicatesMerged: number;
|
duplicatesMerged: number;
|
||||||
duplicateRowsRemoved: number;
|
duplicateRowsRemoved: number;
|
||||||
remappedReferences: number;
|
remappedReferences: number;
|
||||||
|
furniDataIdConflicts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CatalogAuditOptions {
|
export interface CatalogAuditOptions {
|
||||||
@@ -128,6 +130,7 @@ export interface CatalogAuditOptions {
|
|||||||
organizeSql?: boolean;
|
organizeSql?: boolean;
|
||||||
repairFurniData?: boolean;
|
repairFurniData?: boolean;
|
||||||
repairStructure?: boolean;
|
repairStructure?: boolean;
|
||||||
|
checkFurniDataIds?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runCatalogAudit(
|
export async function runCatalogAudit(
|
||||||
@@ -722,6 +725,60 @@ export async function runCatalogAudit(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── FurnitureData.json id conflict check ──────────────
|
||||||
|
let furniDataIdConflicts: {
|
||||||
|
classname: string;
|
||||||
|
itemId: number;
|
||||||
|
conflictingId: number;
|
||||||
|
existingClassname: string;
|
||||||
|
}[] = [];
|
||||||
|
if (options?.checkFurniDataIds) {
|
||||||
|
onEvent?.({
|
||||||
|
type: "progress",
|
||||||
|
message: "Checking FurnitureData.json for spriteId conflicts…",
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const furniData = (await readFurniData()) as Record<
|
||||||
|
string,
|
||||||
|
{ furnitype: Array<Record<string, unknown>> }
|
||||||
|
>;
|
||||||
|
// Build map of id → classname from both sections
|
||||||
|
const idToClassname = new Map<number, string>();
|
||||||
|
for (const section of ["roomitemtypes", "wallitemtypes"] as const) {
|
||||||
|
for (const e of furniData[section]?.furnitype ?? []) {
|
||||||
|
const id = Number(e?.id);
|
||||||
|
const classname = typeof e?.classname === "string" ? e.classname : "";
|
||||||
|
if (!Number.isFinite(id) || id <= 0 || !classname) continue;
|
||||||
|
const existing = idToClassname.get(id);
|
||||||
|
if (existing && existing !== classname) {
|
||||||
|
// Conflict: same id used by different classname
|
||||||
|
furniDataIdConflicts.push({
|
||||||
|
classname,
|
||||||
|
itemId: id,
|
||||||
|
conflictingId: id,
|
||||||
|
existingClassname: existing,
|
||||||
|
});
|
||||||
|
} else if (!existing) {
|
||||||
|
idToClassname.set(id, classname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove duplicates (same classname + id pair counted once)
|
||||||
|
const seen = new Set<string>();
|
||||||
|
furniDataIdConflicts = furniDataIdConflicts.filter((c) => {
|
||||||
|
const key = `${c.classname}:${c.conflictingId}`;
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
onEvent?.({
|
||||||
|
type: "error",
|
||||||
|
message: `FurnitureData id check failed: ${(err as Error).message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onEvent?.({ type: "progress", message: "Comparing with clone sources…" });
|
onEvent?.({ type: "progress", message: "Comparing with clone sources…" });
|
||||||
|
|
||||||
const sources = await listSources();
|
const sources = await listSources();
|
||||||
@@ -811,6 +868,7 @@ export async function runCatalogAudit(
|
|||||||
duplicatesMerged,
|
duplicatesMerged,
|
||||||
duplicateRowsRemoved,
|
duplicateRowsRemoved,
|
||||||
remappedReferences,
|
remappedReferences,
|
||||||
|
furniDataIdConflicts: furniDataIdConflicts.length,
|
||||||
};
|
};
|
||||||
|
|
||||||
onEvent?.({
|
onEvent?.({
|
||||||
|
|||||||
@@ -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> {
|
export async function withFurniDataLock<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
let release!: () => void;
|
let release!: () => void;
|
||||||
const acquired = new Promise<void>((r) => {
|
const acquired = new Promise<void>((r) => {
|
||||||
@@ -228,6 +299,11 @@ export async function appendFurniEntry(
|
|||||||
string,
|
string,
|
||||||
{ furnitype: Array<Record<string, unknown>> }
|
{ 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 (itemType === "i") {
|
||||||
if (!furniData.wallitemtypes) furniData.wallitemtypes = { furnitype: [] };
|
if (!furniData.wallitemtypes) furniData.wallitemtypes = { furnitype: [] };
|
||||||
upsertEntryInSection(furniData.wallitemtypes, entry);
|
upsertEntryInSection(furniData.wallitemtypes, entry);
|
||||||
@@ -331,6 +407,17 @@ export async function appendFurniEntriesBatch(
|
|||||||
string,
|
string,
|
||||||
{ furnitype: unknown[] }
|
{ 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) {
|
for (const { entry, itemType } of entries) {
|
||||||
if (itemType === "i") {
|
if (itemType === "i") {
|
||||||
if (!furniData.wallitemtypes)
|
if (!furniData.wallitemtypes)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ vi.mock("@/lib/services/furni-data", () => ({
|
|||||||
appendFurniEntry: vi.fn(),
|
appendFurniEntry: vi.fn(),
|
||||||
buildFurniEntry: vi.fn(),
|
buildFurniEntry: vi.fn(),
|
||||||
removeFurniEntry: vi.fn(),
|
removeFurniEntry: vi.fn(),
|
||||||
|
findFurniDataIdConflict: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/lib/services/habbo-furnidata-cache", () => ({
|
vi.mock("@/lib/services/habbo-furnidata-cache", () => ({
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
appendFurniEntry,
|
appendFurniEntry,
|
||||||
buildFurniEntry,
|
buildFurniEntry,
|
||||||
|
findFurniDataIdConflict,
|
||||||
removeFurniEntry,
|
removeFurniEntry,
|
||||||
} from "@/lib/services/furni-data";
|
} from "@/lib/services/furni-data";
|
||||||
import {
|
import {
|
||||||
@@ -472,6 +473,16 @@ export async function importSingleFurni(params: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Check if spriteId already used in FurnitureData.json ──
|
||||||
|
const takenBy = await findFurniDataIdConflict(originalId, classname);
|
||||||
|
if (takenBy) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
warnings,
|
||||||
|
error: `spriteId ${originalId} already used by "${takenBy}" in FurnitureData.json`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const itemType = type === "wallitem" ? "i" : "s";
|
const itemType = type === "wallitem" ? "i" : "s";
|
||||||
const spriteId = originalId;
|
const spriteId = originalId;
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"online": "{count} {hotel} online",
|
"online": "{count} {hotel} online",
|
||||||
"nitroClient": "Nitro client",
|
"nitroClient": "Nitro client",
|
||||||
"tagline": "Een online virtuele wereld waar je je eigen avatar kunt maken, vrienden kunt maken, kunt chatten, kamers kunt maken en nog veel meer!",
|
"tagline": "Een online virtuele wereld waar je je eigen avatar kunt maken, vrienden kunt maken, kunt chatten, kamers kunt maken en nog veel meer! Leeftijdsvereiste: 18+",
|
||||||
"login": "Inloggen",
|
"login": "Inloggen",
|
||||||
"or": "Of",
|
"or": "Of",
|
||||||
"createAccount": "Account aanmaken"
|
"createAccount": "Account aanmaken"
|
||||||
@@ -919,6 +919,7 @@
|
|||||||
"passwordError": "Wachtwoord moet minimaal 6 tekens bevatten.",
|
"passwordError": "Wachtwoord moet minimaal 6 tekens bevatten.",
|
||||||
"confirmPasswordError": "Wachtwoorden komen niet overeen.",
|
"confirmPasswordError": "Wachtwoorden komen niet overeen.",
|
||||||
"termsRequired": "Je moet de voorwaarden & regels accepteren.",
|
"termsRequired": "Je moet de voorwaarden & regels accepteren.",
|
||||||
|
"ageVerified": "Ik ben 18+ en ga akkoord met de leeftijdsvereiste.",
|
||||||
"usernameRequired": "Gebruikersnaam is verplicht.",
|
"usernameRequired": "Gebruikersnaam is verplicht.",
|
||||||
"emailRequired": "E-mail is verplicht.",
|
"emailRequired": "E-mail is verplicht.",
|
||||||
"passwordRequired": "Wachtwoord is verplicht.",
|
"passwordRequired": "Wachtwoord is verplicht.",
|
||||||
|
|||||||
Reference in new issue
Block a user