From fa7e28f840c99ae02015480b7f647bf18578acf9 Mon Sep 17 00:00:00 2001 From: Bhuvanesh S Date: Wed, 3 Jun 2026 21:10:31 +0530 Subject: [PATCH 1/2] feat: implement realtime check-in and dashboard optimizations --- Frontendd/package-lock.json | 673 +++++++++--------- Frontendd/package.json | 2 + Frontendd/src/context/SocketContext.jsx | 42 +- .../src/pages/dashboard/CustomerDashboard.jsx | 32 +- .../pages/dashboard/OrganizerDashboard.jsx | 372 ++++++---- Frontendd/vite.config.js | 1 + backend/package-lock.json | 67 +- backend/src/__tests__/admin.bulk.test.js | 2 +- .../src/controllers/registrationController.js | 509 ++++++------- backend/src/routes/notificationRoutes.js | 4 +- backend/src/routes/registrationRoutes.js | 29 +- backend/src/server.js | 50 +- backend/src/services/socket.js | 23 +- 13 files changed, 890 insertions(+), 916 deletions(-) diff --git a/Frontendd/package-lock.json b/Frontendd/package-lock.json index aaec1b3..c5fd615 100644 --- a/Frontendd/package-lock.json +++ b/Frontendd/package-lock.json @@ -40,6 +40,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@playwright/test": "^1.60.0", + "@rolldown/pluginutils": "^1.0.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", @@ -49,6 +50,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "ms": "^2.1.3", "postcss": "^8.4.47", "vite": "^6.0.0", "vite-plugin-pwa": "^1.3.0" @@ -67,13 +69,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -123,14 +125,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -227,9 +229,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -296,9 +298,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -342,23 +344,23 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -366,9 +368,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -415,13 +417,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1252,38 +1254,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-regenerator": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", @@ -1349,24 +1319,15 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", - "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1576,32 +1537,10 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@restart/hooks": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", - "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3" - }, - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", - "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "license": "MIT", "dependencies": { @@ -1623,33 +1562,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -1657,14 +1596,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1677,7 +1616,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1694,7 +1632,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1711,7 +1648,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1728,7 +1664,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1745,7 +1680,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1762,7 +1696,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1779,7 +1712,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1796,7 +1728,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1813,7 +1744,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1830,7 +1760,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1847,7 +1776,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1864,7 +1792,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1881,7 +1808,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1898,7 +1824,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1915,7 +1840,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1932,7 +1856,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1949,7 +1872,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1966,7 +1888,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1983,7 +1904,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2000,7 +1920,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2017,7 +1936,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2034,7 +1952,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2051,7 +1968,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2068,7 +1984,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2085,7 +2000,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2102,7 +2016,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2453,6 +2366,16 @@ "node": ">=18" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@radix-ui/react-avatar": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", @@ -2622,10 +2545,22 @@ } } }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.3", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", - "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, @@ -3598,51 +3533,6 @@ "@tsparticles/engine": "3.9.1" } }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -3674,17 +3564,47 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/@types/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3704,6 +3624,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/warning": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.4.tgz", + "integrity": "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -3711,18 +3637,57 @@ "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.29.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-rc.3", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/acorn": { @@ -4112,7 +4077,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4480,16 +4444,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-arithmetic": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", + "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, "peerDependenciesMeta": { "supports-color": { @@ -4550,6 +4535,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4562,6 +4556,16 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "license": "MIT" }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dompurify": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.4.tgz", @@ -4576,7 +4580,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4733,7 +4736,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4752,7 +4754,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4795,10 +4796,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, "license": "MIT", "bin": { "esbuild": "bin/esbuild" @@ -4858,16 +4861,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/warning": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.4.tgz", - "integrity": "sha512-CqN8MnISMwQbLJXO3doBAV4Yw9hx9/Pyr2rZ78+NfaCnhyRA/nKrpyk6E7mKw17ZOaQdLpK9GiUjrqLzBlN3sg==", - "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", - "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5433,29 +5430,11 @@ "node": ">=6.9.0" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true - }, - "node_modules/date-arithmetic": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz", - "integrity": "sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==", - "license": "MIT" - }, - "node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5472,58 +5451,8 @@ "engines": { "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/dompurify": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.4.tgz", - "integrity": "sha512-r8K7KGKEcztXfA/nfabSYB2hg9tDphORJTdf8xprN/luSLGmNhOBN8dm1/SYjqLLet6YUFEXOcrdTuwryp/Bew==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optional": true, - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-own-enumerable-property-symbols": { @@ -5537,7 +5466,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5641,6 +5569,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/globalize": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz", + "integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==" + }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -5648,10 +5581,10 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/globalthis": { @@ -5684,7 +5617,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5756,7 +5688,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6106,15 +6037,10 @@ "node": ">=0.10.0" } }, - "node_modules/globalize": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/globalize/-/globalize-0.1.1.tgz", - "integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==" - }, - "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { @@ -6591,6 +6517,13 @@ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6645,6 +6578,25 @@ "node": ">=12" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -6686,6 +6638,16 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -6726,6 +6688,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -7256,6 +7219,19 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7443,16 +7419,6 @@ "react-dom": ">=16.3.0" } }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-router": { "version": "7.15.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz", @@ -8633,6 +8599,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -8711,21 +8692,6 @@ "yarn": "*" } }, - "node_modules/uncontrollable": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", - "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.6.3", - "@types/react": ">=16.9.11", - "invariant": "^2.2.4", - "react-lifecycles-compat": "^3.0.4" - }, - "peerDependencies": { - "react": ">=15.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -8961,6 +8927,25 @@ "loose-envify": "^1.0.0" } }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/Frontendd/package.json b/Frontendd/package.json index bf69d06..346f75f 100644 --- a/Frontendd/package.json +++ b/Frontendd/package.json @@ -42,6 +42,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@playwright/test": "^1.60.0", + "@rolldown/pluginutils": "^1.0.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", @@ -51,6 +52,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "ms": "^2.1.3", "postcss": "^8.4.47", "vite": "^6.0.0", "vite-plugin-pwa": "^1.3.0" diff --git a/Frontendd/src/context/SocketContext.jsx b/Frontendd/src/context/SocketContext.jsx index 1d6fec0..76d298b 100644 --- a/Frontendd/src/context/SocketContext.jsx +++ b/Frontendd/src/context/SocketContext.jsx @@ -11,6 +11,8 @@ export const useSocket = () => { export const SocketProvider = ({ children }) => { const [socket, setSocket] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const [connectionError, setConnectionError] = useState(null); const { user } = useAuth(); useEffect(() => { @@ -18,24 +20,58 @@ export const SocketProvider = ({ children }) => { if (user) { newSocket = io(API_BASE_URL, { withCredentials: true, + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionAttempts: Infinity, + reconnectionDelay: 1000, + reconnectionDelayMax: 5000, + timeout: 20000, }); - newSocket.on('connect', () => { + const handleConnect = () => { + setIsConnected(true); + setConnectionError(null); newSocket.emit('user:join', { userId: user._id || user.id }); - }); + }; + + const handleDisconnect = (reason) => { + setIsConnected(false); + console.warn('[SocketContext] Disconnected:', reason); + }; + + const handleConnectError = (error) => { + setIsConnected(false); + setConnectionError(error.message); + console.error('[SocketContext] Connection Error:', error); + }; + + newSocket.on('connect', handleConnect); + newSocket.on('disconnect', handleDisconnect); + newSocket.on('connect_error', handleConnectError); + + if (newSocket.connected) { + setIsConnected(true); + } setSocket(newSocket); + } else { + setIsConnected(false); + setConnectionError(null); + setSocket(null); } return () => { if (newSocket) { + newSocket.off('connect', handleConnect); + newSocket.off('disconnect', handleDisconnect); + newSocket.off('connect_error', handleConnectError); newSocket.disconnect(); } }; }, [user]); return ( - + {children} ); diff --git a/Frontendd/src/pages/dashboard/CustomerDashboard.jsx b/Frontendd/src/pages/dashboard/CustomerDashboard.jsx index 6b5f7b0..924a0df 100644 --- a/Frontendd/src/pages/dashboard/CustomerDashboard.jsx +++ b/Frontendd/src/pages/dashboard/CustomerDashboard.jsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { Calendar, MapPin, Ticket, X, Download, Search, Heart, Calendar, MapPin, Ticket } from 'lucide-react'; +import { Calendar, MapPin, Ticket, X, Download, Search, Heart } from 'lucide-react'; import { io } from 'socket.io-client'; import {Calendar as BigCalendar,momentLocalizer,} from 'react-big-calendar'; import moment from 'moment'; @@ -47,9 +47,9 @@ export default function CustomerDashboard() { const [viewMode, setViewMode] = useState('grid'); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedRegistrationId, setSelectedRegistrationId] = useState(null); - const [selectedRegistrationId, setSelectedRegistrationId] = useState(null); const [highlightedEvents, setHighlightedEvents] = useState({}); const [searchQuery, setSearchQuery] = useState(() => searchParams.get('q') || ''); + const debouncedSearch = useDebounce(searchQuery, 500); const [selectedCategory, setSelectedCategory] = useState(() => searchParams.get('category') || ''); const [isFetching, setIsFetching] = useState(false); const [registrationsError, setRegistrationsError] = useState(''); @@ -130,34 +130,6 @@ export default function CustomerDashboard() { } }, [searchParams]); - const fetchAvailableEvents = useCallback(async () => { - try { - if (mountedRef.current) setLoading(true); - const token = localStorage.getItem('token'); - const res = await fetch(`${API_BASE_URL}/api/registrations/me`, { - headers: { Authorization: `Bearer ${token}` }, - }); - if (res.ok && mountedRef.current) { - const data = await res.json(); - - const upcoming = (data.events || []).filter( - (evt) => new Date(evt.date) >= new Date() - ); - - if (mountedRef.current) { - setAvailableEvents(upcoming); - } - } - } catch (error) { - console.error("Failed to fetch events:", error); - } finally { - if (mountedRef.current) { - setIsFetching(false); - setLoading(false); - } - } - }, [searchParams]); - const fetchSavedEvents = useCallback(async () => { try { const token = localStorage.getItem("token"); diff --git a/Frontendd/src/pages/dashboard/OrganizerDashboard.jsx b/Frontendd/src/pages/dashboard/OrganizerDashboard.jsx index ac66198..3e6e144 100644 --- a/Frontendd/src/pages/dashboard/OrganizerDashboard.jsx +++ b/Frontendd/src/pages/dashboard/OrganizerDashboard.jsx @@ -10,6 +10,7 @@ import { Input } from '../../components/ui/input'; import { Label } from '../../components/ui/label'; import { Textarea } from '../../components/ui/textarea'; import toast from "react-hot-toast"; +import { useSocket } from '../../context/SocketContext'; import { API_BASE_URL } from '../../config'; // Manual Check-In: helper debounce delay @@ -24,13 +25,10 @@ export default function OrganizerDashboard() { const [activeTab, setActiveTab] = useState('My Events'); const [selectedEvent, setSelectedEvent] = useState(null); const [editingEventId, setEditingEventId] = useState(null); + const { socket, isConnected } = useSocket(); // Manual Check-In states const [participants, setParticipants] = useState([]); - const [searchQuery, setSearchQuery] = useState(''); - const [debouncedQuery, setDebouncedQuery] = useState(''); - const [filterMode, setFilterMode] = useState('all'); // all | checked | pending const [loadingId, setLoadingId] = useState(null); - const [manualOpen, setManualOpen] = useState(true); const [formData, setFormData] = useState({ title: '', @@ -61,11 +59,93 @@ export default function OrganizerDashboard() { }; }, []); - // Debounce searchQuery -> debouncedQuery + // Realtime Socket.IO synchronization for organizer events useEffect(() => { - const t = setTimeout(() => setDebouncedQuery(searchQuery.trim().toLowerCase()), SEARCH_DEBOUNCE_MS); - return () => clearTimeout(t); - }, [searchQuery]); + if (!socket || events.length === 0) return; + + const joinEventRooms = () => { + events.forEach((event) => { + socket.emit('event:join', { eventId: event._id }); + }); + }; + + if (socket.connected) { + joinEventRooms(); + } + + socket.on('connect', joinEventRooms); + + const handleRegistrationCountUpdate = ({ eventId, count }) => { + setEvents((prev) => + prev.map((evt) => + evt._id === eventId ? { ...evt, registrations: count } : evt + ) + ); + }; + + socket.on('registration:count', handleRegistrationCountUpdate); + + return () => { + events.forEach((event) => { + socket.emit('event:leave', { eventId: event._id }); + }); + socket.off('connect', joinEventRooms); + socket.off('registration:count', handleRegistrationCountUpdate); + }; + }, [socket, events.length]); + + // Realtime Socket.IO synchronization for the currently managed event + useEffect(() => { + if (!socket || !selectedEvent) return; + + const joinSelectedRoom = () => { + socket.emit('event:join', { eventId: selectedEvent._id }); + }; + + if (socket.connected) { + joinSelectedRoom(); + } + + socket.on('connect', joinSelectedRoom); + + const handleAttendeeUpdate = ({ eventId, registration }) => { + if (eventId !== selectedEvent._id) return; + setParticipants((prev) => + prev.map((p) => + p._id === registration._id + ? { + ...p, + status: registration.status, + checkedIn: registration.status === 'attended', + checkinTime: + registration.status === 'attended' + ? registration.updatedAt + : null, + } + : p + ) + ); + }; + + const handleNewRegistration = (newReg) => { + setParticipants((prev) => { + if (prev.some((p) => p._id === newReg._id)) { + return prev; + } + return [...prev, newReg]; + }); + }; + + socket.on('attendee:update', handleAttendeeUpdate); + socket.on('registration:new', handleNewRegistration); + + return () => { + socket.emit('event:leave', { eventId: selectedEvent._id }); + socket.off('connect', joinSelectedRoom); + socket.off('attendee:update', handleAttendeeUpdate); + socket.off('registration:new', handleNewRegistration); + }; + }, [socket, selectedEvent]); // Fetch participants when selectedEvent changes useEffect(() => { @@ -115,28 +195,22 @@ export default function OrganizerDashboard() { }); if (res.ok && mountedRef.current) { const data = await res.json(); + const allEvents = data.events || []; + // Filter events where the organizer matches the current user - // Adjust logic based on how your backend returns data (populated organizer object vs id) - const myEvents = (data.events || []).filter( + const myEvents = allEvents.filter( e => e.organizer?._id === user?.id || e.organizer === user?.id || e.organizerId === user?.id ); - setEvents(myEvents); - calculateStats(myEvents); - // Fetch co-organized events - try { - const coRes = await fetch(`${API_BASE_URL}/api/events`, { - headers: { Authorization: `Bearer ${token}` } - }); - if (coRes.ok) { - const coData = await coRes.json(); - const coEvents = (coData.events || []).filter( - e => e.organizer?._id !== user?.id && e.organizer !== user?.id && - (e.coOrganizers || []).some(co => co._id === user?.id || co === user?.id) - ).map(e => ({ ...e, _isCoOrganized: true })); - if (coEvents.length > 0) setEvents(prev => [...prev, ...coEvents]); - } - } catch (_) {} + // Filter co-organized events + const coEvents = allEvents.filter( + e => e.organizer?._id !== user?.id && e.organizer !== user?.id && + (e.coOrganizers || []).some(co => co._id === user?.id || co === user?.id) + ).map(e => ({ ...e, _isCoOrganized: true })); + + const combinedEvents = [...myEvents, ...coEvents]; + setEvents(combinedEvents); + calculateStats(combinedEvents); } } catch (error) { console.error("Failed to fetch events", error); @@ -405,7 +479,19 @@ const handleCreateSubmit = async (e) => { Access your dashboard to manage your events and view analytics.

