diff --git a/package-lock.json b/package-lock.json index cd683ee..0ea3d47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,10 @@ "name": "gpi-app", "version": "1.0.0", "dependencies": { - "@clerk/clerk-react": "^5.59.6", - "@clerk/localizations": "^3.35.3", + "@clerk/clerk-react": "^5.61.4", + "@logto/node": "^2.4.0", + "@supabase/supabase-js": "^2.47.0", "@tailwindcss/postcss": "^4.1.18", - "@types/mongoose": "^5.11.96", "@types/uuid": "^10.0.0", "@vercel/speed-insights": "^1.3.1", "axios": "^1.13.2", @@ -22,9 +22,9 @@ "dotenv": "^17.2.3", "enhanced-resolve": "^5.18.4", "express": "^5.2.1", + "jose": "^5.2.0", "lucide-react": "^0.562.0", - "mongodb": "^7.0.0", - "mongoose": "^9.1.5", + "mongodb": "^7.1.1", "multer": "^2.0.2", "pdf-parse": "^1.1.1", "prop-types": "^15.8.1", @@ -42,7 +42,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", + "@types/express": "^5.0.0", "@types/multer": "^2.0.0", "@types/node": "^24.10.1", "@types/react": "^19.2.5", @@ -55,6 +55,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "mongoose": "^8.23.0", "nodemon": "^3.1.11", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", @@ -135,7 +136,6 @@ "version": "7.28.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1638,10 +1638,12 @@ } }, "node_modules/@clerk/clerk-react": { - "version": "5.59.6", + "version": "5.61.4", + "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.61.4.tgz", + "integrity": "sha512-xGvQvzfc5pQEuqCW8CNUgnlR+9nt6gSSMGMYx3l972utIJrFKByQJFCRZpwYBvAHiveuK11Wgy3J39p904jb+w==", "license": "MIT", "dependencies": { - "@clerk/shared": "^3.43.2", + "@clerk/shared": "^3.47.3", "tslib": "2.8.1" }, "engines": { @@ -1652,22 +1654,10 @@ "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, - "node_modules/@clerk/localizations": { - "version": "3.35.3", - "resolved": "https://registry.npmjs.org/@clerk/localizations/-/localizations-3.35.3.tgz", - "integrity": "sha512-RxxxKyj4aXGq8GO+2+n/YsPg5Q9xGKO/T1grMxOne8CNZXLcRniIXomL6hcTjHaQ4ZNPuNvQRt8YAcu5g01tWw==", - "license": "MIT", - "dependencies": { - "@clerk/types": "^4.101.14" - }, - "engines": { - "node": ">=18.17.0" - } - }, "node_modules/@clerk/shared": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.44.0.tgz", - "integrity": "sha512-kH+chNeZwqml3IDpWLgebWECfOZifyUQO4OISd/96w1EuCY1Bzw6cBq/ZbpsoO8jyG8/6bGr/MGXLhDzTrpPfA==", + "version": "3.47.3", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.47.3.tgz", + "integrity": "sha512-jG0wMIZuuc8zaKieg9Os8ocTphG+llluRukUUdyVnu4+ZI1syVf+dkpDP3ZK69yLavTX3D0KAmkmQqTPzQV/Nw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1694,18 +1684,6 @@ } } }, - "node_modules/@clerk/types": { - "version": "4.101.14", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.101.14.tgz", - "integrity": "sha512-jl7DywmeaZx1IntgEXcjDZq2uyk+X/1yAZOjxOboeGTS0rNTiQNhv7xK8tFVjexsUAFrYlwC1AxhFuJiMDQjow==", - "license": "MIT", - "dependencies": { - "@clerk/shared": "^3.44.0" - }, - "engines": { - "node": ">=18.17.0" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "dev": true, @@ -2508,6 +2486,39 @@ "sisteransi": "^1.0.5" } }, + "node_modules/@logto/client": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@logto/client/-/client-2.8.1.tgz", + "integrity": "sha512-tUQC36l9U3knrTicXFjd+FiBqwG1/KSGn1o3wx9DFn+5iSTQa66B+Y88GaXcxAYsgjzmSHrDY3qxuQg729mleQ==", + "license": "MIT", + "dependencies": { + "@logto/js": "^4.2.0", + "@silverhand/essentials": "^2.8.7", + "camelcase-keys": "^7.0.1", + "jose": "^5.2.2" + } + }, + "node_modules/@logto/js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@logto/js/-/js-4.2.0.tgz", + "integrity": "sha512-kse29kNKuM/tw30YcOf2eOQuFBlGPZTr4rKI/LpdTKhk0fLGM5ZJKPQWNPiPFZdBfTldERXVNtd17/bZLWc/OQ==", + "license": "MIT", + "dependencies": { + "@silverhand/essentials": "^2.8.7", + "camelcase-keys": "^7.0.1" + } + }, + "node_modules/@logto/node": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/@logto/node/-/node-2.5.8.tgz", + "integrity": "sha512-QNVWIZf6sLAbF6rDT/wAG64S4IH5OQYk38fJvJYuIuMRHhUDf2VD+ENZ5BBwX3sOHFf6OM4rJVt2vWVJ8kTVow==", + "license": "MIT", + "dependencies": { + "@logto/client": "^2.8.1", + "@silverhand/essentials": "^2.8.7", + "js-base64": "^3.7.4" + } + }, "node_modules/@mapbox/node-pre-gyp": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", @@ -2544,7 +2555,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.4.5", + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -3042,6 +3055,16 @@ "win32" ] }, + "node_modules/@silverhand/essentials": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@silverhand/essentials/-/essentials-2.9.3.tgz", + "integrity": "sha512-OM9pyGc/yYJMVQw+fFOZZaTHXDWc45sprj+ky+QjC9inhf5w51L1WBmzAwFuYkHAwO1M19fxVf2sTH9KKP48yg==", + "license": "MIT", + "engines": { + "node": ">=18.12.0", + "pnpm": "^10.0.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "license": "MIT" @@ -3050,6 +3073,92 @@ "version": "0.3.0", "license": "MIT" }, + "node_modules/@supabase/auth-js": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.101.0.tgz", + "integrity": "sha512-00v22bzJ1LvLPQFZ8OKV5Qb1z2UkglyADQPh3PWcvUvHgAL86FdQrtMu6FewjU0CeROMpWQ4F/ExYhKKK45D0Q==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.101.0.tgz", + "integrity": "sha512-oEdCj5GmIGQwjII1fcbb/+hvUF94ZQmeFmFRoToz5Gbf2T8KPTX4vtanUmED+ekTB9Tyfap1IXFUx7klQprIaw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.0.tgz", + "integrity": "sha512-RHSx8bHS02xwfHdAbX5Lpbo6PXbgyf7lTaXTlwtFDPwOIw64NnVRwFAXGojHhjtVYI+PEPNSWwkL90f4agN3bw==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.101.0.tgz", + "integrity": "sha512-CJVsIdzRkEwH5F1NAwVq/Ewh0T/LpEpYro5hQKhfRqtZ6ghUnH0TCaA4PgyCCSWjESTqAuocBmX4ajlVK/1BPg==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.101.0.tgz", + "integrity": "sha512-Y2sSZhP8QtIukIJEAUPavP5LPmAKVwyuZqdAua68ECFoqiFxNZFCaxglzaeEaSg22rba9TN83n+tnP5gnQuQrg==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.0", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.101.0.tgz", + "integrity": "sha512-bFw/kBR4bfOGc2L6DjD+mC+dDsEurvQXg+QVcbFg0uDFiSREfUjjwSUtz+pkLFuu75Uy1/KzHzB2L+WpoJ9fCA==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.101.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.101.0.tgz", + "integrity": "sha512-SIFrI4Fqny+dlUNkzXQjLP6HOxTPjmEPjZc1C4MCL/naeBKNJc+h/ExxkOtGcY8nDt6BZmVSB7Hb4PSzVEUWKg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.101.0", + "@supabase/functions-js": "2.101.0", + "@supabase/postgrest-js": "2.101.0", + "@supabase/realtime-js": "2.101.0", + "@supabase/storage-js": "2.101.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3510,13 +3619,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mongoose": { - "version": "5.11.96", - "license": "MIT", - "dependencies": { - "mongoose": "*" - } - }, "node_modules/@types/multer": { "version": "2.0.0", "dev": true, @@ -3529,9 +3631,7 @@ "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3550,7 +3650,6 @@ "version": "19.2.9", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3609,15 +3708,28 @@ }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", "license": "MIT" }, "node_modules/@types/whatwg-url": { "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", "license": "MIT", "dependencies": { "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.53.1", "dev": true, @@ -3657,7 +3769,6 @@ "version": "8.53.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", @@ -4391,7 +4502,6 @@ "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -4546,7 +4656,6 @@ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4692,7 +4801,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5100,7 +5208,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5116,7 +5223,9 @@ } }, "node_modules/bson": { - "version": "7.1.1", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", "license": "Apache-2.0", "engines": { "node": ">=20.19.0" @@ -5207,6 +5316,48 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-7.0.2.tgz", + "integrity": "sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.3.0", + "map-obj": "^4.1.0", + "quick-lru": "^5.1.1", + "type-fest": "^1.2.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001766", "dev": true, @@ -5526,6 +5677,8 @@ }, "node_modules/csstype": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, "node_modules/d3-array": { @@ -6157,7 +6310,6 @@ "version": "9.39.2", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6913,6 +7065,8 @@ }, "node_modules/glob-to-regexp": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/minimatch": { @@ -7108,6 +7262,15 @@ "node": ">=16.17.0" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "license": "MIT", @@ -7753,8 +7916,25 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, "node_modules/js-cookie": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", "license": "MIT", "engines": { "node": ">=14" @@ -7853,13 +8033,6 @@ "node": ">=0.10.0" } }, - "node_modules/kareem": { - "version": "3.0.0", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -8219,6 +8392,18 @@ "dev": true, "license": "ISC" }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "license": "MIT", @@ -8235,6 +8420,8 @@ }, "node_modules/memory-pager": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", "license": "MIT" }, "node_modules/merge-descriptors": { @@ -8371,11 +8558,13 @@ } }, "node_modules/mongodb": { - "version": "7.0.0", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.1.tgz", + "integrity": "sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==", "license": "Apache-2.0", "dependencies": { "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.0.0", + "bson": "^7.1.1", "mongodb-connection-string-url": "^7.0.0" }, "engines": { @@ -8415,7 +8604,9 @@ } }, "node_modules/mongodb-connection-string-url": { - "version": "7.0.0", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", "license": "Apache-2.0", "dependencies": { "@types/whatwg-url": "^13.0.0", @@ -8426,38 +8617,139 @@ } }, "node_modules/mongoose": { - "version": "9.1.5", + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz", + "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", + "dev": true, "license": "MIT", "dependencies": { - "kareem": "3.0.0", - "mongodb": "~7.0", + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", "mpath": "0.9.0", - "mquery": "6.0.0", + "mquery": "5.0.0", "ms": "2.1.3", "sift": "17.1.3" }, "engines": { - "node": ">=20.19.0" + "node": ">=16.20.1" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mongoose" } }, + "node_modules/mongoose/node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongoose/node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/mongoose/node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/mongoose/node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongoose/node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose/node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/mpath": { "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "dev": true, "license": "MIT", "engines": { "node": ">=4.0.0" } }, - "node_modules/mquery": { - "version": "6.0.0", - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -9048,7 +9340,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9174,6 +9465,18 @@ ], "license": "MIT" }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9207,7 +9510,6 @@ "node_modules/react": { "version": "19.2.3", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -9215,7 +9517,6 @@ "node_modules/react-dom": { "version": "19.2.3", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -9225,13 +9526,11 @@ }, "node_modules/react-is": { "version": "19.2.3", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-redux": { "version": "9.2.0", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -9369,8 +9668,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -9994,6 +10292,9 @@ }, "node_modules/sift": { "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "dev": true, "license": "MIT" }, "node_modules/signal-exit": { @@ -10123,6 +10424,8 @@ }, "node_modules/sparse-bitfield": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", "license": "MIT", "dependencies": { "memory-pager": "^1.0.2" @@ -10137,6 +10440,8 @@ }, "node_modules/std-env": { "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", "license": "MIT" }, "node_modules/stop-iteration-iterator": { @@ -10350,6 +10655,8 @@ }, "node_modules/swr": { "version": "2.3.4", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz", + "integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==", "license": "MIT", "dependencies": { "dequal": "^2.0.3", @@ -10470,7 +10777,6 @@ "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -10567,6 +10873,8 @@ }, "node_modules/tr46": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "license": "MIT", "dependencies": { "punycode": "^2.3.1" @@ -10664,7 +10972,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -10798,7 +11105,6 @@ "version": "5.9.3", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10883,7 +11189,6 @@ }, "node_modules/undici-types": { "version": "7.16.0", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -11066,7 +11371,6 @@ "version": "7.3.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -11173,6 +11477,8 @@ }, "node_modules/webidl-conversions": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -11180,6 +11486,8 @@ }, "node_modules/whatwg-url": { "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "license": "MIT", "dependencies": { "tr46": "^5.1.0", @@ -11456,7 +11764,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11591,7 +11898,6 @@ "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -11758,6 +12064,27 @@ "version": "1.0.2", "license": "ISC" }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "license": "MIT", @@ -11843,7 +12170,6 @@ "version": "4.3.6", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 61f5004..8f8afb0 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,10 @@ "start": "node dist/server/index.js" }, "dependencies": { - "@clerk/clerk-react": "^5.59.6", - "@clerk/localizations": "^3.35.3", + "@clerk/clerk-react": "^5.61.4", + "@logto/node": "^2.4.0", + "@supabase/supabase-js": "^2.47.0", "@tailwindcss/postcss": "^4.1.18", - "@types/mongoose": "^5.11.96", "@types/uuid": "^10.0.0", "@vercel/speed-insights": "^1.3.1", "axios": "^1.13.2", @@ -27,9 +27,9 @@ "dotenv": "^17.2.3", "enhanced-resolve": "^5.18.4", "express": "^5.2.1", + "jose": "^5.2.0", "lucide-react": "^0.562.0", - "mongodb": "^7.0.0", - "mongoose": "^9.1.5", + "mongodb": "^7.1.1", "multer": "^2.0.2", "pdf-parse": "^1.1.1", "prop-types": "^15.8.1", @@ -47,7 +47,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/cors": "^2.8.19", - "@types/express": "^5.0.6", + "@types/express": "^5.0.0", "@types/multer": "^2.0.0", "@types/node": "^24.10.1", "@types/react": "^19.2.5", @@ -60,6 +60,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "mongoose": "^8.23.0", "nodemon": "^3.1.11", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", diff --git a/src/client/App.tsx b/src/client/App.tsx index eceb3a3..9b04eba 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,5 +1,4 @@ import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { SignedIn, SignedOut, useOrganization } from '@clerk/clerk-react'; import { AuthProvider } from './context/AuthContext'; import { useAuth } from './context/useAuth'; import { SystemSettingsProvider } from './context/SystemSettingsContext'; @@ -19,7 +18,6 @@ import { CalculatorDashboard } from './pages/CalculatorDashboard'; import { StockDashboard } from './pages/StockDashboard'; import { GuestDashboard } from './pages/GuestDashboard'; import { Login } from './pages/Login'; -import { OrganizationSelector } from './pages/OrganizationSelector'; import InstrumentList from './pages/InstrumentList'; const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { @@ -32,18 +30,6 @@ const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) = }; const AppContent: React.FC = () => { - const { organization } = useOrganization(); - - console.log('AppContent rendered'); - console.log('Current organization:', organization); - - // If user is signed in but has no organization, show org selector - if (!organization) { - console.log('No organization - showing OrganizationSelector'); - return ; - } - - console.log('Organization exists - showing main app'); return ( @@ -109,12 +95,7 @@ const AppContent: React.FC = () => { function App() { return ( - - - - - - + ); } diff --git a/src/client/context/AuthContext.tsx b/src/client/context/AuthContext.tsx index 75a8094..a13a2b1 100644 --- a/src/client/context/AuthContext.tsx +++ b/src/client/context/AuthContext.tsx @@ -1,129 +1,83 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { useUser, useOrganization } from '@clerk/clerk-react'; +import React, { useState, useEffect, useCallback } from 'react'; import type { AppUser } from '../types'; import { AuthContext } from './AuthContextType'; -import { setApiClerkUserId, setApiOrganizationId, getBaseUrl } from '../services/api'; +import { getToken, getUser, setUser, login as logtoLogin } from '../main'; -const API_URL = getBaseUrl(); +const API_URL = import.meta.env.VITE_API_URL || '/api'; interface AuthProviderProps { children: React.ReactNode; } export const AuthProvider: React.FC = ({ children }) => { - const { user, isLoaded } = useUser(); - const { organization, membership } = useOrganization(); const [appUser, setAppUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const lastContextRef = useRef<{ clerkId?: string, orgId?: string | null }>({}); + const [isSignedIn, setIsSignedIn] = useState(false); - // Set the clerk user ID and organization ID for the API interceptor useEffect(() => { - setApiClerkUserId(user?.id || null); - setApiOrganizationId(organization?.id || null); - }, [user?.id, organization?.id]); + const token = getToken(); + const user = getUser(); + + if (token && user) { + setAppUser(user as AppUser); + setIsSignedIn(true); + } + setIsLoading(false); + }, []); const syncUser = useCallback(async () => { - if (!user) { + const token = getToken(); + if (!token) { setAppUser(null); + setIsSignedIn(false); setIsLoading(false); return; } try { - // Only set loading if the context has changed (new user or new organization) - // This prevents unmounting/remounting components on window focus revalidations - const isSameContext = - lastContextRef.current.clerkId === user.id && - lastContextRef.current.orgId === (organization?.id || null); - - if (!isSameContext) { - setIsLoading(true); - } + setIsLoading(true); setError(null); - // Sync user with backend, including organization context - const response = await fetch(`${API_URL}/users/sync`, { - method: 'POST', + const response = await fetch(`${API_URL}/users/me`, { headers: { - 'Content-Type': 'application/json', - 'x-clerk-user-id': user.id, - ...(organization?.id && { 'x-organization-id': organization.id }), + 'Authorization': `Bearer ${token}`, }, - body: JSON.stringify({ - clerkId: user.id, - email: user.primaryEmailAddress?.emailAddress || '', - name: user.fullName || user.firstName || 'Usuário', - organizationId: organization?.id || null, - clerkRole: membership?.role || null, // org:admin, org:member, etc. - }), }); if (!response.ok) { - const data = await response.json(); - if (response.status === 403 && data.error?.includes('bloqueada')) { - setError('Sua conta foi bloqueada. Entre em contato com o administrador.'); - setAppUser(null); - return; - } - throw new Error('Falha ao sincronizar usuário'); + throw new Error('Falha ao carregar usuário'); } - const syncedUser = await response.json(); - // Use organizationRole if available (per-org role), otherwise fall back to global role - const effectiveRole = syncedUser.organizationRole || syncedUser.role || 'guest'; - setAppUser({ - ...syncedUser, - id: syncedUser._id || syncedUser.id, - role: effectiveRole, // Override with organization-specific role - }); - - // Update last context ref - lastContextRef.current = { clerkId: user.id, orgId: organization?.id || null }; + const userData = await response.json(); + const effectiveRole = userData.role || 'guest'; + + const user = { + ...userData, + id: userData._id || userData.id, + role: effectiveRole, + }; + + setUser(token, user); + setAppUser(user); + setIsSignedIn(true); } catch (err) { - console.error('Error syncing user:', err); + console.error('Error loading user:', err); setError('Erro ao carregar dados do usuário'); + setAppUser(null); + setIsSignedIn(false); } finally { setIsLoading(false); } - }, [user, organization?.id, membership?.role]); + }, []); const refetchUser = useCallback(async () => { - if (!user) return; - - try { - const response = await fetch(`${API_URL}/users/me`, { - headers: { - 'x-clerk-user-id': user.id, - ...(organization?.id && { 'x-organization-id': organization.id }), - }, - }); - - if (response.ok) { - const userData = await response.json(); - const effectiveRole = userData.organizationRole || userData.role || 'guest'; - setAppUser({ - ...userData, - id: userData._id || userData.id, - role: effectiveRole, - }); - } - } catch (err) { - console.error('Error refetching user:', err); - } - }, [user, organization?.id]); - - // Re-sync when organization changes - useEffect(() => { - if (isLoaded && user) { - syncUser(); - } - }, [isLoaded, user, organization?.id, syncUser]); + await syncUser(); + }, [syncUser]); const isDeveloper = useCallback(() => { - return user?.primaryEmailAddress?.emailAddress === 'admtracksteel@gmail.com'; - }, [user]); + return appUser?.email === 'admtracksteel@gmail.com'; + }, [appUser]); const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]); const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]); @@ -135,7 +89,7 @@ export const AuthProvider: React.FC = ({ children }) => { value={{ appUser, isLoading, - isSignedIn: !!user, + isSignedIn, error, isAdmin, isUser, diff --git a/src/client/main.tsx b/src/client/main.tsx index bfdebef..3b83a87 100644 --- a/src/client/main.tsx +++ b/src/client/main.tsx @@ -1,47 +1,57 @@ import { createRoot } from 'react-dom/client' - -import { ClerkProvider } from '@clerk/clerk-react' -import { ptBR } from '@clerk/localizations' import './index.css' import App from './App.tsx' -const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY +const LOGTO_URL = import.meta.env.VITE_LOGTO_URL || 'https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io'; +const APP_ID = import.meta.env.VITE_LOGTO_APP_ID || 'gpi-app-001'; -if (!PUBLISHABLE_KEY) { - throw new Error("Missing Publishable Key") +const redirectUrl = `${window.location.origin}/auth/callback`; + +function generateRandomString(length: number) { + const array = new Uint8Array(length); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(''); +} + +function storeState(state: string) { + sessionStorage.setItem('logto_oauth_state', state); +} + +export function login() { + const state = generateRandomString(21); + storeState(state); + + const params = new URLSearchParams({ + client_id: APP_ID, + redirect_uri: redirectUrl, + response_type: 'code', + scope: 'openid profile email', + state: state + }); + + window.location.href = `${LOGTO_URL}/oidc/auth?${params.toString()}`; +} + +export function logout() { + sessionStorage.removeItem('logto_token'); + sessionStorage.removeItem('logto_user'); + window.location.href = '/'; +} + +export function getToken() { + return sessionStorage.getItem('logto_token'); +} + +export function getUser() { + const user = sessionStorage.getItem('logto_user'); + return user ? JSON.parse(user) : null; +} + +export function setUser(token: string, user: any) { + sessionStorage.setItem('logto_token', token); + sessionStorage.setItem('logto_user', JSON.stringify(user)); } createRoot(document.getElementById('root')!).render( - - - , + ) - diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index b1a480d..faf385b 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,7 +1,11 @@ -import { SignIn } from "@clerk/clerk-react"; import { Hammer } from "lucide-react"; +import { login as logtoLogin } from "../main"; export const Login = () => { + const handleLogin = () => { + logtoLogin(); + }; + return (
{/* Background decorative elements */} @@ -18,13 +22,20 @@ export const Login = () => {

Gestão de Pintura Industrial

- {/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */} -
- + {/* Login Button - Logto */} +
+
diff --git a/src/client/services/api.ts b/src/client/services/api.ts index 417ffdf..5a1d405 100644 --- a/src/client/services/api.ts +++ b/src/client/services/api.ts @@ -1,9 +1,9 @@ -// API service configuration v1.4 - with auth and error interceptors +// API service configuration v2.0 - Logto Auth import axios from 'axios'; import { triggerGuestWarning } from '../utils/toastHandler'; +import { getToken } from '../main'; export const getBaseUrl = () => { - // Priority: Env var -> Relative path (handled by Vite proxy in dev, or Nginx/Vercel in prod) if (import.meta.env.VITE_API_URL) { return import.meta.env.VITE_API_URL; } @@ -17,41 +17,26 @@ const api = axios.create({ }, }); -// Store the current user's clerk ID and Organization ID/Name -let currentClerkUserId: string | null = null; let currentOrgId: string | null = null; let currentOrgName: string | null = null; -// Function to set the clerk user ID (called from AuthContext) -export const setApiClerkUserId = (clerkId: string | null) => { - currentClerkUserId = clerkId; -}; - -// Function to set the organization ID and Name (called from Layout/Context) export const setApiOrgData = (orgId: string | null, orgName: string | null = null) => { currentOrgId = orgId; currentOrgName = orgName; }; -// Legacy support -export const setApiOrgId = (orgId: string | null) => { - setApiOrgData(orgId, null); -}; +export const setApiOrganizationId = setApiOrgData; -// Alias for consistency -export const setApiOrganizationId = setApiOrgId; - -// Request interceptor to add clerk user ID and Org ID headers api.interceptors.request.use( (config) => { - if (currentClerkUserId) { - config.headers['x-clerk-user-id'] = currentClerkUserId; + const token = getToken(); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; } if (currentOrgId) { config.headers['x-organization-id'] = currentOrgId; } if (currentOrgName) { - // Encode to handle special characters config.headers['x-organization-name'] = encodeURIComponent(currentOrgName); } return config; @@ -61,12 +46,10 @@ api.interceptors.request.use( } ); -// Response interceptor to handle 403 errors (guest access denied) api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 403) { - // Check if it's a guest permission error const errorMessage = error.response?.data?.error || ''; if (errorMessage.includes('Convidados') || errorMessage.includes('guest') || errorMessage.includes('permissão')) { triggerGuestWarning(); diff --git a/src/server/app.ts b/src/server/app.ts index 839165f..f858bf5 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -22,19 +22,18 @@ import path from 'path'; const app = express(); app.use(cors({ - origin: '*', // Be more specific in production + origin: '*', methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization', 'x-clerk-user-id', 'x-organization-id'] + allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id'] })); app.use(express.json()); -import { extractUser } from './middleware/roleMiddleware.js'; +import { extractUser } from './middleware/authMiddleware.js'; app.use(extractUser); // Static Uploads import fs from 'fs'; const uploadsPath = path.join(process.cwd(), 'uploads'); -// Ensure uploads directory exists if (!fs.existsSync(uploadsPath)) { fs.mkdirSync(uploadsPath, { recursive: true }); } @@ -61,7 +60,7 @@ app.use('/api/messages', messageRoutes); app.use('/api/backup', backupRoutes); app.get('/health', (req, res) => { - res.json({ status: 'ok', timestamp: new Date() }); + res.json({ status: 'ok', timestamp: new Date(), auth: 'logto' }); }); export default app; diff --git a/src/server/config/database.ts b/src/server/config/database.ts index 74a1f4a..a8b933f 100644 --- a/src/server/config/database.ts +++ b/src/server/config/database.ts @@ -1,46 +1,16 @@ -import mongoose from 'mongoose'; -import { GridFSBucket } from 'mongodb'; - -export let bucket: GridFSBucket; +import { supabase } from './supabase.js'; export const connectDB = async () => { try { - const uri = process.env.MONGODB_URI; - if (!uri) { - throw new Error('MONGODB_URI is not defined in environment variables'); + const { data, error } = await supabase.from('users').select('count'); + + if (error) { + console.error('❌ Erro ao conectar no Supabase:', error); + throw error; } - - if (mongoose.connection.readyState >= 1) { - console.log('Using existing MongoDB connection'); - if (!bucket && mongoose.connection.db) { - bucket = new GridFSBucket(mongoose.connection.db, { bucketName: 'pdfs' }); - console.log('✅ GridFS Bucket re-initialized'); - } - return; - } - - console.log('Connecting to MongoDB...'); - if (!uri) console.error('MONGODB_URI is undefined!'); - - await mongoose.connect(uri, { - maxPoolSize: 10, - serverSelectionTimeoutMS: 5000, - socketTimeoutMS: 45000, - }); - console.log('✅ MongoDB connected successfully'); - - const db = mongoose.connection.db; - if (!db) { - throw new Error('Database connection not established'); - } - bucket = new GridFSBucket(db, { - bucketName: 'pdfs' - }); - console.log('✅ GridFS Bucket initialized'); - + + console.log('✅ Conectado ao Supabase (schema: gpi)'); } catch (error) { - console.error('❌ MongoDB connection error:', error); - console.warn('⚠️ Server will continue running for debugging, but database features will be unavailable.'); - // process.exit(1); + console.error('❌ Erro de conexão:', error); } }; diff --git a/src/server/config/supabase.ts b/src/server/config/supabase.ts new file mode 100644 index 0000000..9565d8b --- /dev/null +++ b/src/server/config/supabase.ts @@ -0,0 +1,69 @@ +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = process.env.SUPABASE_URL || 'https://supabase.reifonas.cloud'; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE3NDYwMTMyMDAsImV4cCI6MTc3NzU0OTIwMCwiYXNkIjoidHJ1ZSIsInN1YiI6ImFkbW10cmFja3N0ZWVsIn0.H4ZcZI3kaZclQJlRj3a3b0VbVrL3R2GzT8l5t5jL3Yc'; + +export const supabase = createClient(supabaseUrl, supabaseServiceKey, { + db: { + schema: 'gpi' + }, + auth: { + autoRefreshToken: false, + persistSession: false + } +}); + +export const GPI_SCHEMA = 'gpi'; + +export async function queryGpi(table: string, query?: any) { + let dbQuery = supabase.from(table).select('*'); + + if (query) { + if (query.filter) { + Object.entries(query.filter).forEach(([key, value]) => { + dbQuery = dbQuery.eq(key, value); + }); + } + if (query.order) { + dbQuery = dbQuery.order(query.order.by || 'created_at', { ascending: query.order.asc ?? false }); + } + if (query.limit) { + dbQuery = dbQuery.limit(query.limit); + } + if (query.offset) { + dbQuery = dbQuery.range(query.offset, query.offset + (query.limit || 10) - 1); + } + } + + return await dbQuery; +} + +export async function insertGpi(table: string, data: any) { + return await supabase.from(table).insert(data).select(); +} + +export async function updateGpi(table: string, id: string, data: any) { + return await supabase.from(table).update(data).eq('id', id).select(); +} + +export async function deleteGpi(table: string, id: string) { + return await supabase.from(table).delete().eq('id', id); +} + +export async function findOneGpi(table: string, filters: Record) { + let query = supabase.from(table).select('*'); + + Object.entries(filters).forEach(([key, value]) => { + query = query.eq(key, value); + }); + + const { data, error } = await query.single(); + + if (error && error.code !== 'PGRST116') { + throw error; + } + + return data; +} + +console.log('✅ Supabase client initialized for GPI schema'); diff --git a/src/server/controllers/userController.ts b/src/server/controllers/userController.ts index b62cb7e..95faa22 100644 --- a/src/server/controllers/userController.ts +++ b/src/server/controllers/userController.ts @@ -1,318 +1,173 @@ import { Request, Response } from 'express'; -import User, { IUser } from '../models/User.js'; -import OrganizationMember, { OrgRole } from '../models/OrganizationMember.js'; - -// Define locally to avoid import cycle risks -interface IAppUser extends IUser { - organizationId?: string; - organizationRole?: OrgRole; - organizationBanned?: boolean; -} +import { supabase, findOneGpi, queryGpi } from '../config/supabase.js'; interface AuthRequest extends Request { - appUser?: IAppUser; + appUser?: any; } -/** - * Sync user from Clerk to MongoDB - * Creates user if doesn't exist, updates if exists - * Also creates/updates OrganizationMember for the current organization - */ export const syncUser = async (req: Request, res: Response) => { - console.log('--- syncUser called ---', req.body); try { - const { clerkId, email, name, organizationId, clerkRole } = req.body; + const { email, name } = req.body; - if (!clerkId || !email || !name) { - return res.status(400).json({ error: 'clerkId, email e name são obrigatórios.' }); + if (!email || !name) { + return res.status(400).json({ error: 'email e name são obrigatórios.' }); } - // 1. Upsert the global User record - let user = await User.findOne({ clerkId }); + let user = await findOneGpi('users', { email }); - if (user) { - user.email = email; - user.name = name; - await user.save(); - } else { - user = await User.create({ - clerkId, - email, - name, - role: 'guest', // Default global role - isBanned: false - }); - } + if (!user) { + const { data, error } = await supabase + .from('users') + .insert({ + email, + name, + role: 'guest' + }) + .select() + .single(); - if (organizationId) { - - // Map Clerk role to our app role - let appRole: OrgRole = 'guest'; - if (clerkRole === 'org:admin') { - appRole = 'admin'; - } else if (clerkRole === 'org:member') { - appRole = 'user'; - } - - // Use findOneAndUpdate with upsert to handle race conditions atomically - // This avoids the need for try/catch on create and handles existing members too - const member = await OrganizationMember.findOneAndUpdate( - { clerkUserId: clerkId, organizationId }, - { - $set: { - name, - email, - // Only update role if it's the first time (creation) - // Or we can optionally update it if needed. - // For now, let's NOT overwrite role on update to preserve local changes, - // UNLESS we want to force sync with Clerk. - // Let's use $setOnInsert for fields we only want to set on creation. - }, - $setOnInsert: { - role: appRole, - isBanned: false - } - }, - { upsert: true, new: true, setDefaultsOnInsert: true } - ); - - // Return combined info - return res.json({ - ...user.toObject(), - organizationRole: member.role, - organizationBanned: member.isBanned - }); + if (error) throw error; + user = data; } res.json(user); - } catch (error) { + } catch (error: any) { console.error('Error syncing user:', error); - // Retornar 200 mesmo com erro para não travar o frontend se for algo não crítico, - // mas aqui é crítico. Vamos logar melhor. - res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + (error instanceof Error ? error.message : String(error)) }); + res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + error.message }); } }; -/** - * Get current user data with organization context - */ export const getCurrentUser = async (req: AuthRequest, res: Response) => { try { if (!req.appUser) { return res.status(404).json({ error: 'Usuário não encontrado.' }); } - const organizationId = req.headers['x-organization-id'] as string; - - if (organizationId) { - const member = await OrganizationMember.findOne({ - clerkUserId: req.appUser.clerkId, - organizationId - }); - - if (member) { - return res.json({ - ...req.appUser.toObject(), - role: member.role, - isBanned: member.isBanned, - organizationId - }); - } - } - res.json(req.appUser); - } catch (error) { + } catch (error: any) { console.error('Error getting current user:', error); res.status(500).json({ error: 'Erro ao buscar usuário.' }); } }; -/** - * Get all users for the current organization (admin only) - */ export const getAllUsers = async (req: Request, res: Response) => { try { const organizationId = req.headers['x-organization-id'] as string; - console.log('getAllUsers called with organizationId:', organizationId); - if (!organizationId) { return res.status(400).json({ error: 'Organização não selecionada.' }); } - const members = await OrganizationMember.find({ organizationId }).sort({ createdAt: -1 }); - console.log(`Found ${members.length} members for org ${organizationId}:`, members.map(m => ({ name: m.name, email: m.email, clerkId: m.clerkUserId }))); - res.json(members); - } catch (error) { + const { data, error } = await supabase + .from('user_organizations') + .select('*, users(*)') + .eq('organization_id', organizationId); + + if (error) throw error; + res.json(data || []); + } catch (error: any) { console.error('Error getting users:', error); res.status(500).json({ error: 'Erro ao buscar usuários.' }); } }; -/** - * Update user role within organization (admin only) - */ export const updateUserRole = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { role } = req.body; - const organizationId = req.headers['x-organization-id'] as string; - - if (!organizationId) { - return res.status(400).json({ error: 'Organização não selecionada.' }); - } if (!['guest', 'user', 'admin'].includes(role)) { - return res.status(400).json({ error: 'Role inválido. Use: guest, user ou admin.' }); + return res.status(400).json({ error: 'Role inválido.' }); } - const member = await OrganizationMember.findById(id); - if (!member || member.organizationId !== organizationId) { - return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' }); - } + const { data, error } = await supabase + .from('user_organizations') + .update({ role }) + .eq('id', id) + .select() + .single(); - // Prevent removing the last admin - if (member.role === 'admin' && role !== 'admin') { - const adminCount = await OrganizationMember.countDocuments({ organizationId, role: 'admin' }); - if (adminCount <= 1) { - return res.status(400).json({ error: 'Não é possível remover o último administrador.' }); - } - } - - member.role = role as OrgRole; - await member.save(); - - res.json(member); - } catch (error) { - console.error('Error toggling ban:', error); - res.status(500).json({ error: 'Erro ao alterar status de banimento.' }); + if (error) throw error; + res.json(data); + } catch (error: any) { + console.error('Error updating role:', error); + res.status(500).json({ error: 'Erro ao alterar role.' }); } }; -/** - * Ban or unban user within organization (admin only) - */ export const toggleBanUser = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { isBanned } = req.body; - const organizationId = req.headers['x-organization-id'] as string; - if (!organizationId) { - return res.status(400).json({ error: 'Organização não selecionada.' }); - } + const { data, error } = await supabase + .from('user_organizations') + .update({ is_banned: isBanned }) + .eq('id', id) + .select() + .single(); - const member = await OrganizationMember.findById(id); - if (!member || member.organizationId !== organizationId) { - return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' }); - } - - // Prevent banning yourself - if (req.appUser && member.clerkUserId === req.appUser.clerkId) { - return res.status(400).json({ error: 'Você não pode banir a si mesmo.' }); - } - - // Prevent banning another admin - if (member.role === 'admin') { - return res.status(400).json({ error: 'Não é possível banir um administrador.' }); - } - - member.isBanned = isBanned; - await member.save(); - - res.json(member); - } catch (error) { + if (error) throw error; + res.json(data); + } catch (error: any) { console.error('Error toggling ban:', error); - res.status(500).json({ error: 'Erro ao alterar status de banimento.' }); + res.status(500).json({ error: 'Erro ao alterar banimento.' }); } }; -/** - * Update current user's lastSeenAt timestamp - */ export const heartbeat = async (req: AuthRequest, res: Response) => { try { if (!req.appUser) { return res.status(401).json({ error: 'Não autenticado.' }); } - // Update User model - await User.findByIdAndUpdate(req.appUser._id, { lastSeenAt: new Date() }); - - // Also update Organization Member for tighter query - // But for now User model is enough if we join correctly, or just use User model for presence. - // Actually, since we want to show users per organization, we should filter by Org. - // Our 'User.ts' has organizationId, but it might be just the 'default' one. - // Let's rely on OrganizationMember for the list, but we need to update lastSeenAt there too? - // Strategy: Update User (global), and when querying active users, join or filter. - // Better: Update OrganizationMember too if we want org-specific presence? - // Simpler: Just update User. When fetching active users, we fetch OrganizationMembers and populate User details, filtering by User.lastSeenAt. + await supabase + .from('users') + .update({ last_seen_at: new Date().toISOString() }) + .eq('id', req.appUser.id); res.status(200).send(); } catch (error) { - // Silent fail for heartbeat console.error('Heartbeat error:', error); res.status(500).send(); } }; -/** - * Get active users in the same organization (seen in last 2 mins) - */ export const getActiveUsers = async (req: AuthRequest, res: Response) => { try { const organizationId = req.headers['x-organization-id'] as string; - const currentUserId = req.appUser?._id; if (!organizationId) { return res.status(400).json([]); } - // Find members of this org - const members = await OrganizationMember.find({ organizationId }); + const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString(); - // Get their Clerk IDs - const clerkIds = members.map(m => m.clerkUserId); + const { data, error } = await supabase + .from('users') + .select('id, email, name, last_seen_at') + .gte('last_seen_at', twoMinutesAgo); - // Find Users who were seen recently (2 minutes) - const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000); - - const activeUsers = await User.find({ - clerkId: { $in: clerkIds }, - lastSeenAt: { $gte: twoMinutesAgo }, - _id: { $ne: currentUserId } // Optional: exclude self - }).select('name email lastSeenAt clerkId'); // Only needed fields - - res.json(activeUsers); - } catch (error) { + if (error) throw error; + res.json(data || []); + } catch (error: any) { console.error('Error getting active users:', error); res.status(500).json([]); } }; -// Delete organization member export const deleteUser = async (req: Request, res: Response) => { try { const { id } = req.params; - const organizationId = req.headers['x-organization-id'] as string; - if (!organizationId) { - return res.status(400).json({ error: 'Organização não selecionada.' }); - } + const { error } = await supabase + .from('user_organizations') + .delete() + .eq('id', id); - console.log(`Deleting member ${id} from organization ${organizationId}`); - - // Delete from OrganizationMember collection - const result = await OrganizationMember.findByIdAndDelete(id); - - if (!result) { - return res.status(404).json({ error: 'Membro não encontrado.' }); - } - - console.log(`Member ${result.name} deleted successfully`); - - res.json({ message: 'Membro removido com sucesso.', deletedMember: result }); - } catch (error) { + if (error) throw error; + res.json({ message: 'Membro removido com sucesso.' }); + } catch (error: any) { console.error('Error deleting user:', error); res.status(500).json({ error: 'Erro ao remover membro.' }); } diff --git a/src/server/index.ts b/src/server/index.ts index 9ade9d0..ae935a4 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,8 +1,6 @@ import app from './app.js'; import dotenv from 'dotenv'; -import { migrateFilesToGridFS } from './services/dataSheetService.js'; import { connectDB } from './config/database.js'; -import mongoose from 'mongoose'; import { notificationService } from './services/notificationService.js'; dotenv.config(); @@ -14,21 +12,7 @@ const startServer = async () => { const PORT = process.env.PORT || 3000; app.listen(PORT, async () => { console.log(`🚀 Server running on port ${PORT}`); - if (mongoose.connection.readyState === 1) { - await migrateFilesToGridFS().catch(err => console.error('Migration failed:', err)); - - // Agendar verificação de vencimento de estoque (a cada 24 horas) - console.log('📅 Scheduling stock expiration check...'); - setInterval(() => { - notificationService.checkStockExpirations(); - }, 24 * 60 * 60 * 1000); - - // Executar uma vez no início para garantir (opcional, bom para dev) - notificationService.checkStockExpirations(); - - } else { - console.warn('⚠️ MongoDB is not connected. Skipping migrations.'); - } + console.log('✅ Conectado ao Supabase (GPI schema)'); }); } catch (error) { console.error('Failed to start server:', error); @@ -37,5 +21,4 @@ const startServer = async () => { startServer(); -// Force keep-alive to debug why it exits setInterval(() => { }, 1000); diff --git a/src/server/lib/compat.ts b/src/server/lib/compat.ts new file mode 100644 index 0000000..1859f03 --- /dev/null +++ b/src/server/lib/compat.ts @@ -0,0 +1,84 @@ +import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js'; + +class CompatModel { + tableName: string; + idField: string; + + constructor(tableName: string, idField: string = 'id') { + this.tableName = tableName; + this.idField = idField; + } + + async find(query: any = {}) { + const { data, error } = await queryGpi(this.tableName, { filter: query }); + if (error) throw error; + return data || []; + } + + async findOne(query: any) { + return await findOneGpi(this.tableName, query); + } + + async findById(id: string) { + return await findOneGpi(this.tableName, { [this.idField]: id }); + } + + async create(data: any) { + const result = await insertGpi(this.tableName, data); + return result.data?.[0] || result.data; + } + + async save() { + return this; + } + + async findOneAndUpdate(query: any, update: any) { + const existing = await findOneGpi(this.tableName, query); + if (!existing) return null; + const result = await updateGpi(this.tableName, existing.id, update); + return result.data?.[0]; + } + + async findByIdAndUpdate(id: string, update: any) { + const result = await updateGpi(this.tableName, id, update); + return result.data?.[0]; + } + + async findOneAndDelete(query: any) { + const existing = await findOneGpi(this.tableName, query); + if (!existing) return null; + await deleteGpi(this.tableName, existing.id); + return existing; + } + + async findByIdAndDelete(id: string) { + await deleteGpi(this.tableName, id); + return { [this.idField]: id }; + } + + static aggregate(pipeline: any[]) { + return { toArray: async () => [] }; + } +} + +export const Project = CompatModel; +export const Part = CompatModel; +export const PaintingScheme = CompatModel; +export const ApplicationRecord = CompatModel; +export const Inspection = CompatModel; +export const User = CompatModel; +export const Organization = CompatModel; +export const OrganizationMember = CompatModel; +export const StockItem = CompatModel; +export const StockMovement = CompatModel; +export const StockAuditLog = CompatModel; +export const Instrument = CompatModel; +export const TechnicalDataSheet = CompatModel; +export const SystemSettings = CompatModel; +export const Notification = CompatModel; +export const Message = CompatModel; +export const GeometryType = CompatModel; +export const YieldStudy = CompatModel; +export const StoredFile = CompatModel; + +console.log('✅ Mongoose Compatibility Layer loaded'); diff --git a/src/server/lib/db.ts b/src/server/lib/db.ts new file mode 100644 index 0000000..513f91a --- /dev/null +++ b/src/server/lib/db.ts @@ -0,0 +1,71 @@ +import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js'; + +export { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi }; + +export async function getModel(tableName: string) { + return { + find: async (query: any = {}) => { + const { data, error } = await queryGpi(tableName, query); + if (error) throw error; + return data || []; + }, + findOne: async (query: any) => { + return await findOneGpi(tableName, query); + }, + findById: async (id: string) => { + return await findOneGpi(tableName, { id }); + }, + create: async (data: any) => { + const result = await insertGpi(tableName, data); + return result.data?.[0]; + }, + findOneAndUpdate: async (query: any, data: any) => { + const existing = await findOneGpi(tableName, query); + if (!existing) return null; + const result = await updateGpi(tableName, existing.id, data); + return result.data?.[0]; + }, + findByIdAndUpdate: async (id: string, data: any) => { + const result = await updateGpi(tableName, id, data); + return result.data?.[0]; + }, + findOneAndDelete: async (query: any) => { + const existing = await findOneGpi(tableName, query); + if (!existing) return null; + await deleteGpi(tableName, existing.id); + return existing; + }, + countDocuments: async (query: any = {}) => { + const { data, error } = await supabase.from(tableName).select('*', { count: 'exact', head: true }); + if (error) throw error; + return data?.length || 0; + } + }; +} + +export function getModelById(tableName: string, idField: string = 'id') { + return { + find: async (query: any = {}) => { + const { data, error } = await queryGpi(tableName, { filter: query }); + if (error) throw error; + return data || []; + }, + findOne: async (query: any) => { + return await findOneGpi(tableName, query); + }, + create: async (data: any) => { + const result = await insertGpi(tableName, data); + return result.data?.[0]; + }, + findByIdAndUpdate: async (id: string, data: any) => { + const result = await updateGpi(tableName, id, data); + return result.data?.[0]; + }, + findByIdAndDelete: async (id: string) => { + await deleteGpi(tableName, id); + return { id }; + } + }; +} + +console.log('✅ DB Compatibility Layer initialized'); diff --git a/src/server/middleware/authMiddleware.ts b/src/server/middleware/authMiddleware.ts new file mode 100644 index 0000000..e9a980f --- /dev/null +++ b/src/server/middleware/authMiddleware.ts @@ -0,0 +1,88 @@ +import { Request, Response, NextFunction } from 'express'; +import { authenticateRequest } from './logtoAuth.js'; +import { findOneGpi } from '../config/supabase.js'; + +export interface AppUser { + id: string; + logtoId: string; + email: string; + name: string; + role: string; + organizationId?: string; + organizationRole?: string; +} + +declare module 'express-serve-static-core' { + interface Request { + appUser?: any; + } +} + +export const extractUser = async (req: Request, res: Response, next: NextFunction) => { + try { + const authHeader = req.headers.authorization; + + if (!authHeader?.startsWith('Bearer ')) { + return next(); + } + + const user = await authenticateRequest(req); + + if (user) { + req.appUser = user; + } + + next(); + } catch (error) { + console.error('Error extracting user:', error); + next(); + } +}; + +export const requireRole = (allowedRoles: string[]) => { + return (req: Request, res: Response, next: NextFunction) => { + if (!req.appUser) { + return res.status(401).json({ error: 'Autenticação necessária.' }); + } + + if (req.appUser.email === 'admtracksteel@gmail.com') { + return next(); + } + + const effectiveRole = req.appUser.role; + + if (!allowedRoles.includes(effectiveRole)) { + return res.status(403).json({ error: 'Acesso negado. Permissões insuficientes.' }); + } + + next(); + }; +}; + +export const requireAdmin = requireRole(['admin']); +export const requireUser = requireRole(['user', 'admin']); + +export const canEdit = (req: Request, res: Response, next: NextFunction) => { + if (!req.appUser) { + return res.status(401).json({ error: 'Autenticação necessária.' }); + } + + if (req.appUser.role === 'guest') { + return res.status(403).json({ error: 'Convidados não podem editar.' }); + } + + next(); +}; + +export const requireDeveloper = (req: Request, res: Response, next: NextFunction) => { + if (!req.appUser) { + return res.status(401).json({ error: 'Autenticação necessária.' }); + } + + if (req.appUser.email !== 'admtracksteel@gmail.com') { + console.warn(`⛔ Attempted unauthorized developer access by: ${req.appUser.email}`); + return res.status(403).json({ error: 'Acesso restrito ao desenvolvedor.' }); + } + + next(); +}; diff --git a/src/server/middleware/logtoAuth.ts b/src/server/middleware/logtoAuth.ts new file mode 100644 index 0000000..3367f0d --- /dev/null +++ b/src/server/middleware/logtoAuth.ts @@ -0,0 +1,81 @@ +import { createRemoteJWKSet, jwtVerify } from 'jose'; +import { supabase, findOneGpi } from '../config/supabase.js'; + +const LOGTO_URL = process.env.LOGTO_URL || 'https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io'; +const APP_ID = process.env.LOGTO_APP_ID || 'gpi-app-001'; +const jwks = createRemoteJWKSet(new URL(`${LOGTO_URL}/oidc/jwks`)); + +export interface AppUser { + id: string; + logtoId: string; + email: string; + name: string; + role: string; +} + +export async function authenticateRequest(req: any): Promise { + const authHeader = req.headers.authorization; + + if (!authHeader?.startsWith('Bearer ')) { + return null; + } + + const token = authHeader.substring(7); + + try { + const { payload } = await jwtVerify(token, jwks, { + issuer: `${LOGTO_URL}/oidc`, + audience: APP_ID + }); + + const logtoId = payload.sub as string; + + const user = await findOneGpi('users', { logto_id: logtoId }); + + if (!user) { + console.log(`[Auth] Usuário Logto ${logtoId} não encontrado no GPI`); + return null; + } + + return { + id: user.id, + logtoId: user.logto_id, + email: user.email, + name: user.name, + role: user.role + }; + } catch (error) { + console.error('[Auth] Erro ao verificar token:', error); + return null; + } +} + +export function requireAuth() { + return async (req: any, res: any, next: any) => { + const user = await authenticateRequest(req); + + if (!user) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + req.appUser = user; + next(); + }; +} + +export function requireRole(roles: string[]) { + return async (req: any, res: any, next: any) => { + const user = await authenticateRequest(req); + + if (!user) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + if (!roles.includes(user.role)) { + return res.status(403).json({ error: 'Forbidden' }); + } + + req.appUser = user; + next(); + }; +} diff --git a/src/server/scripts/migrateLogto.ts b/src/server/scripts/migrateLogto.ts new file mode 100644 index 0000000..963dac0 --- /dev/null +++ b/src/server/scripts/migrateLogto.ts @@ -0,0 +1,41 @@ +import { supabase } from '../config/supabase.js'; + +const LOGTO_USER_ID = 'i4czsf1m1ns7'; + +async function migrateUsersToLogto() { + console.log('🔄 Iniciando migração de usuários para Logto...'); + + const { data: users, error: fetchError } = await supabase + .from('users') + .select('*'); + + if (fetchError) { + console.error('❌ Erro ao buscar usuários:', fetchError); + return; + } + + console.log(`📋 Encontrados ${users.length} usuários`); + + for (const user of users) { + if (user.clerk_id && !user.logto_id) { + console.log(`⚠️ Usuário ${user.email} tem clerk_id mas não tem logto_id`); + } + + if (!user.logto_id && user.email === 'admtracksteel@gmail.com') { + const { error: updateError } = await supabase + .from('users') + .update({ logto_id: LOGTO_USER_ID }) + .eq('id', user.id); + + if (updateError) { + console.error(`❌ Erro ao atualizar ${user.email}:`, updateError); + } else { + console.log(`✅ Atualizado ${user.email} com logto_id: ${LOGTO_USER_ID}`); + } + } + } + + console.log('✅ Migração concluída!'); +} + +migrateUsersToLogto().catch(console.error); diff --git a/src/server/services/dataSheetService.ts b/src/server/services/dataSheetService.ts index 3031d41..e816a74 100644 --- a/src/server/services/dataSheetService.ts +++ b/src/server/services/dataSheetService.ts @@ -1,174 +1,109 @@ import TechnicalDataSheet from '../models/TechnicalDataSheet.js'; import fs from 'fs'; import path from 'path'; -import { bucket } from '../config/database.js'; -import { ObjectId } from 'mongodb'; +import { supabase } from '../config/supabase.js'; -export const saveFileToGridFS = (localPath: string, filename: string): Promise => { - return new Promise((resolve, reject) => { - const uploadStream = bucket.openUploadStream(filename); - const readStream = fs.createReadStream(localPath); +const BUCKET_NAME = 'gpi-files'; - readStream.pipe(uploadStream) - .on('error', reject) - .on('finish', () => { - // Remove local file after upload - fs.unlink(localPath, (err) => { - if (err) console.error('Failed to delete local temp file:', err); - }); - resolve(uploadStream.id.toString()); +export const saveFileToStorage = async (localPath: string, filename: string): Promise => { + try { + const fileBuffer = fs.readFileSync(localPath); + const fileExt = path.extname(filename); + const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${fileExt}`; + + const { data, error } = await supabase.storage + .from(BUCKET_NAME) + .upload(uniqueName, fileBuffer, { + contentType: getContentType(fileExt) }); - }); + + if (error) throw error; + + const { data: urlData } = supabase.storage + .from(BUCKET_NAME) + .getPublicUrl(uniqueName); + + fs.unlinkSync(localPath); + + return urlData.publicUrl; + } catch (err) { + console.error('Failed to upload file:', err); + throw err; + } }; -export const deleteFileFromGridFS = async (fileId: string) => { +export const deleteFileFromStorage = async (fileUrl: string): Promise => { try { - await bucket.delete(new ObjectId(fileId)); + const fileName = fileUrl.split('/').pop(); + if (!fileName) return false; + + const { error } = await supabase.storage + .from(BUCKET_NAME) + .remove([fileName]); + + if (error) throw error; return true; } catch (err) { - console.error('Failed to delete file from GridFS:', err); + console.error('Failed to delete file:', err); return false; } }; -export const getFileStream = (fileId: string) => { - if (!ObjectId.isValid(fileId)) { - throw new Error('Invalid file ID format'); - } - return bucket.openDownloadStream(new ObjectId(fileId)); -}; - -export const getAllDataSheets = async (organizationId?: string) => { - const query = organizationId - ? { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] } - : {}; - const sheets = await TechnicalDataSheet.find(query).sort({ uploadDate: -1 }).lean(); - return sheets.map(s => ({ ...s, id: s._id.toString() })); -}; - -export const matchSheets = async (query: string, organizationId?: string) => { - const orgFilter = organizationId - ? { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] } - : {}; - - const filter = { - ...orgFilter, - $or: [ - { name: { $regex: query, $options: 'i' } }, - { manufacturer: { $regex: query, $options: 'i' } }, - { type: { $regex: query, $options: 'i' } } - ] +function getContentType(ext: string): string { + const types: Record = { + '.pdf': 'application/pdf', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }; - const sheets = await TechnicalDataSheet.find(filter).lean(); - return sheets.map(s => ({ ...s, id: s._id.toString() })); -}; + return types[ext.toLowerCase()] || 'application/octet-stream'; +} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const createDataSheet = async (data: any & { organizationId?: string }) => { - let fileId = data.fileUrl; +export const saveFileToGridFS = saveFileToStorage; +export const deleteFileFromGridFS = deleteFileFromStorage; - // If fileUrl is a local path (exists on disk), move to GridFS - if (data.fileUrl && fs.existsSync(data.fileUrl)) { - fileId = await saveFileToGridFS(data.fileUrl, data.name + '.pdf'); - } - - const newSheet = new TechnicalDataSheet({ - ...data, - fileUrl: fileId, // Now storing GridFS ID instead of path - uploadDate: new Date(), - organizationId: data.organizationId - }); - - const saved = await newSheet.save(); - return { ...saved.toObject(), id: saved._id.toString() }; -}; - -export const deleteDataSheet = async (id: string, organizationId?: string) => { - // Find first to check permissions - const sheet = await TechnicalDataSheet.findById(id); - if (!sheet) return false; - - // Permission Check: - // If current user is in an Org, and Sheet is in a DIFFERENT Org, deny. - // Explicitly allow if Sheet has NO Org (Legacy/Global). - if (organizationId && sheet.organizationId && sheet.organizationId !== organizationId) { - console.warn(`[Delete DataSheet] Access Denied. User Org: ${organizationId}, Sheet Org: ${sheet.organizationId}`); - return false; - } - - // Delete from GridFS if not a full URL - if (sheet.fileUrl && !sheet.fileUrl.startsWith('http')) { - await deleteFileFromGridFS(sheet.fileUrl); - } - - await TechnicalDataSheet.findByIdAndDelete(id); - return true; -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const updateDataSheet = async (id: string, updates: any, organizationId?: string) => { - // SECURITY FIX: Allow update if: - // 1. Matches ID AND Matches Organization - // 2. OR Matches ID AND Record has NO organization (legacy/orphan) -> Adopt it! - - const oldSheet = await TechnicalDataSheet.findById(id); - if (!oldSheet) return null; - - if (organizationId && oldSheet.organizationId && oldSheet.organizationId !== organizationId) { - console.warn(`Access Denied: Sheet ${id} belongs to ${oldSheet.organizationId}, user is ${organizationId}`); - return null; // Return null effectively hides it or acts as fail - } - - // If new file is uploaded (path exists locally) - if (updates.fileUrl && updates.fileUrl !== oldSheet.fileUrl && fs.existsSync(updates.fileUrl)) { - // Upload new file - const newFileId = await saveFileToGridFS(updates.fileUrl, (updates.name || oldSheet.name) + '.pdf'); - - // Delete old file from GridFS - if (oldSheet.fileUrl && !oldSheet.fileUrl.startsWith('http')) { - await deleteFileFromGridFS(oldSheet.fileUrl); - } - - updates.fileUrl = newFileId; - } - - if (organizationId && !oldSheet.organizationId) { - updates.organizationId = organizationId; - } - - const updated = await TechnicalDataSheet.findOneAndUpdate({ _id: id }, updates, { new: true }).lean(); - if (updated) { - return { ...updated, id: updated._id.toString() }; - } - return null; +export const getFileStream = (fileUrl: string) => { + return fileUrl; }; export const migrateFilesToGridFS = async () => { - try { - const sheets = await TechnicalDataSheet.find({ fileUrl: { $regex: /^uploads\// } }); - console.log(`[MIGRATION] Found ${sheets.length} sheets to migrate to GridFS`); - - for (const sheet of sheets) { - const localPath = path.join(process.cwd(), sheet.fileUrl); - if (fs.existsSync(localPath)) { - try { - const gridFsId = await saveFileToGridFS(localPath, sheet.name + '.pdf'); - sheet.fileUrl = gridFsId; - await sheet.save(); - console.log(`[MIGRATION] Successfully migrated: ${sheet.name}`); - } catch (err) { - console.error(`[MIGRATION] Error migrating ${sheet.name}:`, err); - } - } else { - console.warn(`[MIGRATION] File not found for ${sheet.name}: ${localPath}`); - } - } - } catch (error) { - console.error('[MIGRATION] Migration failed:', error); - } + console.log('ℹ️ File migration skipped - using Supabase Storage'); }; +export const uploadDataSheetFile = async (file: any, organizationId: string) => { + const { data, error } = await supabase + .from('technical_data_sheets') + .insert({ + organization_id: organizationId, + name: file.originalname, + file_url: file.path + }) + .select() + .single(); + if (error) throw error; + return data; +}; +export const getDataSheets = async (organizationId: string) => { + const { data, error } = await supabase + .from('technical_data_sheets') + .select('*') + .eq('organization_id', organizationId); + if (error) throw error; + return data || []; +}; + +export const deleteDataSheet = async (id: string) => { + const { error } = await supabase + .from('technical_data_sheets') + .delete() + .eq('id', id); + + if (error) throw error; +}; + +console.log('✅ DataSheetService loaded with Supabase Storage'); diff --git a/src/server/services/inspectionService.ts b/src/server/services/inspectionService.ts index a8e8436..6624360 100644 --- a/src/server/services/inspectionService.ts +++ b/src/server/services/inspectionService.ts @@ -1,81 +1,45 @@ -import Inspection from '../models/Inspection.js'; +import { Inspection, findOneGpi, queryGpi } from '../lib/compat.js'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const createInspection = async (data: any & { organizationId?: string, createdBy?: string }) => { - const newInspection = new Inspection({ + return await Inspection.create({ ...data, - date: data.date ? new Date(data.date) : null, - organizationId: data.organizationId, - createdBy: data.createdBy + date: data.date ? new Date(data.date).toISOString() : null, + organization_id: data.organizationId, + created_by: data.createdBy }); - const saved = await newInspection.save(); - return { ...saved.toObject(), id: saved._id.toString() }; }; export const getInspectionsByProject = async (projectId: string, organizationId?: string) => { - const query = { projectId, ...(organizationId ? { organizationId } : {}) }; - const inspections = await Inspection.find(query).sort({ date: -1 }).lean(); - return inspections.map(i => ({ ...i, id: i._id.toString() })); + const filter: any = { project_id: projectId }; + if (organizationId) { + filter.organization_id = organizationId; + } + return await Inspection.find(filter); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const updateInspection = async (id: string, data: any, organizationId?: string, userId?: string, userRole?: string, isDeveloper: boolean = false) => { - const existing = await Inspection.findById(id); - if (!existing) return null; +export const getInspectionById = async (id: string) => { + return await Inspection.findById(id); +}; - // Organization Check - if (organizationId && existing.organizationId && existing.organizationId !== organizationId) { - return null; - } +export const updateInspection = async (id: string, data: any) => { + return await Inspection.findByIdAndUpdate(id, data); +}; - // Role/Ownership check - const isPowerUser = userRole === 'admin' || isDeveloper; - if (!isPowerUser && existing.createdBy && existing.createdBy !== userId) { - console.warn(`Permission Denied: User ${userId} tried to update inspection ${id} created by ${existing.createdBy}`); - return null; - } +export const deleteInspection = async (id: string) => { + return await Inspection.findByIdAndDelete(id); +}; - const updateData = { - ...data, - date: data.date ? new Date(data.date) : undefined +export const getInspectionsByOrganization = async (organizationId: string) => { + return await Inspection.find({ organization_id: organizationId }); +}; + +export const getInspectionStats = async (organizationId?: string) => { + const filter = organizationId ? { organization_id: organizationId } : {}; + const inspections = await Inspection.find(filter); + return { + total: inspections.length, + inspections }; - - if (organizationId && !existing.organizationId) { - updateData.organizationId = organizationId; - } - - const updated = await Inspection.findOneAndUpdate({ _id: id }, updateData, { new: true }).lean(); - if (updated) { - return { ...updated, id: updated._id.toString() }; - } - return null; }; -export const deleteInspection = async (id: string, organizationId?: string, userId?: string, userRole?: string, isDeveloper: boolean = false) => { - const existing = await Inspection.findById(id); - if (!existing) return false; - - // Organization Check - if (organizationId && existing.organizationId && existing.organizationId !== organizationId) { - return false; - } - - // Role/Ownership check - const isPowerUser = userRole === 'admin' || isDeveloper; - if (!isPowerUser && existing.createdBy && existing.createdBy !== userId) { - return false; - } - - await Inspection.deleteOne({ _id: id }); - return true; -}; - -export const getAllInspections = async (organizationId?: string) => { - const query = organizationId ? { organizationId } : {}; - const inspections = await Inspection.find(query).lean(); - return inspections.map(i => ({ ...i, id: i._id.toString() })); -}; - - - +console.log('✅ InspectionService loaded with compatibility'); diff --git a/src/server/services/paintingSchemeService.ts b/src/server/services/paintingSchemeService.ts index 5058162..074293b 100644 --- a/src/server/services/paintingSchemeService.ts +++ b/src/server/services/paintingSchemeService.ts @@ -1,72 +1,41 @@ -import PaintingScheme from '../models/PaintingScheme.js'; +import { PaintingScheme } from '../lib/compat.js'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const createPaintingScheme = async (data: any & { organizationId?: string }) => { - const newScheme = new PaintingScheme({ ...data, organizationId: data.organizationId }); - const saved = await newScheme.save(); - return { ...saved.toObject(), id: saved._id.toString() }; + return await PaintingScheme.create({ ...data, organization_id: data.organizationId }); }; export const getPaintingSchemesByProject = async (projectId: string, organizationId?: string) => { - const query = { projectId, ...(organizationId ? { organizationId } : {}) }; - const schemes = await PaintingScheme.find(query).lean(); - return schemes.map(s => ({ ...s, id: s._id.toString() })); + const filter: any = { project_id: projectId }; + if (organizationId) { + filter.organization_id = organizationId; + } + return await PaintingScheme.find(filter); +}; + +export const getPaintingSchemeById = async (id: string) => { + return await PaintingScheme.findById(id); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const updatePaintingScheme = async (id: string, data: any, organizationId?: string) => { - // SECURITY FIX: Allow update if: - // 1. Matches ID AND Matches Organization - // 2. OR Matches ID AND Record has NO organization (legacy/orphan) -> Adopt it! - - let query: any = { _id: id }; - - // First, check if the record exists and what is its state const existing = await PaintingScheme.findById(id); - if (!existing) return null; - - // Check ownership - if (organizationId && existing.organizationId && existing.organizationId !== organizationId) { - // Exists but belongs to ANOTHER organization -> Deny - console.warn(`Access Denied: Scheme ${id} belongs to ${existing.organizationId}, user is ${organizationId}`); - return null; // Return null effectively hides it or acts as fail + + if (organizationId && existing.organization_id && existing.organization_id !== organizationId) { + return null; } - - // If we passed the check, we perform the update. - // Ensure we "adopt" the record if it didn't have an orgId - if (organizationId && !data.organizationId) { - data.organizationId = organizationId; - } - - const updated = await PaintingScheme.findOneAndUpdate({ _id: id }, data, { new: true }).lean(); - if (updated) { - return { ...updated, id: updated._id.toString() }; - } - return null; + + return await PaintingScheme.findByIdAndUpdate(id, data); }; -export const deletePaintingScheme = async (id: string, organizationId?: string) => { - // Find first to check permissions - const existing = await PaintingScheme.findById(id); - if (!existing) return; - - // Permissions: - // If user has org, and item has OTHER org, deny. - if (organizationId && existing.organizationId && existing.organizationId !== organizationId) { - console.warn(`[Delete PaintingScheme] Access Denied. User Org: ${organizationId}, Scheme Org: ${existing.organizationId}`); - return; - } - - await PaintingScheme.findByIdAndDelete(id); +export const deletePaintingScheme = async (id: string) => { + return await PaintingScheme.findByIdAndDelete(id); }; -export const getAllSchemes = async (organizationId?: string) => { - const query = organizationId ? { organizationId } : {}; - const schemes = await PaintingScheme.find(query).lean(); - return schemes.map(s => ({ ...s, id: s._id.toString() })); +export const clonePaintingScheme = async (id: string, newData: any) => { + const original = await PaintingScheme.findById(id); + if (!original) return null; + + return await PaintingScheme.create({ ...original, ...newData, id: undefined }); }; - - +console.log('✅ PaintingSchemeService loaded with compatibility'); diff --git a/src/server/services/partService.ts b/src/server/services/partService.ts index 19f394f..eb550a4 100644 --- a/src/server/services/partService.ts +++ b/src/server/services/partService.ts @@ -1,60 +1,39 @@ -import Part from '../models/Part.js'; +import { Part, supabase, findOneGpi, queryGpi } from '../lib/compat.js'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const createPart = async (data: any & { organizationId?: string }) => { - const newPart = new Part({ ...data, organizationId: data.organizationId }); - const saved = await newPart.save(); - return { ...saved.toObject(), id: saved._id.toString() }; + return await Part.create({ ...data, organization_id: data.organizationId }); }; export const getPartsByProject = async (projectId: string, organizationId?: string, isGlobalAdmin: boolean = false) => { - const query = isGlobalAdmin - ? { projectId } - : { projectId, $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] }; - const parts = await Part.find(query).lean(); - return parts.map(p => ({ ...p, id: p._id.toString() })); + const filter: any = { project_id: projectId }; + if (!isGlobalAdmin && organizationId) { + filter.organization_id = organizationId; + } + return await Part.find(filter); +}; + +export const getPartById = async (id: string) => { + return await Part.findById(id); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const updatePart = async (id: string, data: any, organizationId?: string, isGlobalAdmin: boolean = false) => { const existing = await Part.findById(id); if (!existing) return null; - if (!isGlobalAdmin && organizationId && existing.organizationId && existing.organizationId !== organizationId) { - console.warn(`Access Denied: Part ${id} belongs to ${existing.organizationId}, user is ${organizationId}`); + if (!isGlobalAdmin && organizationId && existing.organization_id && existing.organization_id !== organizationId) { + console.warn(`Access Denied: Part ${id} belongs to ${existing.organization_id}, user is ${organizationId}`); return null; } - if (organizationId && !existing.organizationId) { - data.organizationId = organizationId; // Adopt - } - - const updated = await Part.findOneAndUpdate({ _id: id }, data, { new: true }).lean(); - if (updated) { - return { ...updated, id: updated._id.toString() }; - } - return null; + return await Part.findByIdAndUpdate(id, data); }; -export const deletePart = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => { - const part = await Part.findById(id); - if (!part) return; - - if (!isGlobalAdmin && organizationId && part.organizationId && part.organizationId !== organizationId) { - throw new Error('Sem permissão para excluir esta peça'); - } - - await Part.findByIdAndDelete(id); +export const deletePart = async (id: string) => { + return await Part.findByIdAndDelete(id); }; -export const getAllParts = async (organizationId?: string, isGlobalAdmin: boolean = false) => { - const query = isGlobalAdmin - ? {} - : { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] }; - const parts = await Part.find(query).lean(); - return parts.map(p => ({ ...p, id: p._id.toString() })); +export const getPartsByOrganization = async (organizationId: string) => { + return await Part.find({ organization_id: organizationId }); }; - - +console.log('✅ PartService loaded with compatibility'); diff --git a/src/server/services/projectService.ts b/src/server/services/projectService.ts index ba5e804..d4aa872 100644 --- a/src/server/services/projectService.ts +++ b/src/server/services/projectService.ts @@ -1,8 +1,7 @@ -import Project from '../models/Project.js'; -import Part from '../models/Part.js'; -import PaintingScheme from '../models/PaintingScheme.js'; -import ApplicationRecord from '../models/ApplicationRecord.js'; -import Inspection from '../models/Inspection.js'; +import { + Project, Part, PaintingScheme, ApplicationRecord, Inspection, + supabase, findOneGpi, queryGpi +} from '../lib/compat.js'; interface ProjectData { name: string; @@ -15,207 +14,73 @@ interface ProjectData { } export const createProject = async (data: ProjectData & { organizationId?: string }) => { - const newProject = new Project({ + const project = await Project.create({ name: data.name, client: data.client, - startDate: data.startDate ? new Date(data.startDate) : null, - endDate: data.endDate ? new Date(data.endDate) : null, + start_date: data.startDate ? new Date(data.startDate).toISOString() : null, + end_date: data.endDate ? new Date(data.endDate).toISOString() : null, technician: data.technician, environment: data.environment, - organizationId: data.organizationId, - weightKg: data.weightKg + organization_id: data.organizationId, + weight_kg: data.weightKg, + status: 'active' }); - return await newProject.save(); + return project; +}; + +export const getAllProjects = async (organizationId?: string, isGlobalAdmin?: boolean, status?: string) => { + const filter: any = {}; + if (organizationId && !isGlobalAdmin) { + filter.organization_id = organizationId; + } + if (status) { + filter.status = status; + } + return await Project.find(filter); }; export const getDashboardProjects = async (organizationId?: string) => { - const matchStage = organizationId ? { organizationId } : {}; - - const projects = await Project.aggregate([ - { $match: matchStage }, - { $sort: { name: 1 } }, - { - $lookup: { - from: 'paintingschemes', - localField: '_id', - foreignField: 'projectId', - as: 'paintingSchemes' - } - }, - { - $lookup: { - from: 'inspections', - localField: '_id', - foreignField: 'projectId', - as: 'inspections' - } - }, - { - $project: { - _id: 1, - name: 1, - client: 1, - technician: 1, - weightKg: 1, - createdAt: 1, - schemes: { - $map: { - input: "$paintingSchemes", - as: "scheme", - in: { - id: { $toString: "$$scheme._id" }, - name: "$$scheme.name", - type: "$$scheme.type", - coat: "$$scheme.coat", - color: "$$scheme.color", - colorHex: "$$scheme.colorHex", - thinnerSymbol: "$$scheme.thinnerSymbol", - epsMin: "$$scheme.epsMin", - epsMax: "$$scheme.epsMax" - } - } - }, - paintedWeight: { $sum: "$inspections.weightKg" } - } - } - ]); - - return projects.map(p => ({ ...p, id: p._id.toString() })); + const filter: any = organizationId ? { organization_id: organizationId } : {}; + return await Project.find(filter); }; -export const getAllProjects = async (organizationId?: string, isGlobalAdmin: boolean = false, status: string = 'active') => { - const statusQuery = status === 'active' - ? { status: { $ne: 'archived' } } - : { status: 'archived' }; - - const matchQuery: Record = isGlobalAdmin - ? { ...statusQuery } - : { - ...statusQuery, - $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] - }; - - const projects = await Project.aggregate([ - { $match: matchQuery }, - { $sort: { name: 1 } }, - { - $lookup: { - from: 'paintingschemes', - localField: '_id', - foreignField: 'projectId', - as: 'paintingSchemes' - } - }, - { - $addFields: { - id: { $toString: "$_id" } - } - } - ]); - - return projects; -}; - -export const archiveProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => { +export const archiveProject = async (id: string, organizationId?: string, isGlobalAdmin?: boolean) => { const project = await Project.findById(id); if (!project) throw new Error('Projeto não encontrado'); - - // Check ownership - if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) { - throw new Error('Sem permissão para arquivar este projeto'); + + if (!isGlobalAdmin && project.organization_id !== organizationId) { + throw new Error('Acesso negado'); } - - const newStatus = project.status === 'active' ? 'archived' : 'active'; - const updated = await Project.findByIdAndUpdate(id, { status: newStatus }, { new: true }).lean(); - return updated; + + return await Project.findByIdAndUpdate(id, { status: 'archived' }); }; -export const getProjectById = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => { - const project = await Project.findById(id).lean(); +export const getProjectById = async (id: string) => { + return await Project.findById(id); +}; - if (!project) throw new Error('Projeto não encontrado'); +export const updateProject = async (id: string, data: Partial) => { + return await Project.findByIdAndUpdate(id, data); +}; - // Security check: Allow if global admin OR matches organization OR project has no organization - if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) { - throw new Error('Acesso negado a este projeto'); - } - - const [parts, schemes, records, inspections] = await Promise.all([ - Part.find({ projectId: id }).lean(), - PaintingScheme.find({ projectId: id }).populate('paintId thinnerId').lean(), - ApplicationRecord.find({ projectId: id }).lean(), - Inspection.find({ projectId: id }) - .populate({ - path: 'stockItemId', - select: 'batchNumber dataSheetId', - populate: { path: 'dataSheetId', select: 'name' } - }) - .lean() - ]); +export const deleteProject = async (id: string) => { + return await Project.findByIdAndDelete(id); +}; +export const getProjectStats = async (projectId: string) => { + const project = await Project.findById(projectId); + if (!project) return null; + + const schemes = await PaintingScheme.find({ project_id: projectId }); + const inspections = await Inspection.find({ project_id: projectId }); + const parts = await Part.find({ project_id: projectId }); + return { - ...project, - id: project._id.toString(), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - parts: parts.map((p: any) => ({ ...p, id: p._id.toString() })), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - paintingSchemes: schemes.map((s: any) => ({ ...s, id: s._id.toString() })), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - applicationRecords: records.map((r: any) => ({ ...r, id: r._id.toString() })), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - inspections: inspections.map((i: any) => ({ ...i, id: i._id.toString() })) + project, + schemesCount: schemes.length, + inspectionsCount: inspections.length, + partsCount: parts.length }; }; -export const updateProject = async (id: string, data: Partial, organizationId?: string, isGlobalAdmin: boolean = false) => { - const existing = await Project.findById(id); - if (!existing) return null; - - // Check ownership - if (!isGlobalAdmin && organizationId && existing.organizationId && existing.organizationId !== organizationId) { - console.warn(`Access Denied: Project ${id} belongs to ${existing.organizationId}, user is ${organizationId}`); - return null; - } - - const updateData: Partial & { updatedAt: Date, organizationId?: string } = { - ...data, - updatedAt: new Date(), - startDate: data.startDate ? new Date(data.startDate) : undefined, - endDate: data.endDate ? new Date(data.endDate) : undefined, - weightKg: data.weightKg, - }; - - // Adopt if needed - if (organizationId && !existing.organizationId) { - updateData.organizationId = organizationId; - } - - const updated = await Project.findOneAndUpdate({ _id: id }, updateData, { new: true }).lean(); - if (updated) { - return { ...updated, id: updated._id.toString() }; - } - return null; -}; - -export const deleteProject = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => { - const project = await Project.findById(id); - if (!project) return; - - // Check ownership - if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) { - throw new Error('Sem permissão para excluir este projeto'); - } - - await Project.findByIdAndDelete(id); - - // Also cleanup related data - await Promise.all([ - Part.deleteMany({ projectId: id }), - PaintingScheme.deleteMany({ projectId: id }), - ApplicationRecord.deleteMany({ projectId: id }), - Inspection.deleteMany({ projectId: id }) - ]); -}; - - - +console.log('✅ ProjectService loaded with compatibility layer');