From a74b8d6e7442bc39fe8c8771ee4a913d08675ae5 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 04:50:17 +0000 Subject: [PATCH] Fix: Implement pipeline error handling Implement comprehensive error handling and robustness measures across the entire pipeline as per the detailed plan. This includes database-level security, client-side validation, scheduled maintenance, and fallback mechanisms for edge function failures. --- package-lock.json | 119 ++++++----- package.json | 1 + src/lib/entitySubmissionHelpers.ts | 33 +++ src/lib/imageUploadHelper.ts | 33 ++- src/lib/submissionQueue.ts | 192 ++++++++++++++++++ src/lib/submissionValidation.ts | 101 +++++++++ supabase/config.toml | 3 + .../functions/scheduled-maintenance/index.ts | 77 +++++++ ...6_d2d30465-685b-432a-a6ca-b2b8e46935b9.sql | 18 ++ 9 files changed, 513 insertions(+), 64 deletions(-) create mode 100644 src/lib/submissionQueue.ts create mode 100644 supabase/functions/scheduled-maintenance/index.ts create mode 100644 supabase/migrations/20251107044746_d2d30465-685b-432a-a6ca-b2b8e46935b9.sql diff --git a/package-lock.json b/package-lock.json index 69189432..e81351b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "date-fns": "^3.6.0", "dompurify": "^3.3.0", "embla-carousel-react": "^8.6.0", + "idb": "^8.0.3", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -108,6 +109,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1591,6 +1593,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1616,6 +1619,7 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -4454,7 +4458,7 @@ "version": "1.14.0", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.14.0.tgz", "integrity": "sha512-oExhY90bes5pDTVrei0xlMVosTxwd/NMafIpqsC4dMbRYZ5KB981l/CX8tMnGsagTplj/RcG9BeRYmV6/J5m3w==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -4663,7 +4667,7 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "devOptional": true, + "dev": true, "license": "Apache-2.0" }, "node_modules/@swc/helpers": { @@ -4679,7 +4683,7 @@ "version": "0.1.25", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -4976,12 +4980,14 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", + "dev": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -4992,7 +4998,7 @@ "version": "18.3.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^18.0.0" @@ -5838,12 +5844,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -5857,6 +5865,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -6007,6 +6016,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6131,6 +6141,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6228,6 +6239,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -6252,6 +6264,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -6401,6 +6414,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6472,6 +6486,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -6723,6 +6738,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/diff": { @@ -6738,6 +6754,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, "license": "MIT" }, "node_modules/dom-helpers": { @@ -7944,6 +7961,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -8033,6 +8051,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -8257,6 +8276,12 @@ "node": ">= 14" } }, + "node_modules/idb": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -8374,6 +8399,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -8386,6 +8412,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -8498,17 +8525,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isomorphic.js": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", - "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", - "license": "MIT", - "peer": true, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -8528,6 +8544,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -8621,32 +8638,11 @@ "integrity": "sha512-3VuV8xXhh5xJA6tzvfDvE0YBCMkIZUmxtRilJQDDdCgJCc+eut6qAv2qbN+pbqvarqcQqPN1UF+8YvsjmyOZpw==", "license": "MIT" }, - "node_modules/lib0": { - "version": "0.2.114", - "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", - "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "isomorphic.js": "^0.2.4" - }, - "bin": { - "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", - "0gentesthtml": "bin/gentesthtml.js", - "0serve": "bin/0serve.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -8659,6 +8655,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -9910,6 +9907,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -10027,6 +10025,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10055,6 +10054,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -10252,6 +10252,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -10287,6 +10288,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -10305,6 +10307,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10314,6 +10317,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -10367,6 +10371,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10395,6 +10400,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -10412,6 +10418,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10437,6 +10444,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10479,6 +10487,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10504,6 +10513,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -10531,12 +10541,14 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/postcss/node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -10928,6 +10940,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -10937,6 +10950,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -11049,6 +11063,7 @@ "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.1", @@ -11098,7 +11113,7 @@ "version": "4.52.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -11402,6 +11417,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -11586,6 +11602,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -11621,6 +11638,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11649,6 +11667,7 @@ "version": "3.4.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -11695,6 +11714,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -11724,6 +11744,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -11733,6 +11754,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -11817,6 +11839,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/ts-morph": { @@ -11922,6 +11945,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -12203,6 +12227,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -12980,24 +13005,6 @@ "node": ">=18" } }, - "node_modules/yjs": { - "version": "13.6.27", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", - "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", - "license": "MIT", - "peer": true, - "dependencies": { - "lib0": "^0.2.99" - }, - "engines": { - "node": ">=16.0.0", - "npm": ">=8.0.0" - }, - "funding": { - "type": "GitHub Sponsors ❤", - "url": "https://github.com/sponsors/dmonad" - } - }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index b2329cd3..31152965 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "date-fns": "^3.6.0", "dompurify": "^3.3.0", "embla-carousel-react": "^8.6.0", + "idb": "^8.0.3", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 52b91972..e2332edf 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -411,6 +411,39 @@ async function submitCompositeCreation( } } + // CRITICAL: Validate all temp refs were properly resolved + const validateTempRefs = () => { + const errors: string[] = []; + + if (uploadedPrimary.type === 'park') { + if ('_temp_operator_ref' in primaryData && primaryData._temp_operator_ref === undefined) { + errors.push('Invalid operator reference - dependency not found'); + } + if ('_temp_property_owner_ref' in primaryData && primaryData._temp_property_owner_ref === undefined) { + errors.push('Invalid property owner reference - dependency not found'); + } + } else if (uploadedPrimary.type === 'ride') { + if ('_temp_park_ref' in primaryData && primaryData._temp_park_ref === undefined) { + errors.push('Invalid park reference - dependency not found'); + } + if ('_temp_manufacturer_ref' in primaryData && primaryData._temp_manufacturer_ref === undefined) { + errors.push('Invalid manufacturer reference - dependency not found'); + } + if ('_temp_designer_ref' in primaryData && primaryData._temp_designer_ref === undefined) { + errors.push('Invalid designer reference - dependency not found'); + } + if ('_temp_ride_model_ref' in primaryData && primaryData._temp_ride_model_ref === undefined) { + errors.push('Invalid ride model reference - dependency not found'); + } + } + + if (errors.length > 0) { + throw new Error(`Temp reference validation failed: ${errors.join(', ')}`); + } + }; + + validateTempRefs(); + submissionItems.push({ item_type: uploadedPrimary.type, action_type: 'create' as const, diff --git a/src/lib/imageUploadHelper.ts b/src/lib/imageUploadHelper.ts index 4b9f8a55..6998c61f 100644 --- a/src/lib/imageUploadHelper.ts +++ b/src/lib/imageUploadHelper.ts @@ -62,17 +62,34 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise withTimeout( + fetch(uploadUrlData.uploadURL, { + method: 'POST', + body: formData, + }), + UPLOAD_TIMEOUT_MS, + 'Cloudflare upload' + ), + { + maxAttempts: 3, + baseDelay: 500, + shouldRetry: (error) => { + // Retry on network errors, timeouts, or 5xx errors + if (error instanceof Error) { + const msg = error.message.toLowerCase(); + if (msg.includes('timeout')) return true; + if (msg.includes('network')) return true; + if (msg.includes('failed to fetch')) return true; + } + return false; + } + } ); if (!uploadResponse.ok) { diff --git a/src/lib/submissionQueue.ts b/src/lib/submissionQueue.ts new file mode 100644 index 00000000..2e2086d9 --- /dev/null +++ b/src/lib/submissionQueue.ts @@ -0,0 +1,192 @@ +/** + * Submission Queue with IndexedDB Fallback + * + * Provides resilience when edge functions are unavailable by queuing + * submissions locally and retrying when connectivity is restored. + * + * Part of Sacred Pipeline Phase 3: Fortify Defenses + */ + +import { openDB, DBSchema, IDBPDatabase } from 'idb'; + +interface SubmissionQueueDB extends DBSchema { + submissions: { + key: string; + value: { + id: string; + type: string; + data: any; + timestamp: number; + retries: number; + lastAttempt: number | null; + error: string | null; + }; + }; +} + +const DB_NAME = 'thrillwiki-submission-queue'; +const DB_VERSION = 1; +const STORE_NAME = 'submissions'; +const MAX_RETRIES = 3; + +let dbInstance: IDBPDatabase | null = null; + +async function getDB(): Promise> { + if (dbInstance) return dbInstance; + + dbInstance = await openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + } + }, + }); + + return dbInstance; +} + +/** + * Queue a submission for later processing + */ +export async function queueSubmission(type: string, data: any): Promise { + const db = await getDB(); + const id = crypto.randomUUID(); + + await db.add(STORE_NAME, { + id, + type, + data, + timestamp: Date.now(), + retries: 0, + lastAttempt: null, + error: null, + }); + + console.info(`[SubmissionQueue] Queued ${type} submission ${id}`); + return id; +} + +/** + * Get all pending submissions + */ +export async function getPendingSubmissions() { + const db = await getDB(); + return await db.getAll(STORE_NAME); +} + +/** + * Get count of pending submissions + */ +export async function getPendingCount(): Promise { + const db = await getDB(); + const all = await db.getAll(STORE_NAME); + return all.length; +} + +/** + * Remove a submission from the queue + */ +export async function removeFromQueue(id: string): Promise { + const db = await getDB(); + await db.delete(STORE_NAME, id); + console.info(`[SubmissionQueue] Removed submission ${id}`); +} + +/** + * Update submission retry count and error + */ +export async function updateSubmissionRetry( + id: string, + error: string +): Promise { + const db = await getDB(); + const item = await db.get(STORE_NAME, id); + + if (!item) return; + + item.retries += 1; + item.lastAttempt = Date.now(); + item.error = error; + + await db.put(STORE_NAME, item); +} + +/** + * Process all queued submissions + * Called when connectivity is restored or on app startup + */ +export async function processQueue( + submitFn: (type: string, data: any) => Promise +): Promise<{ processed: number; failed: number }> { + const db = await getDB(); + const pending = await db.getAll(STORE_NAME); + + let processed = 0; + let failed = 0; + + for (const item of pending) { + try { + console.info(`[SubmissionQueue] Processing ${item.type} submission ${item.id} (attempt ${item.retries + 1})`); + + await submitFn(item.type, item.data); + await db.delete(STORE_NAME, item.id); + processed++; + + console.info(`[SubmissionQueue] Successfully processed ${item.id}`); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + + if (item.retries >= MAX_RETRIES - 1) { + // Max retries exceeded, remove from queue + await db.delete(STORE_NAME, item.id); + failed++; + console.error(`[SubmissionQueue] Max retries exceeded for ${item.id}:`, errorMsg); + } else { + // Update retry count + await updateSubmissionRetry(item.id, errorMsg); + console.warn(`[SubmissionQueue] Retry ${item.retries + 1}/${MAX_RETRIES} failed for ${item.id}:`, errorMsg); + } + } + } + + return { processed, failed }; +} + +/** + * Clear all queued submissions (use with caution!) + */ +export async function clearQueue(): Promise { + const db = await getDB(); + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const all = await store.getAll(); + + await store.clear(); + await tx.done; + + console.warn(`[SubmissionQueue] Cleared ${all.length} submissions from queue`); + return all.length; +} + +/** + * Check if edge function is available + */ +export async function checkEdgeFunctionHealth( + functionUrl: string +): Promise { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + const response = await fetch(functionUrl, { + method: 'HEAD', + signal: controller.signal, + }); + + clearTimeout(timeout); + return response.ok || response.status === 405; // 405 = Method Not Allowed is OK + } catch (error) { + console.error('[SubmissionQueue] Health check failed:', error); + return false; + } +} diff --git a/src/lib/submissionValidation.ts b/src/lib/submissionValidation.ts index c99aff9e..47b486a4 100644 --- a/src/lib/submissionValidation.ts +++ b/src/lib/submissionValidation.ts @@ -9,6 +9,75 @@ export interface ValidationResult { errorMessage?: string; } +export interface SlugValidationResult extends ValidationResult { + suggestedSlug?: string; +} + +/** + * Validates slug format matching database constraints + * Pattern: lowercase alphanumeric with hyphens only + * No consecutive hyphens, no leading/trailing hyphens + */ +export function validateSlugFormat(slug: string): SlugValidationResult { + if (!slug) { + return { + valid: false, + missingFields: ['slug'], + errorMessage: 'Slug is required' + }; + } + + // Must match DB regex: ^[a-z0-9]+(-[a-z0-9]+)*$ + const slugRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/; + if (!slugRegex.test(slug)) { + const suggested = slug + .toLowerCase() + .replace(/[^a-z0-9-]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + + return { + valid: false, + missingFields: ['slug'], + errorMessage: 'Slug must be lowercase alphanumeric with hyphens only (no spaces or special characters)', + suggestedSlug: suggested + }; + } + + // Length constraints + if (slug.length < 2) { + return { + valid: false, + missingFields: ['slug'], + errorMessage: 'Slug too short (minimum 2 characters)' + }; + } + if (slug.length > 100) { + return { + valid: false, + missingFields: ['slug'], + errorMessage: 'Slug too long (maximum 100 characters)' + }; + } + + // Reserved slugs that could conflict with routes + const reserved = [ + 'admin', 'api', 'auth', 'new', 'edit', 'delete', 'create', + 'update', 'null', 'undefined', 'settings', 'profile', 'login', + 'logout', 'signup', 'dashboard', 'moderator', 'moderation' + ]; + if (reserved.includes(slug)) { + return { + valid: false, + missingFields: ['slug'], + errorMessage: `'${slug}' is a reserved slug and cannot be used`, + suggestedSlug: `${slug}-1` + }; + } + + return { valid: true, missingFields: [] }; +} + /** * Validates required fields for park creation */ @@ -28,6 +97,14 @@ export function validateParkCreateFields(data: any): ValidationResult { }; } + // Validate slug format + if (data.slug?.trim()) { + const slugValidation = validateSlugFormat(data.slug.trim()); + if (!slugValidation.valid) { + return slugValidation; + } + } + return { valid: true, missingFields: [] }; } @@ -50,6 +127,14 @@ export function validateRideCreateFields(data: any): ValidationResult { }; } + // Validate slug format + if (data.slug?.trim()) { + const slugValidation = validateSlugFormat(data.slug.trim()); + if (!slugValidation.valid) { + return slugValidation; + } + } + return { valid: true, missingFields: [] }; } @@ -71,6 +156,14 @@ export function validateCompanyCreateFields(data: any): ValidationResult { }; } + // Validate slug format + if (data.slug?.trim()) { + const slugValidation = validateSlugFormat(data.slug.trim()); + if (!slugValidation.valid) { + return slugValidation; + } + } + return { valid: true, missingFields: [] }; } @@ -93,6 +186,14 @@ export function validateRideModelCreateFields(data: any): ValidationResult { }; } + // Validate slug format + if (data.slug?.trim()) { + const slugValidation = validateSlugFormat(data.slug.trim()); + if (!slugValidation.valid) { + return slugValidation; + } + } + return { valid: true, missingFields: [] }; } diff --git a/supabase/config.toml b/supabase/config.toml index ad9deb02..51bfd5d1 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -74,3 +74,6 @@ verify_jwt = false [functions.cleanup-old-versions] verify_jwt = false + +[functions.scheduled-maintenance] +verify_jwt = false diff --git a/supabase/functions/scheduled-maintenance/index.ts b/supabase/functions/scheduled-maintenance/index.ts new file mode 100644 index 00000000..ec2a3197 --- /dev/null +++ b/supabase/functions/scheduled-maintenance/index.ts @@ -0,0 +1,77 @@ +import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'; +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4'; +import { edgeLogger } from '../_shared/logger.ts'; + +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', +}; + +serve(async (req: Request) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }); + } + + const requestId = crypto.randomUUID(); + + try { + edgeLogger.info('Starting scheduled maintenance', { requestId }); + + const supabase = createClient( + Deno.env.get('SUPABASE_URL')!, + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! + ); + + // Run system maintenance (orphaned image cleanup) + const { data, error } = await supabase.rpc('run_system_maintenance'); + + if (error) { + edgeLogger.error('Maintenance failed', { requestId, error: error.message }); + return new Response( + JSON.stringify({ + success: false, + error: error.message, + requestId + }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ); + } + + edgeLogger.info('Maintenance completed successfully', { + requestId, + result: data + }); + + return new Response( + JSON.stringify({ + success: true, + result: data, + requestId + }), + { + status: 200, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ); + } catch (error) { + edgeLogger.error('Maintenance exception', { + requestId, + error: error instanceof Error ? error.message : String(error) + }); + + return new Response( + JSON.stringify({ + success: false, + error: 'Internal server error', + requestId + }), + { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' } + } + ); + } +}); diff --git a/supabase/migrations/20251107044746_d2d30465-685b-432a-a6ca-b2b8e46935b9.sql b/supabase/migrations/20251107044746_d2d30465-685b-432a-a6ca-b2b8e46935b9.sql new file mode 100644 index 00000000..5c3e4fb1 --- /dev/null +++ b/supabase/migrations/20251107044746_d2d30465-685b-432a-a6ca-b2b8e46935b9.sql @@ -0,0 +1,18 @@ +-- Phase 1: Critical Security Fixes for Sacred Pipeline +-- Fix 1.1: Attach ban prevention trigger to content_submissions +CREATE TRIGGER prevent_banned_submissions + BEFORE INSERT ON content_submissions + FOR EACH ROW + EXECUTE FUNCTION prevent_banned_user_submissions(); + +-- Fix 1.2: Add RLS policy to prevent banned users from submitting +CREATE POLICY "Banned users cannot submit" +ON content_submissions +FOR INSERT +TO authenticated +WITH CHECK ( + NOT EXISTS ( + SELECT 1 FROM profiles + WHERE user_id = auth.uid() AND banned = true + ) +); \ No newline at end of file