-
+
+ {/* Realtime Connection Status Pill */} + {isConnected ? ( + + + Realtime Sync + + ) : ( + + + Reconnecting... + + )} Organizer Dashboard @@ -1098,111 +1184,13 @@ const handleCreateSubmit = async (e) => {
{/* Manual Check-In Panel */} -
-
-
-

Manual Check-In

- Fallback if QR fails -
-
-
{participants.filter(p=>p.checkedIn).length} / {participants.length} checked in
- -
-
- - {manualOpen && ( -
-
-
- setSearchQuery(e.target.value)} - aria-label="Search participants" - /> -
-
- - - -
-
- - {/* Participant list */} -
- - - - - - - - - - - - {(() => { - const q = debouncedQuery; - let list = participants || []; - if (q) { - list = list.filter(p => ((p.name||'').toLowerCase().includes(q) || (p.email||'').toLowerCase().includes(q))); - } - if (filterMode === 'checked') list = list.filter(p => p.checkedIn === true); - if (filterMode === 'pending') list = list.filter(p => !p.checkedIn); - if (list.length === 0) { - return ( - - ); - } - return list.map(p => ( - - - - - - - - )); - })()} - -
NameEmailStatusCheck-InAction
No participants
{p.name}{p.email}{p.status || 'Registered'} - {p.checkedIn ? ( - ✓ Checked In{p.checkinTime?` (${new Date(p.checkinTime).toLocaleTimeString()})`:''} - ) : ( - Pending - )} - - {!p.checkedIn ? ( - - ) : ( - - )} -
-
-
- )} -
+
@@ -1229,3 +1217,123 @@ const handleCreateSubmit = async (e) => {
); } + +const ManualCheckInPanel = React.memo(({ participants, handleCheckin, loadingId, selectedEvent, user }) => { + const [manualOpen, setManualOpen] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [debouncedQuery, setDebouncedQuery] = useState(''); + const [filterMode, setFilterMode] = useState('all'); + + useEffect(() => { + const t = setTimeout(() => setDebouncedQuery(searchQuery.trim().toLowerCase()), 150); + return () => clearTimeout(t); + }, [searchQuery]); + + return ( +
+
+
+

Manual Check-In

+ Fallback if QR fails +
+
+
{participants.filter(p=>p.checkedIn).length} / {participants.length} checked in
+ +
+
+ + {manualOpen && ( +
+
+
+ setSearchQuery(e.target.value)} + aria-label="Search participants" + /> +
+
+ + + +
+
+ +
+ + + + + + + + + + + + {(() => { + const q = debouncedQuery; + let list = participants || []; + if (q) { + list = list.filter(p => ((p.name||'').toLowerCase().includes(q) || (p.email||'').toLowerCase().includes(q))); + } + if (filterMode === 'checked') list = list.filter(p => p.checkedIn === true); + if (filterMode === 'pending') list = list.filter(p => !p.checkedIn); + if (list.length === 0) { + return ( + + ); + } + return list.map(p => ( + + + + + + + + )); + })()} + +
NameEmailStatusCheck-InAction
No participants
{p.name}{p.email}{p.status || 'Registered'} + {p.checkedIn ? ( + ✓ Checked In{p.checkinTime?` (${new Date(p.checkinTime).toLocaleTimeString()})`:''} + ) : ( + Pending + )} + + {!p.checkedIn ? ( + + ) : ( + + )} +
+
+
+ )} +
+ ); +}); +ManualCheckInPanel.displayName = 'ManualCheckInPanel'; diff --git a/Frontendd/vite.config.js b/Frontendd/vite.config.js index 6d37527..5b4f013 100644 --- a/Frontendd/vite.config.js +++ b/Frontendd/vite.config.js @@ -37,6 +37,7 @@ export default defineConfig({ ] }, workbox: { + maximumFileSizeToCacheInBytes: 4000000, globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,woff2}'], runtimeCaching: [ { diff --git a/backend/package-lock.json b/backend/package-lock.json index fffc11c..688a345 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -2356,16 +2356,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2394,11 +2384,8 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "peerDependencies": { + "lodash": ">=4.0" } }, "node_modules/cliui/node_modules/wrap-ansi": { @@ -2420,27 +2407,15 @@ } }, "node_modules/cloudinary": { - "version": "1.41.3", - "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-1.41.3.tgz", - "integrity": "sha512-4o84y+E7dbif3lMns+p3UW6w6hLHEifbX/7zBJvaih1E9QNMZITENQ14GPYJC4JmhygYXsuuBb9bRA3xWEoOfg==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.10.0.tgz", + "integrity": "sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==", "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "lodash": "^4.17.23" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "lodash": ">=4.0" + "node": ">=9" } }, "node_modules/co": { @@ -2625,17 +2600,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-js": { - "version": "3.49.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", - "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -5517,9 +5481,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.13.tgz", - "integrity": "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw==", + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.10.tgz", + "integrity": "sha512-BLFuSth7QtHOkBzyqTehWWyub0NTRDuK2Q2SQfnGLsrJnzyU+Yeh4WpV1eZGuARFj1xQJHIdnTuJZLP+b9R1GQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -6028,17 +5992,6 @@ ], "license": "MIT" }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", - "license": "MIT", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", diff --git a/backend/src/__tests__/admin.bulk.test.js b/backend/src/__tests__/admin.bulk.test.js index 3133dad..164b83f 100644 --- a/backend/src/__tests__/admin.bulk.test.js +++ b/backend/src/__tests__/admin.bulk.test.js @@ -24,7 +24,7 @@ jest.unstable_mockModule('../utils/email.js', () => ({ sendEventRejectionEmail: mockSendEventRejectionEmail, })); -jest.unstable_mockModule('./notificationController.js', () => ({ +jest.unstable_mockModule('../controllers/notificationController.js', () => ({ createNotification: mockCreateNotification, })); diff --git a/backend/src/controllers/registrationController.js b/backend/src/controllers/registrationController.js index ce5d7aa..c99f5b1 100644 --- a/backend/src/controllers/registrationController.js +++ b/backend/src/controllers/registrationController.js @@ -8,48 +8,15 @@ import { sendEmail } from '../utils/email.js'; import path from 'path'; import { calculateRefund } from '../utils/refundPolicy.js'; import { createObjectCsvWriter } from 'csv-writer'; -import { emitRegistrationCount } from '../services/socket.js'; +import { emitRegistrationCount, emitAttendeeUpdate, emitNewRegistration } from '../services/socket.js'; import { createNotification } from './notificationController.js'; - +// Register for an event (handles capacity and waitlist status) export const registerForEvent = async (req, res) => { try { const event = await Event.findById(req.params.id); - if (!event || event.status !== 'approved') return res.status(400).json({ message: 'Event not available' }); - const payload = JSON.stringify({ userId: req.user.id, eventId: event._id, at: Date.now() }); - const qrCodeDataUrl = await generateQRCodeDataUrl(payload); - - // Current implementation includes : - // Checks for an existing cancelled registration - // Reactivating the existing registration instead of inserting a new record - // Capacity validation on event registration - // Keeps the audit trail intact while avoiding unique index conflicts - - // Check active registration - const activeRegistrations = await Registration.countDocuments({ - event: req.params.id, - status: { $ne: "cancelled" }, - }); - - // Capacity validation - if (activeRegistrations >= event.capacity && event.capacity > 0) { - return res.status(400).json({ - message: "Event is fully booked" - }) - } - - // To reinitiate the existing registered event - const existingRegistration = await Registration.findOne({ user: req.user.id, event: req.params.id }); - - if (existingRegistration) { - if (existingRegistration.status === "cancelled") { - existingRegistration.status = 'registered'; - } - if (!event || event.status !== 'approved') { - return res.status(400).json({ - message: 'Event not available', - }); + return res.status(400).json({ message: 'Event not available' }); } // Check existing registration @@ -58,48 +25,34 @@ export const registerForEvent = async (req, res) => { event: event._id, }); - // Already active + // If already active if ( existingRegistration && - ['registered', 'waitlisted', 'attended'].includes( - existingRegistration.status - ) + ['registered', 'waitlisted', 'attended'].includes(existingRegistration.status) ) { return res.status(400).json({ message: 'Already registered or waitlisted', }); } - // Atomically increment count only if under capacity - const updatedEvent = await Event.findOneAndUpdate( - { - _id: event._id, - status: 'approved', - $expr: { - $lt: ['$registeredCount', '$capacity'], - }, - }, - { - $inc: { - registeredCount: 1, - }, - }, - { - new: true, - } - ); + // Atomically check capacity and increment registeredCount only if under capacity + const query = { + _id: event._id, + status: 'approved', + }; - return res.status(201).json({ - registration: existingRegistration, - }) + if (event.capacity > 0) { + query.$expr = { + $lt: ['$registeredCount', '$capacity'], + }; } - else { - const reg = await Registration.create({ user: req.user.id, event: event._id, qrCodeDataUrl }); - try { - await sendEmail({ to: req.user.email, subject: `Registered: ${event.title}`, html: `

