Migracao Logto + Supabase - backend e frontend atualizados para nova autenticação
This commit is contained in:
510
package-lock.json
generated
510
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
13
package.json
13
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",
|
||||
|
||||
@@ -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 <OrganizationSelector />;
|
||||
}
|
||||
|
||||
console.log('Organization exists - showing main app');
|
||||
return (
|
||||
<ToastProvider>
|
||||
<AuthProvider>
|
||||
@@ -109,12 +95,7 @@ const AppContent: React.FC = () => {
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<SignedOut>
|
||||
<Login />
|
||||
</SignedOut>
|
||||
<SignedIn>
|
||||
<AppContent />
|
||||
</SignedIn>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<AuthProviderProps> = ({ children }) => {
|
||||
const { user, isLoaded } = useUser();
|
||||
const { organization, membership } = useOrganization();
|
||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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);
|
||||
}
|
||||
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 };
|
||||
} catch (err) {
|
||||
console.error('Error syncing user:', err);
|
||||
setError('Erro ao carregar dados do usuário');
|
||||
} 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({
|
||||
const effectiveRole = userData.role || 'guest';
|
||||
|
||||
const user = {
|
||||
...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();
|
||||
setUser(token, user);
|
||||
setAppUser(user);
|
||||
setIsSignedIn(true);
|
||||
} catch (err) {
|
||||
console.error('Error loading user:', err);
|
||||
setError('Erro ao carregar dados do usuário');
|
||||
setAppUser(null);
|
||||
setIsSignedIn(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [isLoaded, user, organization?.id, syncUser]);
|
||||
}, []);
|
||||
|
||||
const refetchUser = useCallback(async () => {
|
||||
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<AuthProviderProps> = ({ children }) => {
|
||||
value={{
|
||||
appUser,
|
||||
isLoading,
|
||||
isSignedIn: !!user,
|
||||
isSignedIn,
|
||||
error,
|
||||
isAdmin,
|
||||
isUser,
|
||||
|
||||
@@ -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(
|
||||
<ClerkProvider
|
||||
publishableKey={PUBLISHABLE_KEY}
|
||||
afterSignOutUrl="/"
|
||||
localization={ptBR}
|
||||
appearance={{
|
||||
variables: {
|
||||
colorPrimary: '#fb923c', // Cor primária do GPI (Laranja)
|
||||
colorBackground: '#ffffff',
|
||||
colorText: '#1c1917',
|
||||
colorTextSecondary: '#57534e',
|
||||
borderRadius: '0.75rem',
|
||||
},
|
||||
elements: {
|
||||
card: "shadow-none border-0 bg-transparent", // Deixamos o container da página controlar o card
|
||||
navbar: "hidden",
|
||||
headerTitle: "text-2xl font-bold tracking-tight",
|
||||
headerSubtitle: "text-text-muted font-medium",
|
||||
formButtonPrimary: "bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20",
|
||||
socialButtonsBlockButton: "bg-white hover:bg-surface-hover border-border/40 text-text-main font-semibold transition-all duration-300 rounded-xl",
|
||||
footerActionLink: "text-primary hover:text-primary/80 font-bold",
|
||||
formFieldInput: "bg-surface-soft border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl",
|
||||
organizationSwitcherTrigger: "hover:bg-surface-hover transition-colors rounded-xl",
|
||||
organizationPreviewMainIdentifier: "font-bold",
|
||||
// Personalização específica para a lista de organizações que aparece na imagem
|
||||
organizationListPreview: "hover:bg-surface-soft rounded-xl transition-all p-3",
|
||||
organizationListCreateOrganizationButton: "text-primary font-bold hover:text-primary/80",
|
||||
}
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</ClerkProvider>,
|
||||
)
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden">
|
||||
{/* Background decorative elements */}
|
||||
@@ -18,13 +22,20 @@ export const Login = () => {
|
||||
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
|
||||
</div>
|
||||
|
||||
{/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */}
|
||||
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-4 animate-in slide-in-from-bottom-8 duration-1000">
|
||||
<SignIn
|
||||
afterSignInUrl="/"
|
||||
afterSignUpUrl="/"
|
||||
forceRedirectUrl="/"
|
||||
/>
|
||||
{/* Login Button - Logto */}
|
||||
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-8 animate-in slide-in-from-bottom-8 duration-1000">
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-primary hover:bg-primary/90 text-white font-bold rounded-xl transition-all shadow-lg shadow-primary/20"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
Continuar com Google
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
69
src/server/config/supabase.ts
Normal file
69
src/server/config/supabase.ts
Normal file
@@ -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<string, any>) {
|
||||
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');
|
||||
@@ -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,
|
||||
if (!user) {
|
||||
const { data, error } = await supabase
|
||||
.from('users')
|
||||
.insert({
|
||||
email,
|
||||
name,
|
||||
role: 'guest', // Default global role
|
||||
isBanned: false
|
||||
});
|
||||
}
|
||||
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.' });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
84
src/server/lib/compat.ts
Normal file
84
src/server/lib/compat.ts
Normal file
@@ -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');
|
||||
71
src/server/lib/db.ts
Normal file
71
src/server/lib/db.ts
Normal file
@@ -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');
|
||||
88
src/server/middleware/authMiddleware.ts
Normal file
88
src/server/middleware/authMiddleware.ts
Normal file
@@ -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();
|
||||
};
|
||||
81
src/server/middleware/logtoAuth.ts
Normal file
81
src/server/middleware/logtoAuth.ts
Normal file
@@ -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<AppUser | null> {
|
||||
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();
|
||||
};
|
||||
}
|
||||
41
src/server/scripts/migrateLogto.ts
Normal file
41
src/server/scripts/migrateLogto.ts
Normal file
@@ -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);
|
||||
@@ -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<string> => {
|
||||
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<string> => {
|
||||
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<boolean> => {
|
||||
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<string, string> = {
|
||||
'.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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 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() };
|
||||
}
|
||||
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
||||
return null;
|
||||
};
|
||||
|
||||
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);
|
||||
return await PaintingScheme.findByIdAndUpdate(id, data);
|
||||
};
|
||||
|
||||
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 deletePaintingScheme = async (id: string) => {
|
||||
return await PaintingScheme.findByIdAndDelete(id);
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<string, unknown> = 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<ProjectData>) => {
|
||||
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');
|
||||
}
|
||||
export const deleteProject = async (id: string) => {
|
||||
return await Project.findByIdAndDelete(id);
|
||||
};
|
||||
|
||||
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 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<ProjectData>, 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<ProjectData> & { 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');
|
||||
|
||||
Reference in New Issue
Block a user