You are registered for ${event.title}.

` }); - } catch (_) { } - // Event is full — reject immediately, no registration created + const updatedEvent = await Event.findOneAndUpdate( + query, + { $inc: { registeredCount: 1 } }, + { new: true } + ); + if (!updatedEvent) { return res.status(400).json({ message: 'Event is full', @@ -111,59 +64,45 @@ export const registerForEvent = async (req, res) => { eventId: event._id, at: Date.now(), }); - - const qrCodeDataUrl = - await generateQRCodeDataUrl(payload); + const qrCodeDataUrl = await generateQRCodeDataUrl(payload); let registration; - - // Reuse cancelled registration - if ( - existingRegistration && - existingRegistration.status === 'cancelled' - ) { + // Reuse cancelled registration if it exists + if (existingRegistration && existingRegistration.status === 'cancelled') { existingRegistration.status = 'registered'; - existingRegistration.qrCodeDataUrl = - qrCodeDataUrl; - - registration = - await existingRegistration.save(); + existingRegistration.qrCodeDataUrl = qrCodeDataUrl; + registration = await existingRegistration.save(); } else { - try { - registration = await Registration.create({ - user: req.user.id, - event: event._id, - qrCodeDataUrl, - status: 'registered', - }); - } catch (dupErr) { - if (dupErr.code === 11000) { - await Event.findByIdAndUpdate( - event._id, - { - $inc: { - registeredCount: -1, - }, - } - ); - - return res.status(400).json({ - message: - 'Already registered or waitlisted', - }); - } - - throw dupErr; - } + registration = await Registration.create({ + user: req.user.id, + event: event._id, + qrCodeDataUrl, + status: 'registered', + }); } + // Emit new registration count to socket rooms + emitRegistrationCount(updatedEvent._id, updatedEvent.registeredCount); - // Send email - emitRegistrationCount( - updatedEvent._id, - updatedEvent.registeredCount, - ); + // Broadcast the new registration to the event room in real time + try { + await registration.populate('user', 'name email'); + const flatRegistration = { + _id: registration._id, + userId: registration.user?._id || registration.user, + name: registration.user?.name || 'Unknown', + email: registration.user?.email || 'N/A', + status: registration.status, + checkedIn: registration.status === 'attended', + checkinTime: registration.status === 'attended' ? registration.updatedAt : null, + createdAt: registration.createdAt, + }; + emitNewRegistration(updatedEvent._id, flatRegistration); + } catch (broadcastErr) { + console.error('Failed to broadcast new registration:', broadcastErr); + } + // Send confirmation email try { await sendEmail({ to: req.user.email, @@ -172,7 +111,7 @@ export const registerForEvent = async (req, res) => { }); } catch (_) {} - // Send notification + // Send confirmed notification try { await createNotification( req.user.id, @@ -190,176 +129,121 @@ export const registerForEvent = async (req, res) => { }); } catch (err) { console.error('ERROR:', err); - res.status(500).json({ message: err.message, }); } }; -// Fetch registrations with waitlist position -export const myRegistrations = async ( - req, - res -) => { +// Fetch current user's registrations +export const myRegistrations = async (req, res) => { try { const regs = await Registration.find({ user: req.user.id }).populate('event'); - - - const payload = JSON.stringify({ userId: req.user.id, eventId: event._id, at: Date.now() }); - const qrCodeDataUrl = await generateQRCodeDataUrl(payload); - - // Current implementation includes : - // Checks for an existing cancelled registration - // Reactivating the existing registration instead of inserting a new record - // Capacity validation on event registration - // Keeps the audit trail intact while avoiding unique index conflicts - - // Check active registration - const activeRegistrations = await Registration.countDocuments({ - event: req.params.id, - status: { $ne: "cancelled" }, + res.json({ + registrations: regs, }); - - // Capacity validation - if (activeRegistrations>=event.capacity && event.capacity>0){ - return res.status(400).json({ - message:"Event is fully booked" - }) - } - - // To reinitiate the existing registered event - const existingRegistration = await Registration.findOne({user:req.user.id,event:req.params.id}); - - if (existingRegistration){ - if (existingRegistration.status==="cancelled"){ - existingRegistration.status = 'registered'; - } - - await existingRegistration.save(); - try { - await sendEmail({ to: req.user.email, subject: `Registered: ${event.title}`, html: `

You are registered for ${event.title}.

` }); - } catch (_) { } - - return res.status(201).json({ - registration:existingRegistration, - }) - } - - else{ - const reg = await Registration.create({ user: req.user.id, event: event._id, qrCodeDataUrl }); - try { - await sendEmail({ to: req.user.email, subject: `Registered: ${event.title}`, html: `

You are registered for ${event.title}.

` }); - } catch (_) { } - - res.status(201).json({ registration: reg }); - } - - - } catch (err) { - console.error("ERROR:", err); + console.error('ERROR:', err); res.status(500).json({ message: err.message }); } }; -// fetching registrations with waiting position - - +// Get participants list mapped as flat structures for the organizer dashboard export const participantsForEvent = async (req, res) => { try { const regs = await Registration.find({ event: req.params.id, }).populate('user', 'name email'); + const mapped = regs.map((r) => ({ + _id: r._id, + userId: r.user?._id || r.user, + name: r.user?.name || 'Unknown', + email: r.user?.email || 'N/A', + status: r.status, + checkedIn: r.status === 'attended', + checkinTime: r.status === 'attended' ? r.updatedAt : null, + createdAt: r.createdAt, + })); + res.json({ - participants: regs, + participants: mapped, }); - } catch (err) { console.error('ERROR:', err); - res.status(500).json({ message: err.message, }); } }; -// Get participants for organizer/admin -export const participantsForEvent = - async (req, res) => { - try { - const regs = await Registration.find({ - event: req.params.id, - }).populate('user', 'name email'); - - res.json({ - participants: regs, +// Secure check-in handler supporting registration ID or event/user ID queries +export const checkInParticipant = async (req, res) => { + try { + if (!req.user) { + return res.status(401).json({ + message: 'Unauthorized: user not authenticated', }); - } catch (err) { - console.error('ERROR:', err); + } + + const validStatuses = ['attended', 'cancelled', 'no-show']; + const status = (req.body?.status || 'attended') + .toString() + .trim() + .toLowerCase(); - res.status(500).json({ - message: err.message, + if (!validStatuses.includes(status)) { + return res.status(400).json({ + message: 'Invalid status', }); } - }; -// Secure check-in handler -export const checkInParticipant = - async (req, res) => { - try { - if (!req.user) { - return res.status(401).json({ - message: - 'Unauthorized: user not authenticated', - }); - } + // Check query format: Search by event + user ID if provided, otherwise assume reg ID + let query = {}; + if (req.body && req.body.userId) { + query = { event: req.params.id, user: req.body.userId }; + } else { + query = { _id: req.params.id }; + } - if (!req.body || !req.body.userId) { - return res.status(400).json({ - message: - 'Bad Request: userId is required', - }); - } + const registration = await Registration.findOne(query).populate('event'); + if (!registration) { + return res.status(404).json({ message: 'Registration not found' }); + } - const validStatuses = [ - 'attended', - 'cancelled', - 'no-show', - ]; - - const status = ( - req.body.status || 'attended' - ) - .toString() - .trim() - .toLowerCase(); - - if (!validStatuses.includes(status)) { - return res.status(400).json({ - message: 'Invalid status', - }); - } + // Ownership check: organizer, co-organizers, or admin only + const isOrganizer = + registration.event?.organizer?.toString() === req.user.id || + registration.event?.coOrganizers?.some((co) => co.toString() === req.user.id); + const isAdmin = req.user.role === 'admin'; - // Perform atomic update - const registration = await Registration.findOneAndUpdate( - { event: req.params.id, user: req.body.userId }, - { status }, - { new: true } - ); - if (!registration) { - return res.status(404).json({ message: 'Registration not found for this user and event' }); + if (!isOrganizer && !isAdmin) { + return res.status(403).json({ + message: 'Forbidden: Only the event organizer, co-organizers, or admin can check in participants', + }); } - return res.json({ message: 'Check-in updated', registration }); + + registration.status = status; + await registration.save(); + + // Populate user info for socket updates and scanner response + await registration.populate('user', 'name email'); + + // Broadcast check-in update to event room + emitAttendeeUpdate(registration.event._id, registration); + + return res.json({ + message: 'Check-in updated', + registration, + attendeeName: registration.user?.name, + }); } catch (err) { console.error('[checkInParticipant] Error:', err); return res.status(500).json({ message: err.message }); } }; - - +// Export attendee lists as CSV export const exportParticipantsCsv = async (req, res) => { try { const regs = await Registration.find({ event: req.params.id }).populate('user', 'name email'); @@ -387,15 +271,19 @@ export const exportParticipantsCsv = async (req, res) => { } }; - isWaitlisted: - registration?.status === - 'waitlisted', +// Check if a user is registered or waitlisted for an event +export const checkRegistrationStatus = async (req, res) => { + try { + const registration = await Registration.findOne({ + event: req.params.id, + user: req.user.id, + }); res.json({ - isRegistered: registration?.status === 'registered', + isRegistered: registration?.status === 'registered' || registration?.status === 'attended', isWaitlisted: registration?.status === 'waitlisted', registration, - event: req.params.id + event: req.params.id, }); } catch (err) { console.error('ERROR:', err); @@ -403,8 +291,7 @@ export const exportParticipantsCsv = async (req, res) => { } }; - -// promoting from waitlist to registered +// Promote a participant from the waitlist to registered status export const promoteFromWaitlist = async (eventId) => { const nextRegistration = await Registration.findOne({ event: eventId, @@ -413,54 +300,55 @@ export const promoteFromWaitlist = async (eventId) => { .sort({ createdAt: 1 }) .populate('user') .populate('event'); - if (!nextRegistration) { - return; -} - const payload = JSON.stringify({ - userId: - nextRegistration.user._id, - eventId: - nextRegistration.event._id, - at: Date.now(), - }); + if (!nextRegistration) { + return false; + } - const qrCodeDataUrl = - await generateQRCodeDataUrl( - payload - ); + const payload = JSON.stringify({ + userId: nextRegistration.user._id, + eventId: nextRegistration.event._id, + at: Date.now(), + }); - nextRegistration.status = - 'registered'; + const qrCodeDataUrl = await generateQRCodeDataUrl(payload); - nextRegistration.qrCodeDataUrl = - qrCodeDataUrl; + nextRegistration.status = 'registered'; + nextRegistration.qrCodeDataUrl = qrCodeDataUrl; + await nextRegistration.save(); - await nextRegistration.save(); + // Atomically increment registeredCount on Event + await Event.findByIdAndUpdate(eventId, { $inc: { registeredCount: 1 } }); - try { - await sendEmail({ - to: nextRegistration.user.email, - subject: `Spot Confirmed: ${nextRegistration.event.title}`, - html: ` + try { + await sendEmail({ + to: nextRegistration.user.email, + subject: `Spot Confirmed: ${nextRegistration.event.title}`, + html: `

You have been promoted from the waitlist.

Your registration for ${nextRegistration.event.title} is now confirmed.

`, - }); - } catch (_) {} + }); + } catch (_) {} - try { - await createNotification( - nextRegistration.user._id, - 'waitlist_promoted', - `Good news! A spot opened up for ${nextRegistration.event.title}`, - `/events/${nextRegistration.event._id}` - ); - } catch (notifErr) { - console.error('Failed to create waitlist notification:', notifErr); - } - }; + try { + await createNotification( + nextRegistration.user._id, + 'waitlist_promoted', + `Good news! A spot opened up for ${nextRegistration.event.title}`, + `/events/${nextRegistration.event._id}` + ); + } catch (notifErr) { + console.error('Failed to create waitlist notification:', notifErr); + } + // Notify organizer of status update + emitAttendeeUpdate(eventId, nextRegistration); + + return true; +}; + +// Cancel an event registration (releases spots and triggers waitlist promotion) export const cancelRegistration = async (req, res) => { try { const { id } = req.params; @@ -472,18 +360,73 @@ export const cancelRegistration = async (req, res) => { if (registration.user.toString() !== userId) { return res.status(403).json({ message: 'Unauthorized' }); } - if (registration.status === 'cancelled') { + const previousStatus = registration.status; + if (previousStatus === 'cancelled') { return res.status(400).json({ message: 'Already cancelled' }); } const eventDate = new Date(registration.event.date); if (eventDate < new Date()) { return res.status(400).json({ message: 'Cannot cancel past events' }); } + registration.status = 'cancelled'; await registration.save(); + + const event = await Event.findById(registration.event._id); + if (event) { + // Decrement count and promote only if they had a confirmed spot + if (previousStatus === 'registered' || previousStatus === 'attended') { + event.registeredCount = Math.max(0, event.registeredCount - 1); + await event.save(); + + // Atomically promote next waitlisted user + await promoteFromWaitlist(event._id); + } + + // Fetch final updated count + const updatedEvent = await Event.findById(event._id); + emitRegistrationCount(updatedEvent._id, updatedEvent.registeredCount); + } + + // Broadcast cancelled status to organizer + await registration.populate('user', 'name email'); + emitAttendeeUpdate(registration.event._id, registration); + res.status(200).json({ message: 'Registration cancelled successfully', registration }); } catch (error) { console.error('ERROR:', error); res.status(500).json({ message: error.message }); } }; + +// Check refund status for a registration +export const checkRefundStatus = async (req, res) => { + try { + const registration = await Registration.findById(req.params.id); + if (!registration) { + return res.status(404).json({ message: 'Registration not found' }); + } + res.json({ + refundStatus: registration.refundStatus || 'not_applicable', + refundAmount: registration.refundAmount || 0, + refundedAt: registration.refundedAt || null, + refundId: registration.refundId || null, + }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +// Calculate potential refund policy terms +export const checkRefundPolicy = async (req, res) => { + try { + const registration = await Registration.findById(req.params.id).populate('event'); + if (!registration) { + return res.status(404).json({ message: 'Registration not found' }); + } + const refund = calculateRefund(registration.event.date, registration.event.price || 0); + res.json(refund); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; diff --git a/backend/src/routes/notificationRoutes.js b/backend/src/routes/notificationRoutes.js index d666fdc..3277c52 100644 --- a/backend/src/routes/notificationRoutes.js +++ b/backend/src/routes/notificationRoutes.js @@ -1,5 +1,5 @@ import express from 'express'; -import { protect } from '../middleware/auth.js'; +import { authenticate } from '../middleware/auth.js'; import { getNotifications, markNotificationAsRead, @@ -10,7 +10,7 @@ import { const router = express.Router(); -router.use(protect); // All routes below are protected +router.use(authenticate); // All routes below are protected router.get('/', getNotifications); router.get('/unread-count', getUnreadCount); diff --git a/backend/src/routes/registrationRoutes.js b/backend/src/routes/registrationRoutes.js index d514166..c9060b1 100644 --- a/backend/src/routes/registrationRoutes.js +++ b/backend/src/routes/registrationRoutes.js @@ -2,18 +2,23 @@ import { Router } from 'express'; import { authenticate } from '../middleware/auth.js'; import { authorizeRoles } from '../middleware/roles.js'; import { registrationRateLimiter } from '../middleware/rateLimiters.js'; -import { registerForEvent, myRegistrations, participantsForEvent, checkInParticipant, exportParticipantsCsv, checkRegistrationStatus, cancelRegistration } from '../controllers/registrationController.js'; - - - - - +import { + registerForEvent, + myRegistrations, + participantsForEvent, + checkInParticipant, + exportParticipantsCsv, + checkRegistrationStatus, + cancelRegistration, + checkRefundStatus, + checkRefundPolicy, +} from '../controllers/registrationController.js'; const router = Router(); router.post( '/:id/register', - registrationLimiter, + registrationRateLimiter, authenticate, authorizeRoles('customer', 'organizer', 'admin'), registerForEvent @@ -26,14 +31,10 @@ router.post('/:id/checkin', authenticate, authorizeRoles('customer', 'organizer' router.get('/:id/participants.csv', authenticate, authorizeRoles('customer', 'organizer', 'admin'), exportParticipantsCsv); // End point to cancel registration -router.delete("/:id/cancel",authenticate,cancelRegistration); +router.delete("/:id/cancel", authenticate, cancelRegistration); // End point to check refund status -router.get("/:id/refund-status",authenticate,checkRefundStatus); +router.get("/:id/refund-status", authenticate, checkRefundStatus); // End point to check refund policy -router.get("/:id/refund-policy",authenticate,checkRefundPolicy) - -// End point to cancel registration -router.delete("/:id/cancel",authenticate,cancelRegistration); - +router.get("/:id/refund-policy", authenticate, checkRefundPolicy); export default router; diff --git a/backend/src/server.js b/backend/src/server.js index 65cbf55..748c48e 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -1,62 +1,20 @@ -import authRoutes from './routes/authRoutes.js'; -import eventRoutes from './routes/eventRoutes.js'; -import registrationRoutes from './routes/registrationRoutes.js'; -import reviewRoutes from './routes/reviewRoutes.js'; -import adminRoutes from './routes/adminRoutes.js'; -import statsRoutes from './routes/statsRoutes.js'; -import userRoutes from './routes/userRoutes.js'; import http from 'http'; -import helmet from 'helmet'; import app from './app.js'; -import cors from 'cors'; -import helmet from 'helmet'; -import cors from 'cors'; -import morgan from 'morgan'; -import compression from 'compression'; -import cookieParser from 'cookie-parser'; -import rateLimit from 'express-rate-limit'; -import app from './app.js'; - - import { env } from './config/env.js'; import { connectDB } from './config/db.js'; - -import { initSocket } from './services/socket.js'; - - - import { initSocket } from './services/socket.js'; const server = http.createServer(app); -// routes (keep only if NOT already in app.js) app.get('/api/health', (req, res) => { res.json({ status: 'ok' }); }); -app.use('/api/auth', authRoutes); -app.use('/api/events', eventRoutes); -app.use('/api/registrations', registrationRoutes); -app.use('/api/reviews', reviewRoutes); -app.use('/api/admin', adminRoutes); -app.use('/api/stats', statsRoutes); -app.use('/api/users', userRoutes); - -// 404 -app.use((req, res) => { - res.status(404).json({ message: 'Route not found' }); -}); - -// error handler -app.use((err, req, res, next) => { - console.error('Error:', err); - res.status(err.status || 500).json({ - message: err.message || 'Server error', - }); -}); - async function start() { - //await connectDB(); + await connectDB(); + + // Initialize Socket.IO + initSocket(server, env.clientUrl); server.listen(env.port, () => { console.log(`Server running on http://localhost:${env.port}`); diff --git a/backend/src/services/socket.js b/backend/src/services/socket.js index 468c22e..2554f17 100644 --- a/backend/src/services/socket.js +++ b/backend/src/services/socket.js @@ -20,22 +20,20 @@ export function initSocket(server, clientOrigin) { socket.on('event:join', (payload = {}) => { const eventId = payload?.eventId; - if (!eventId) { return; } - socket.join(getEventRoom(eventId)); }); socket.on('event:leave', (payload = {}) => { const eventId = payload?.eventId; - if (!eventId) { return; } - socket.leave(getEventRoom(eventId)); + }); + socket.on('user:join', (payload = {}) => { const userId = payload?.userId; if (!userId) { @@ -66,3 +64,20 @@ export function emitNotification(userId, notificationData) { ioInstance.to(`user_${userId}`).emit('notification:new', notificationData); } +export function emitAttendeeUpdate(eventId, registration) { + if (!ioInstance || !eventId) { + return; + } + ioInstance.to(getEventRoom(eventId)).emit('attendee:update', { + eventId: String(eventId), + registration, + }); +} + +export function emitNewRegistration(eventId, registration) { + if (!ioInstance || !eventId) { + return; + } + ioInstance.to(getEventRoom(eventId)).emit('registration:new', registration); +} + From 8bc666386e2b95c6ec5ef0b85c493b7199f24645 Mon Sep 17 00:00:00 2001 From: Bhuvanesh S Date: Wed, 3 Jun 2026 21:33:07 +0530 Subject: [PATCH 2/2] feat: improve authentication security and route protection workflows --- Frontendd/src/App.jsx | 12 +++- Frontendd/src/context/AuthContext.jsx | 12 ++-- Frontendd/src/pages/SignIn.jsx | 7 +- backend/src/app.js | 12 +++- backend/src/middleware/auth.js | 16 ++++- .../src/middleware/validationMiddleware.js | 67 +++++++++++++++++++ backend/src/routes/eventRoutes.js | 9 +-- backend/src/routes/reviewRoutes.js | 4 +- backend/src/utils/errors.js | 12 ++++ 9 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 backend/src/utils/errors.js diff --git a/Frontendd/src/App.jsx b/Frontendd/src/App.jsx index 2ffc942..338df1d 100644 --- a/Frontendd/src/App.jsx +++ b/Frontendd/src/App.jsx @@ -3,6 +3,7 @@ import { Routes, Route, Navigate, + useLocation, } from "react-router-dom"; import { useEffect, useState } from "react"; import "./index.css"; @@ -33,6 +34,7 @@ import QRScanner from "./pages/dashboard/QRScanner"; // Protected Route Component const ProtectedRoute = ({ children, allowedRoles }) => { const { user, loading } = useAuth(); + const location = useLocation(); if (loading) { return ( @@ -46,10 +48,16 @@ const ProtectedRoute = ({ children, allowedRoles }) => { } if (!user) { - return ; + return ; } - if (allowedRoles && !allowedRoles.includes(user.role)) { + const hasAccess = !allowedRoles || allowedRoles.some(r => { + if (r === 'customer' && user.role === 'attendee') return true; + if (r === 'attendee' && user.role === 'customer') return true; + return r === user.role; + }); + + if (!hasAccess) { return ; } diff --git a/Frontendd/src/context/AuthContext.jsx b/Frontendd/src/context/AuthContext.jsx index c08d4d5..fa51c6b 100644 --- a/Frontendd/src/context/AuthContext.jsx +++ b/Frontendd/src/context/AuthContext.jsx @@ -1,5 +1,6 @@ import React, { createContext, useState, useEffect, useContext, useRef } from 'react'; import { API_BASE_URL } from '../config'; +import toast from 'react-hot-toast'; const AuthContext = createContext(null); @@ -18,18 +19,16 @@ export const AuthProvider = ({ children }) => { if (response.ok && mountedRef.current) { const userData = await response.json(); setUser(userData.user); - } else if (!response.ok) { + } else if (response.status === 401 || response.status === 403) { localStorage.removeItem('token'); if (mountedRef.current) { setUser(null); } + toast.error('Session expired. Please log in again.'); } } catch (error) { - console.error('Failed to fetch user', error); - localStorage.removeItem('token'); - if (mountedRef.current) { - setUser(null); - } + console.error('Failed to fetch user due to network issue', error); + // Do not delete token on network error to allow reconnection } finally { if (mountedRef.current) { setLoading(false); @@ -37,6 +36,7 @@ export const AuthProvider = ({ children }) => { } }; + useEffect(() => { let mounted = true; diff --git a/Frontendd/src/pages/SignIn.jsx b/Frontendd/src/pages/SignIn.jsx index 080baba..737c01f 100644 --- a/Frontendd/src/pages/SignIn.jsx +++ b/Frontendd/src/pages/SignIn.jsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Link, useNavigate } from "react-router-dom"; +import { Link, useNavigate, useLocation } from "react-router-dom"; import { motion } from "framer-motion"; import { Eye, EyeOff } from "lucide-react"; import { useAuth } from "../context/AuthContext"; @@ -14,6 +14,7 @@ export default function SignIn() { const [password, setPassword] = useState(''); const { login } = useAuth(); const navigate = useNavigate(); + const location = useLocation(); const toggleVisibility = () => setIsVisible(!isVisible); @@ -39,7 +40,9 @@ const handleSubmit = async (e) => { id: loadingToast, }); - navigate('/'); + const from = location.state?.from?.pathname || '/'; + navigate(from, { replace: true }); + } else { toast.error(data.message || 'Login failed', { id: loadingToast, diff --git a/backend/src/app.js b/backend/src/app.js index a2188e3..c950db6 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -54,16 +54,22 @@ app.use('/api/notifications', notificationRoutes); // 404 handler app.use((req, res) => { - res.status(404).json({ message: 'Route not found' }); + res.status(404).json({ + success: false, + message: 'Route not found', + }); }); // Global error handler app.use((err, req, res, next) => { console.error('Error:', err); - res.status(err.status || 500).json({ - message: err.message || 'Server error' + const statusCode = err.statusCode || err.status || 500; + res.status(statusCode).json({ + success: false, + message: err.message || 'Server error', }); }); + export default app; diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index 1f8f070..a6f05d8 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -1,13 +1,24 @@ import jwt from 'jsonwebtoken'; import { env } from '../config/env.js'; +import User from '../models/User.js'; -export function authenticate(req, res, next) { +export async function authenticate(req, res, next) { const authHeader = req.headers.authorization || ''; const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null; if (!token) return res.status(401).json({ message: 'Unauthorized' }); try { const decoded = jwt.verify(token, env.jwtSecret); - req.user = decoded; + + // Check if user still exists and is not blocked + const user = await User.findById(decoded.id); + if (!user) { + return res.status(401).json({ message: 'User no longer exists' }); + } + if (user.isBlocked) { + return res.status(403).json({ message: 'User is blocked' }); + } + + req.user = user; next(); } catch (err) { return res.status(401).json({ message: 'Invalid token' }); @@ -15,3 +26,4 @@ export function authenticate(req, res, next) { } + diff --git a/backend/src/middleware/validationMiddleware.js b/backend/src/middleware/validationMiddleware.js index c021341..1703152 100644 --- a/backend/src/middleware/validationMiddleware.js +++ b/backend/src/middleware/validationMiddleware.js @@ -41,12 +41,78 @@ export const loginValidation = [ .withMessage('Password is required'), ]; +export const eventValidation = [ + body('title') + .trim() + .notEmpty() + .withMessage('Title is required') + .isLength({ min: 3 }) + .withMessage('Title must be at least 3 characters long'), + + body('description') + .trim() + .notEmpty() + .withMessage('Description is required'), + + body('category') + .trim() + .notEmpty() + .withMessage('Category is required') + .isIn(['Tech', 'Sports', 'Cultural', 'Workshop', 'Business']) + .withMessage('Category must be one of: Tech, Sports, Cultural, Workshop, Business'), + + body('date') + .notEmpty() + .withMessage('Date is required') + .isISO8601() + .withMessage('Please provide a valid ISO8601 date'), + + body('location') + .trim() + .notEmpty() + .withMessage('Location is required'), + + body('capacity') + .optional() + .isInt({ min: 0 }) + .withMessage('Capacity must be a non-negative integer'), + + body('price') + .optional() + .isFloat({ min: 0 }) + .withMessage('Price must be a non-negative number'), +]; + +export const reviewValidation = [ + body('rating') + .notEmpty() + .withMessage('Rating is required') + .isInt({ min: 1, max: 5 }) + .withMessage('Rating must be an integer between 1 and 5'), + + body('comment') + .optional() + .trim() + .isString() + .withMessage('Comment must be a string'), +]; + +export const coOrganizerValidation = [ + body('email') + .trim() + .notEmpty() + .withMessage('Email is required') + .isEmail() + .withMessage('Please provide a valid email'), +]; + export const validate = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ success: false, + message: 'Validation failed', errors: errors.array().map((err) => ({ field: err.path, message: err.msg, @@ -56,3 +122,4 @@ export const validate = (req, res, next) => { next(); }; + diff --git a/backend/src/routes/eventRoutes.js b/backend/src/routes/eventRoutes.js index ac44ef6..4171197 100644 --- a/backend/src/routes/eventRoutes.js +++ b/backend/src/routes/eventRoutes.js @@ -13,17 +13,20 @@ import { addCoOrganizer, removeCoOrganizer } from '../controllers/eventController.js'; +import { eventValidation, coOrganizerValidation, validate } from '../middleware/validationMiddleware.js'; const router = Router(); router.get('/', listEvents); router.get('/tags/popular', getPopularTags); router.get('/:id', getEvent); -router.post('/', authenticate, authorizeRoles('organizer', 'admin'), upload.single('poster'), createEvent); +router.post('/', authenticate, authorizeRoles('organizer', 'admin'), upload.single('poster'), eventValidation, validate, createEvent); router.post('/:id/remind', authenticate, authorizeRoles('organizer'), sendEventReminders); router.post( "/:id/co-organizers", authenticate, + coOrganizerValidation, + validate, addCoOrganizer ); @@ -32,9 +35,7 @@ router.delete( authenticate, removeCoOrganizer ); -router.put('/:id', authenticate, authorizeRoles('organizer', 'admin'), upload.single('poster'), updateEvent); +router.put('/:id', authenticate, authorizeRoles('organizer', 'admin'), upload.single('poster'), eventValidation, validate, updateEvent); router.delete('/:id', authenticate, authorizeRoles('organizer', 'admin'), deleteEvent); export default router; - - diff --git a/backend/src/routes/reviewRoutes.js b/backend/src/routes/reviewRoutes.js index fd528a5..9506051 100644 --- a/backend/src/routes/reviewRoutes.js +++ b/backend/src/routes/reviewRoutes.js @@ -2,12 +2,14 @@ import { Router } from 'express'; import { authenticate } from '../middleware/auth.js'; import { authorizeRoles } from '../middleware/roles.js'; import { addReview, listReviews } from '../controllers/reviewController.js'; +import { reviewValidation, validate } from '../middleware/validationMiddleware.js'; const router = Router(); router.get('/:id', listReviews); -router.post('/:id', authenticate, authorizeRoles('customer', 'organizer', 'admin'), addReview); +router.post('/:id', authenticate, authorizeRoles('customer', 'organizer', 'admin'), reviewValidation, validate, addReview); export default router; + diff --git a/backend/src/utils/errors.js b/backend/src/utils/errors.js new file mode 100644 index 0000000..de04d7f --- /dev/null +++ b/backend/src/utils/errors.js @@ -0,0 +1,12 @@ +export class AppError extends Error { + constructor(message, statusCode) { + super(message); + this.statusCode = statusCode; + this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error'; + this.isOperational = true; + + Error.captureStackTrace(this, this.constructor); + } +} + +export default AppError;