Migracao Logto + Supabase - backend e frontend atualizados para nova autenticação

This commit is contained in:
2026-03-30 20:50:10 +00:00
parent 9d3958b82b
commit f89d5571f4
22 changed files with 1266 additions and 1047 deletions

510
package-lock.json generated
View File

@@ -8,10 +8,10 @@
"name": "gpi-app", "name": "gpi-app",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@clerk/clerk-react": "^5.59.6", "@clerk/clerk-react": "^5.61.4",
"@clerk/localizations": "^3.35.3", "@logto/node": "^2.4.0",
"@supabase/supabase-js": "^2.47.0",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@types/mongoose": "^5.11.96",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vercel/speed-insights": "^1.3.1", "@vercel/speed-insights": "^1.3.1",
"axios": "^1.13.2", "axios": "^1.13.2",
@@ -22,9 +22,9 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"enhanced-resolve": "^5.18.4", "enhanced-resolve": "^5.18.4",
"express": "^5.2.1", "express": "^5.2.1",
"jose": "^5.2.0",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"mongodb": "^7.0.0", "mongodb": "^7.1.1",
"mongoose": "^9.1.5",
"multer": "^2.0.2", "multer": "^2.0.2",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
@@ -42,7 +42,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.5", "@types/react": "^19.2.5",
@@ -55,6 +55,7 @@
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0", "globals": "^16.5.0",
"mongoose": "^8.23.0",
"nodemon": "^3.1.11", "nodemon": "^3.1.11",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
@@ -135,7 +136,6 @@
"version": "7.28.6", "version": "7.28.6",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.28.6", "@babel/code-frame": "^7.28.6",
"@babel/generator": "^7.28.6", "@babel/generator": "^7.28.6",
@@ -1638,10 +1638,12 @@
} }
}, },
"node_modules/@clerk/clerk-react": { "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", "license": "MIT",
"dependencies": { "dependencies": {
"@clerk/shared": "^3.43.2", "@clerk/shared": "^3.47.3",
"tslib": "2.8.1" "tslib": "2.8.1"
}, },
"engines": { "engines": {
@@ -1652,22 +1654,10 @@
"react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" "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": { "node_modules/@clerk/shared": {
"version": "3.44.0", "version": "3.47.3",
"resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.44.0.tgz", "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.47.3.tgz",
"integrity": "sha512-kH+chNeZwqml3IDpWLgebWECfOZifyUQO4OISd/96w1EuCY1Bzw6cBq/ZbpsoO8jyG8/6bGr/MGXLhDzTrpPfA==", "integrity": "sha512-jG0wMIZuuc8zaKieg9Os8ocTphG+llluRukUUdyVnu4+ZI1syVf+dkpDP3ZK69yLavTX3D0KAmkmQqTPzQV/Nw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "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": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"dev": true, "dev": true,
@@ -2508,6 +2486,39 @@
"sisteransi": "^1.0.5" "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": { "node_modules/@mapbox/node-pre-gyp": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.3.tgz", "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": { "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", "license": "MIT",
"dependencies": { "dependencies": {
"sparse-bitfield": "^3.0.3" "sparse-bitfield": "^3.0.3"
@@ -3042,6 +3055,16 @@
"win32" "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": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"license": "MIT" "license": "MIT"
@@ -3050,6 +3073,92 @@
"version": "0.3.0", "version": "0.3.0",
"license": "MIT" "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": { "node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", "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, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/mongoose": {
"version": "5.11.96",
"license": "MIT",
"dependencies": {
"mongoose": "*"
}
},
"node_modules/@types/multer": { "node_modules/@types/multer": {
"version": "2.0.0", "version": "2.0.0",
"dev": true, "dev": true,
@@ -3529,9 +3631,7 @@
"version": "24.10.9", "version": "24.10.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz",
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -3550,7 +3650,6 @@
"version": "19.2.9", "version": "19.2.9",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -3609,15 +3708,28 @@
}, },
"node_modules/@types/webidl-conversions": { "node_modules/@types/webidl-conversions": {
"version": "7.0.3", "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" "license": "MIT"
}, },
"node_modules/@types/whatwg-url": { "node_modules/@types/whatwg-url": {
"version": "13.0.0", "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", "license": "MIT",
"dependencies": { "dependencies": {
"@types/webidl-conversions": "*" "@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": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.53.1", "version": "8.53.1",
"dev": true, "dev": true,
@@ -3657,7 +3769,6 @@
"version": "8.53.1", "version": "8.53.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/scope-manager": "8.53.1",
"@typescript-eslint/types": "8.53.1", "@typescript-eslint/types": "8.53.1",
@@ -4391,7 +4502,6 @@
"integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==", "integrity": "sha512-o9bjXmDNcF7GbM4CNQpmi+TutCgap/K3w1JyKgxAjqx41zp9qlIAVFi0IhCNsJcXolEqLWhbFbEeL0PvYm4pcQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@@ -4546,7 +4656,6 @@
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -4692,7 +4801,6 @@
"version": "8.15.0", "version": "8.15.0",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -5100,7 +5208,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -5116,7 +5223,9 @@
} }
}, },
"node_modules/bson": { "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", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=20.19.0"
@@ -5207,6 +5316,48 @@
"node": ">=6" "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": { "node_modules/caniuse-lite": {
"version": "1.0.30001766", "version": "1.0.30001766",
"dev": true, "dev": true,
@@ -5526,6 +5677,8 @@
}, },
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": { "node_modules/d3-array": {
@@ -6157,7 +6310,6 @@
"version": "9.39.2", "version": "9.39.2",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -6913,6 +7065,8 @@
}, },
"node_modules/glob-to-regexp": { "node_modules/glob-to-regexp": {
"version": "0.4.1", "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" "license": "BSD-2-Clause"
}, },
"node_modules/glob/node_modules/minimatch": { "node_modules/glob/node_modules/minimatch": {
@@ -7108,6 +7262,15 @@
"node": ">=16.17.0" "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": { "node_modules/iconv-lite": {
"version": "0.7.2", "version": "0.7.2",
"license": "MIT", "license": "MIT",
@@ -7753,8 +7916,25 @@
"jiti": "lib/jiti-cli.mjs" "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": { "node_modules/js-cookie": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@@ -7853,13 +8033,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/kareem": {
"version": "3.0.0",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"dev": true, "dev": true,
@@ -8219,6 +8392,18 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"license": "MIT", "license": "MIT",
@@ -8235,6 +8420,8 @@
}, },
"node_modules/memory-pager": { "node_modules/memory-pager": {
"version": "1.5.0", "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" "license": "MIT"
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
@@ -8371,11 +8558,13 @@
} }
}, },
"node_modules/mongodb": { "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", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@mongodb-js/saslprep": "^1.3.0", "@mongodb-js/saslprep": "^1.3.0",
"bson": "^7.0.0", "bson": "^7.1.1",
"mongodb-connection-string-url": "^7.0.0" "mongodb-connection-string-url": "^7.0.0"
}, },
"engines": { "engines": {
@@ -8415,7 +8604,9 @@
} }
}, },
"node_modules/mongodb-connection-string-url": { "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", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@types/whatwg-url": "^13.0.0", "@types/whatwg-url": "^13.0.0",
@@ -8426,38 +8617,139 @@
} }
}, },
"node_modules/mongoose": { "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", "license": "MIT",
"dependencies": { "dependencies": {
"kareem": "3.0.0", "bson": "^6.10.4",
"mongodb": "~7.0", "kareem": "2.6.3",
"mongodb": "~6.20.0",
"mpath": "0.9.0", "mpath": "0.9.0",
"mquery": "6.0.0", "mquery": "5.0.0",
"ms": "2.1.3", "ms": "2.1.3",
"sift": "17.1.3" "sift": "17.1.3"
}, },
"engines": { "engines": {
"node": ">=20.19.0" "node": ">=16.20.1"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/mongoose" "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": { "node_modules/mpath": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
"integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4.0.0" "node": ">=4.0.0"
} }
}, },
"node_modules/mquery": {
"version": "6.0.0",
"license": "MIT",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/mri": { "node_modules/mri": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -9048,7 +9340,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -9174,6 +9465,18 @@
], ],
"license": "MIT" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -9207,7 +9510,6 @@
"node_modules/react": { "node_modules/react": {
"version": "19.2.3", "version": "19.2.3",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -9215,7 +9517,6 @@
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.3", "version": "19.2.3",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -9225,13 +9526,11 @@
}, },
"node_modules/react-is": { "node_modules/react-is": {
"version": "19.2.3", "version": "19.2.3",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/react-redux": { "node_modules/react-redux": {
"version": "9.2.0", "version": "9.2.0",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@@ -9369,8 +9668,7 @@
}, },
"node_modules/redux": { "node_modules/redux": {
"version": "5.0.1", "version": "5.0.1",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@@ -9994,6 +10292,9 @@
}, },
"node_modules/sift": { "node_modules/sift": {
"version": "17.1.3", "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" "license": "MIT"
}, },
"node_modules/signal-exit": { "node_modules/signal-exit": {
@@ -10123,6 +10424,8 @@
}, },
"node_modules/sparse-bitfield": { "node_modules/sparse-bitfield": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"memory-pager": "^1.0.2" "memory-pager": "^1.0.2"
@@ -10137,6 +10440,8 @@
}, },
"node_modules/std-env": { "node_modules/std-env": {
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/stop-iteration-iterator": { "node_modules/stop-iteration-iterator": {
@@ -10350,6 +10655,8 @@
}, },
"node_modules/swr": { "node_modules/swr": {
"version": "2.3.4", "version": "2.3.4",
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.4.tgz",
"integrity": "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dequal": "^2.0.3", "dequal": "^2.0.3",
@@ -10470,7 +10777,6 @@
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0", "acorn": "^8.15.0",
@@ -10567,6 +10873,8 @@
}, },
"node_modules/tr46": { "node_modules/tr46": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"punycode": "^2.3.1" "punycode": "^2.3.1"
@@ -10664,7 +10972,6 @@
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "~0.27.0", "esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5" "get-tsconfig": "^4.7.5"
@@ -10798,7 +11105,6 @@
"version": "5.9.3", "version": "5.9.3",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -10883,7 +11189,6 @@
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.16.0", "version": "7.16.0",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-canonical-property-names-ecmascript": { "node_modules/unicode-canonical-property-names-ecmascript": {
@@ -11066,7 +11371,6 @@
"version": "7.3.1", "version": "7.3.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -11173,6 +11477,8 @@
}, },
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "7.0.0", "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", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -11180,6 +11486,8 @@
}, },
"node_modules/whatwg-url": { "node_modules/whatwg-url": {
"version": "14.2.0", "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", "license": "MIT",
"dependencies": { "dependencies": {
"tr46": "^5.1.0", "tr46": "^5.1.0",
@@ -11456,7 +11764,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -11591,7 +11898,6 @@
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
}, },
@@ -11758,6 +12064,27 @@
"version": "1.0.2", "version": "1.0.2",
"license": "ISC" "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": { "node_modules/wsl-utils": {
"version": "0.1.0", "version": "0.1.0",
"license": "MIT", "license": "MIT",
@@ -11843,7 +12170,6 @@
"version": "4.3.6", "version": "4.3.6",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -13,10 +13,10 @@
"start": "node dist/server/index.js" "start": "node dist/server/index.js"
}, },
"dependencies": { "dependencies": {
"@clerk/clerk-react": "^5.59.6", "@clerk/clerk-react": "^5.61.4",
"@clerk/localizations": "^3.35.3", "@logto/node": "^2.4.0",
"@supabase/supabase-js": "^2.47.0",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@types/mongoose": "^5.11.96",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vercel/speed-insights": "^1.3.1", "@vercel/speed-insights": "^1.3.1",
"axios": "^1.13.2", "axios": "^1.13.2",
@@ -27,9 +27,9 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"enhanced-resolve": "^5.18.4", "enhanced-resolve": "^5.18.4",
"express": "^5.2.1", "express": "^5.2.1",
"jose": "^5.2.0",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"mongodb": "^7.0.0", "mongodb": "^7.1.1",
"mongoose": "^9.1.5",
"multer": "^2.0.2", "multer": "^2.0.2",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
@@ -47,7 +47,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.0",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^19.2.5", "@types/react": "^19.2.5",
@@ -60,6 +60,7 @@
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0", "globals": "^16.5.0",
"mongoose": "^8.23.0",
"nodemon": "^3.1.11", "nodemon": "^3.1.11",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",

View File

@@ -1,5 +1,4 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; 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 { AuthProvider } from './context/AuthContext';
import { useAuth } from './context/useAuth'; import { useAuth } from './context/useAuth';
import { SystemSettingsProvider } from './context/SystemSettingsContext'; import { SystemSettingsProvider } from './context/SystemSettingsContext';
@@ -19,7 +18,6 @@ import { CalculatorDashboard } from './pages/CalculatorDashboard';
import { StockDashboard } from './pages/StockDashboard'; import { StockDashboard } from './pages/StockDashboard';
import { GuestDashboard } from './pages/GuestDashboard'; import { GuestDashboard } from './pages/GuestDashboard';
import { Login } from './pages/Login'; import { Login } from './pages/Login';
import { OrganizationSelector } from './pages/OrganizationSelector';
import InstrumentList from './pages/InstrumentList'; import InstrumentList from './pages/InstrumentList';
const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { 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 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 ( return (
<ToastProvider> <ToastProvider>
<AuthProvider> <AuthProvider>
@@ -109,12 +95,7 @@ const AppContent: React.FC = () => {
function App() { function App() {
return ( return (
<Router> <Router>
<SignedOut> <AppContent />
<Login />
</SignedOut>
<SignedIn>
<AppContent />
</SignedIn>
</Router> </Router>
); );
} }

View File

@@ -1,129 +1,83 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { useUser, useOrganization } from '@clerk/clerk-react';
import type { AppUser } from '../types'; import type { AppUser } from '../types';
import { AuthContext } from './AuthContextType'; 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 { interface AuthProviderProps {
children: React.ReactNode; children: React.ReactNode;
} }
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const { user, isLoaded } = useUser();
const { organization, membership } = useOrganization();
const [appUser, setAppUser] = useState<AppUser | null>(null); const [appUser, setAppUser] = useState<AppUser | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); 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(() => { useEffect(() => {
setApiClerkUserId(user?.id || null); const token = getToken();
setApiOrganizationId(organization?.id || null); const user = getUser();
}, [user?.id, organization?.id]);
if (token && user) {
setAppUser(user as AppUser);
setIsSignedIn(true);
}
setIsLoading(false);
}, []);
const syncUser = useCallback(async () => { const syncUser = useCallback(async () => {
if (!user) { const token = getToken();
if (!token) {
setAppUser(null); setAppUser(null);
setIsSignedIn(false);
setIsLoading(false); setIsLoading(false);
return; return;
} }
try { try {
// Only set loading if the context has changed (new user or new organization) setIsLoading(true);
// 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); setError(null);
// Sync user with backend, including organization context const response = await fetch(`${API_URL}/users/me`, {
const response = await fetch(`${API_URL}/users/sync`, {
method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`,
'x-clerk-user-id': user.id,
...(organization?.id && { 'x-organization-id': organization.id }),
}, },
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) { if (!response.ok) {
const data = await response.json(); throw new Error('Falha ao carregar usuário');
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');
} }
const syncedUser = await response.json(); const userData = await response.json();
// Use organizationRole if available (per-org role), otherwise fall back to global role const effectiveRole = userData.role || 'guest';
const effectiveRole = syncedUser.organizationRole || syncedUser.role || 'guest';
setAppUser({ const user = {
...syncedUser, ...userData,
id: syncedUser._id || syncedUser.id, id: userData._id || userData.id,
role: effectiveRole, // Override with organization-specific role role: effectiveRole,
}); };
// Update last context ref setUser(token, user);
lastContextRef.current = { clerkId: user.id, orgId: organization?.id || null }; setAppUser(user);
setIsSignedIn(true);
} catch (err) { } catch (err) {
console.error('Error syncing user:', err); console.error('Error loading user:', err);
setError('Erro ao carregar dados do usuário'); setError('Erro ao carregar dados do usuário');
setAppUser(null);
setIsSignedIn(false);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [user, organization?.id, membership?.role]); }, []);
const refetchUser = useCallback(async () => { const refetchUser = useCallback(async () => {
if (!user) return; await syncUser();
}, [syncUser]);
try {
const response = await fetch(`${API_URL}/users/me`, {
headers: {
'x-clerk-user-id': user.id,
...(organization?.id && { 'x-organization-id': organization.id }),
},
});
if (response.ok) {
const userData = await response.json();
const effectiveRole = userData.organizationRole || userData.role || 'guest';
setAppUser({
...userData,
id: userData._id || userData.id,
role: effectiveRole,
});
}
} catch (err) {
console.error('Error refetching user:', err);
}
}, [user, organization?.id]);
// Re-sync when organization changes
useEffect(() => {
if (isLoaded && user) {
syncUser();
}
}, [isLoaded, user, organization?.id, syncUser]);
const isDeveloper = useCallback(() => { const isDeveloper = useCallback(() => {
return user?.primaryEmailAddress?.emailAddress === 'admtracksteel@gmail.com'; return appUser?.email === 'admtracksteel@gmail.com';
}, [user]); }, [appUser]);
const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]); const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]);
const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]); const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]);
@@ -135,7 +89,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
value={{ value={{
appUser, appUser,
isLoading, isLoading,
isSignedIn: !!user, isSignedIn,
error, error,
isAdmin, isAdmin,
isUser, isUser,

View File

@@ -1,47 +1,57 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { ClerkProvider } from '@clerk/clerk-react'
import { ptBR } from '@clerk/localizations'
import './index.css' import './index.css'
import App from './App.tsx' 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) { const redirectUrl = `${window.location.origin}/auth/callback`;
throw new Error("Missing Publishable Key")
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( createRoot(document.getElementById('root')!).render(
<ClerkProvider <App />
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>,
) )

View File

@@ -1,7 +1,11 @@
import { SignIn } from "@clerk/clerk-react";
import { Hammer } from "lucide-react"; import { Hammer } from "lucide-react";
import { login as logtoLogin } from "../main";
export const Login = () => { export const Login = () => {
const handleLogin = () => {
logtoLogin();
};
return ( return (
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden"> <div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden">
{/* Background decorative elements */} {/* 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> <p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
</div> </div>
{/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */} {/* Login Button - Logto */}
<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"> <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">
<SignIn <button
afterSignInUrl="/" onClick={handleLogin}
afterSignUpUrl="/" 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"
forceRedirectUrl="/" >
/> <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>
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium"> <div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">

View File

@@ -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 axios from 'axios';
import { triggerGuestWarning } from '../utils/toastHandler'; import { triggerGuestWarning } from '../utils/toastHandler';
import { getToken } from '../main';
export const getBaseUrl = () => { 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) { if (import.meta.env.VITE_API_URL) {
return 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 currentOrgId: string | null = null;
let currentOrgName: 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) => { export const setApiOrgData = (orgId: string | null, orgName: string | null = null) => {
currentOrgId = orgId; currentOrgId = orgId;
currentOrgName = orgName; currentOrgName = orgName;
}; };
// Legacy support export const setApiOrganizationId = setApiOrgData;
export const setApiOrgId = (orgId: string | null) => {
setApiOrgData(orgId, null);
};
// Alias for consistency
export const setApiOrganizationId = setApiOrgId;
// Request interceptor to add clerk user ID and Org ID headers
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
if (currentClerkUserId) { const token = getToken();
config.headers['x-clerk-user-id'] = currentClerkUserId; if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
} }
if (currentOrgId) { if (currentOrgId) {
config.headers['x-organization-id'] = currentOrgId; config.headers['x-organization-id'] = currentOrgId;
} }
if (currentOrgName) { if (currentOrgName) {
// Encode to handle special characters
config.headers['x-organization-name'] = encodeURIComponent(currentOrgName); config.headers['x-organization-name'] = encodeURIComponent(currentOrgName);
} }
return config; return config;
@@ -61,12 +46,10 @@ api.interceptors.request.use(
} }
); );
// Response interceptor to handle 403 errors (guest access denied)
api.interceptors.response.use( api.interceptors.response.use(
(response) => response, (response) => response,
(error) => { (error) => {
if (error.response?.status === 403) { if (error.response?.status === 403) {
// Check if it's a guest permission error
const errorMessage = error.response?.data?.error || ''; const errorMessage = error.response?.data?.error || '';
if (errorMessage.includes('Convidados') || errorMessage.includes('guest') || errorMessage.includes('permissão')) { if (errorMessage.includes('Convidados') || errorMessage.includes('guest') || errorMessage.includes('permissão')) {
triggerGuestWarning(); triggerGuestWarning();

View File

@@ -22,19 +22,18 @@ import path from 'path';
const app = express(); const app = express();
app.use(cors({ app.use(cors({
origin: '*', // Be more specific in production origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], 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()); app.use(express.json());
import { extractUser } from './middleware/roleMiddleware.js'; import { extractUser } from './middleware/authMiddleware.js';
app.use(extractUser); app.use(extractUser);
// Static Uploads // Static Uploads
import fs from 'fs'; import fs from 'fs';
const uploadsPath = path.join(process.cwd(), 'uploads'); const uploadsPath = path.join(process.cwd(), 'uploads');
// Ensure uploads directory exists
if (!fs.existsSync(uploadsPath)) { if (!fs.existsSync(uploadsPath)) {
fs.mkdirSync(uploadsPath, { recursive: true }); fs.mkdirSync(uploadsPath, { recursive: true });
} }
@@ -61,7 +60,7 @@ app.use('/api/messages', messageRoutes);
app.use('/api/backup', backupRoutes); app.use('/api/backup', backupRoutes);
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() }); res.json({ status: 'ok', timestamp: new Date(), auth: 'logto' });
}); });
export default app; export default app;

View File

@@ -1,46 +1,16 @@
import mongoose from 'mongoose'; import { supabase } from './supabase.js';
import { GridFSBucket } from 'mongodb';
export let bucket: GridFSBucket;
export const connectDB = async () => { export const connectDB = async () => {
try { try {
const uri = process.env.MONGODB_URI; const { data, error } = await supabase.from('users').select('count');
if (!uri) {
throw new Error('MONGODB_URI is not defined in environment variables'); if (error) {
console.error('❌ Erro ao conectar no Supabase:', error);
throw error;
} }
if (mongoose.connection.readyState >= 1) { console.log('✅ Conectado ao Supabase (schema: gpi)');
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');
} catch (error) { } catch (error) {
console.error('❌ MongoDB connection error:', error); console.error('❌ Erro de conexão:', error);
console.warn('⚠️ Server will continue running for debugging, but database features will be unavailable.');
// process.exit(1);
} }
}; };

View 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');

View File

@@ -1,318 +1,173 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import User, { IUser } from '../models/User.js'; import { supabase, findOneGpi, queryGpi } from '../config/supabase.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;
}
interface AuthRequest extends Request { 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) => { export const syncUser = async (req: Request, res: Response) => {
console.log('--- syncUser called ---', req.body);
try { try {
const { clerkId, email, name, organizationId, clerkRole } = req.body; const { email, name } = req.body;
if (!clerkId || !email || !name) { if (!email || !name) {
return res.status(400).json({ error: 'clerkId, email e name são obrigatórios.' }); return res.status(400).json({ error: 'email e name são obrigatórios.' });
} }
// 1. Upsert the global User record let user = await findOneGpi('users', { email });
let user = await User.findOne({ clerkId });
if (user) { if (!user) {
user.email = email; const { data, error } = await supabase
user.name = name; .from('users')
await user.save(); .insert({
} else { email,
user = await User.create({ name,
clerkId, role: 'guest'
email, })
name, .select()
role: 'guest', // Default global role .single();
isBanned: false
});
}
if (organizationId) { if (error) throw error;
user = data;
// 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
});
} }
res.json(user); res.json(user);
} catch (error) { } catch (error: any) {
console.error('Error syncing user:', error); console.error('Error syncing user:', error);
// Retornar 200 mesmo com erro para não travar o frontend se for algo não crítico, res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + error.message });
// mas aqui é crítico. Vamos logar melhor.
res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + (error instanceof Error ? error.message : String(error)) });
} }
}; };
/**
* Get current user data with organization context
*/
export const getCurrentUser = async (req: AuthRequest, res: Response) => { export const getCurrentUser = async (req: AuthRequest, res: Response) => {
try { try {
if (!req.appUser) { if (!req.appUser) {
return res.status(404).json({ error: 'Usuário não encontrado.' }); 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); res.json(req.appUser);
} catch (error) { } catch (error: any) {
console.error('Error getting current user:', error); console.error('Error getting current user:', error);
res.status(500).json({ error: 'Erro ao buscar usuário.' }); 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) => { export const getAllUsers = async (req: Request, res: Response) => {
try { try {
const organizationId = req.headers['x-organization-id'] as string; const organizationId = req.headers['x-organization-id'] as string;
console.log('getAllUsers called with organizationId:', organizationId);
if (!organizationId) { if (!organizationId) {
return res.status(400).json({ error: 'Organização não selecionada.' }); return res.status(400).json({ error: 'Organização não selecionada.' });
} }
const members = await OrganizationMember.find({ organizationId }).sort({ createdAt: -1 }); const { data, error } = await supabase
console.log(`Found ${members.length} members for org ${organizationId}:`, members.map(m => ({ name: m.name, email: m.email, clerkId: m.clerkUserId }))); .from('user_organizations')
res.json(members); .select('*, users(*)')
} catch (error) { .eq('organization_id', organizationId);
if (error) throw error;
res.json(data || []);
} catch (error: any) {
console.error('Error getting users:', error); console.error('Error getting users:', error);
res.status(500).json({ error: 'Erro ao buscar usuários.' }); 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) => { export const updateUserRole = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { role } = req.body; 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)) { 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); const { data, error } = await supabase
if (!member || member.organizationId !== organizationId) { .from('user_organizations')
return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' }); .update({ role })
} .eq('id', id)
.select()
.single();
// Prevent removing the last admin if (error) throw error;
if (member.role === 'admin' && role !== 'admin') { res.json(data);
const adminCount = await OrganizationMember.countDocuments({ organizationId, role: 'admin' }); } catch (error: any) {
if (adminCount <= 1) { console.error('Error updating role:', error);
return res.status(400).json({ error: 'Não é possível remover o último administrador.' }); res.status(500).json({ error: 'Erro ao alterar role.' });
}
}
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.' });
} }
}; };
/**
* Ban or unban user within organization (admin only)
*/
export const toggleBanUser = async (req: AuthRequest, res: Response) => { export const toggleBanUser = async (req: AuthRequest, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const { isBanned } = req.body; const { isBanned } = req.body;
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) { const { data, error } = await supabase
return res.status(400).json({ error: 'Organização não selecionada.' }); .from('user_organizations')
} .update({ is_banned: isBanned })
.eq('id', id)
.select()
.single();
const member = await OrganizationMember.findById(id); if (error) throw error;
if (!member || member.organizationId !== organizationId) { res.json(data);
return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' }); } catch (error: any) {
}
// 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) {
console.error('Error toggling ban:', error); 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) => { export const heartbeat = async (req: AuthRequest, res: Response) => {
try { try {
if (!req.appUser) { if (!req.appUser) {
return res.status(401).json({ error: 'Não autenticado.' }); return res.status(401).json({ error: 'Não autenticado.' });
} }
// Update User model await supabase
await User.findByIdAndUpdate(req.appUser._id, { lastSeenAt: new Date() }); .from('users')
.update({ last_seen_at: new Date().toISOString() })
// Also update Organization Member for tighter query .eq('id', req.appUser.id);
// 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.
res.status(200).send(); res.status(200).send();
} catch (error) { } catch (error) {
// Silent fail for heartbeat
console.error('Heartbeat error:', error); console.error('Heartbeat error:', error);
res.status(500).send(); res.status(500).send();
} }
}; };
/**
* Get active users in the same organization (seen in last 2 mins)
*/
export const getActiveUsers = async (req: AuthRequest, res: Response) => { export const getActiveUsers = async (req: AuthRequest, res: Response) => {
try { try {
const organizationId = req.headers['x-organization-id'] as string; const organizationId = req.headers['x-organization-id'] as string;
const currentUserId = req.appUser?._id;
if (!organizationId) { if (!organizationId) {
return res.status(400).json([]); return res.status(400).json([]);
} }
// Find members of this org const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
const members = await OrganizationMember.find({ organizationId });
// Get their Clerk IDs const { data, error } = await supabase
const clerkIds = members.map(m => m.clerkUserId); .from('users')
.select('id, email, name, last_seen_at')
.gte('last_seen_at', twoMinutesAgo);
// Find Users who were seen recently (2 minutes) if (error) throw error;
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000); res.json(data || []);
} catch (error: any) {
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) {
console.error('Error getting active users:', error); console.error('Error getting active users:', error);
res.status(500).json([]); res.status(500).json([]);
} }
}; };
// Delete organization member
export const deleteUser = async (req: Request, res: Response) => { export const deleteUser = async (req: Request, res: Response) => {
try { try {
const { id } = req.params; const { id } = req.params;
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) { const { error } = await supabase
return res.status(400).json({ error: 'Organização não selecionada.' }); .from('user_organizations')
} .delete()
.eq('id', id);
console.log(`Deleting member ${id} from organization ${organizationId}`); if (error) throw error;
res.json({ message: 'Membro removido com sucesso.' });
// Delete from OrganizationMember collection } catch (error: any) {
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) {
console.error('Error deleting user:', error); console.error('Error deleting user:', error);
res.status(500).json({ error: 'Erro ao remover membro.' }); res.status(500).json({ error: 'Erro ao remover membro.' });
} }

View File

@@ -1,8 +1,6 @@
import app from './app.js'; import app from './app.js';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { migrateFilesToGridFS } from './services/dataSheetService.js';
import { connectDB } from './config/database.js'; import { connectDB } from './config/database.js';
import mongoose from 'mongoose';
import { notificationService } from './services/notificationService.js'; import { notificationService } from './services/notificationService.js';
dotenv.config(); dotenv.config();
@@ -14,21 +12,7 @@ const startServer = async () => {
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, async () => { app.listen(PORT, async () => {
console.log(`🚀 Server running on port ${PORT}`); console.log(`🚀 Server running on port ${PORT}`);
if (mongoose.connection.readyState === 1) { console.log('✅ Conectado ao Supabase (GPI schema)');
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.');
}
}); });
} catch (error) { } catch (error) {
console.error('Failed to start server:', error); console.error('Failed to start server:', error);
@@ -37,5 +21,4 @@ const startServer = async () => {
startServer(); startServer();
// Force keep-alive to debug why it exits
setInterval(() => { }, 1000); setInterval(() => { }, 1000);

84
src/server/lib/compat.ts Normal file
View 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
View 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');

View 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();
};

View 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();
};
}

View 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);

View File

@@ -1,174 +1,109 @@
import TechnicalDataSheet from '../models/TechnicalDataSheet.js'; import TechnicalDataSheet from '../models/TechnicalDataSheet.js';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { bucket } from '../config/database.js'; import { supabase } from '../config/supabase.js';
import { ObjectId } from 'mongodb';
export const saveFileToGridFS = (localPath: string, filename: string): Promise<string> => { const BUCKET_NAME = 'gpi-files';
return new Promise((resolve, reject) => {
const uploadStream = bucket.openUploadStream(filename);
const readStream = fs.createReadStream(localPath);
readStream.pipe(uploadStream) export const saveFileToStorage = async (localPath: string, filename: string): Promise<string> => {
.on('error', reject) try {
.on('finish', () => { const fileBuffer = fs.readFileSync(localPath);
// Remove local file after upload const fileExt = path.extname(filename);
fs.unlink(localPath, (err) => { const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${fileExt}`;
if (err) console.error('Failed to delete local temp file:', err);
}); const { data, error } = await supabase.storage
resolve(uploadStream.id.toString()); .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 { 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; return true;
} catch (err) { } catch (err) {
console.error('Failed to delete file from GridFS:', err); console.error('Failed to delete file:', err);
return false; return false;
} }
}; };
export const getFileStream = (fileId: string) => { function getContentType(ext: string): string {
if (!ObjectId.isValid(fileId)) { const types: Record<string, string> = {
throw new Error('Invalid file ID format'); '.pdf': 'application/pdf',
} '.jpg': 'image/jpeg',
return bucket.openDownloadStream(new ObjectId(fileId)); '.jpeg': 'image/jpeg',
}; '.png': 'image/png',
'.doc': 'application/msword',
export const getAllDataSheets = async (organizationId?: string) => { '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
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' } }
]
}; };
const sheets = await TechnicalDataSheet.find(filter).lean(); return types[ext.toLowerCase()] || 'application/octet-stream';
return sheets.map(s => ({ ...s, id: s._id.toString() })); }
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any export const saveFileToGridFS = saveFileToStorage;
export const createDataSheet = async (data: any & { organizationId?: string }) => { export const deleteFileFromGridFS = deleteFileFromStorage;
let fileId = data.fileUrl;
// If fileUrl is a local path (exists on disk), move to GridFS export const getFileStream = (fileUrl: string) => {
if (data.fileUrl && fs.existsSync(data.fileUrl)) { return 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 migrateFilesToGridFS = async () => { export const migrateFilesToGridFS = async () => {
try { console.log(' File migration skipped - using Supabase Storage');
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);
}
}; };
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');

View File

@@ -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 }) => { export const createInspection = async (data: any & { organizationId?: string, createdBy?: string }) => {
const newInspection = new Inspection({ return await Inspection.create({
...data, ...data,
date: data.date ? new Date(data.date) : null, date: data.date ? new Date(data.date).toISOString() : null,
organizationId: data.organizationId, organization_id: data.organizationId,
createdBy: data.createdBy created_by: data.createdBy
}); });
const saved = await newInspection.save();
return { ...saved.toObject(), id: saved._id.toString() };
}; };
export const getInspectionsByProject = async (projectId: string, organizationId?: string) => { export const getInspectionsByProject = async (projectId: string, organizationId?: string) => {
const query = { projectId, ...(organizationId ? { organizationId } : {}) }; const filter: any = { project_id: projectId };
const inspections = await Inspection.find(query).sort({ date: -1 }).lean(); if (organizationId) {
return inspections.map(i => ({ ...i, id: i._id.toString() })); filter.organization_id = organizationId;
}
return await Inspection.find(filter);
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any export const getInspectionById = async (id: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any return await Inspection.findById(id);
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;
// Organization Check export const updateInspection = async (id: string, data: any) => {
if (organizationId && existing.organizationId && existing.organizationId !== organizationId) { return await Inspection.findByIdAndUpdate(id, data);
return null; };
}
// Role/Ownership check export const deleteInspection = async (id: string) => {
const isPowerUser = userRole === 'admin' || isDeveloper; return await Inspection.findByIdAndDelete(id);
if (!isPowerUser && existing.createdBy && existing.createdBy !== userId) { };
console.warn(`Permission Denied: User ${userId} tried to update inspection ${id} created by ${existing.createdBy}`);
return null;
}
const updateData = { export const getInspectionsByOrganization = async (organizationId: string) => {
...data, return await Inspection.find({ organization_id: organizationId });
date: data.date ? new Date(data.date) : undefined };
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) => { console.log('✅ InspectionService loaded with compatibility');
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() }));
};

View File

@@ -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 }) => { export const createPaintingScheme = async (data: any & { organizationId?: string }) => {
const newScheme = new PaintingScheme({ ...data, organizationId: data.organizationId }); return await PaintingScheme.create({ ...data, organization_id: data.organizationId });
const saved = await newScheme.save();
return { ...saved.toObject(), id: saved._id.toString() };
}; };
export const getPaintingSchemesByProject = async (projectId: string, organizationId?: string) => { export const getPaintingSchemesByProject = async (projectId: string, organizationId?: string) => {
const query = { projectId, ...(organizationId ? { organizationId } : {}) }; const filter: any = { project_id: projectId };
const schemes = await PaintingScheme.find(query).lean(); if (organizationId) {
return schemes.map(s => ({ ...s, id: s._id.toString() })); 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) => { 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); const existing = await PaintingScheme.findById(id);
if (!existing) return null; if (!existing) return null;
// Check ownership if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
if (organizationId && existing.organizationId && existing.organizationId !== organizationId) { return null;
// 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. return await PaintingScheme.findByIdAndUpdate(id, data);
// Ensure we "adopt" the record if it didn't have an orgId
if (organizationId && !data.organizationId) {
data.organizationId = organizationId;
}
const updated = await PaintingScheme.findOneAndUpdate({ _id: id }, data, { new: true }).lean();
if (updated) {
return { ...updated, id: updated._id.toString() };
}
return null;
}; };
export const deletePaintingScheme = async (id: string, organizationId?: string) => { export const deletePaintingScheme = async (id: string) => {
// Find first to check permissions return await PaintingScheme.findByIdAndDelete(id);
const existing = await PaintingScheme.findById(id);
if (!existing) return;
// Permissions:
// If user has org, and item has OTHER org, deny.
if (organizationId && existing.organizationId && existing.organizationId !== organizationId) {
console.warn(`[Delete PaintingScheme] Access Denied. User Org: ${organizationId}, Scheme Org: ${existing.organizationId}`);
return;
}
await PaintingScheme.findByIdAndDelete(id);
}; };
export const getAllSchemes = async (organizationId?: string) => { export const clonePaintingScheme = async (id: string, newData: any) => {
const query = organizationId ? { organizationId } : {}; const original = await PaintingScheme.findById(id);
const schemes = await PaintingScheme.find(query).lean(); if (!original) return null;
return schemes.map(s => ({ ...s, id: s._id.toString() }));
return await PaintingScheme.create({ ...original, ...newData, id: undefined });
}; };
console.log('✅ PaintingSchemeService loaded with compatibility');

View File

@@ -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 }) => { export const createPart = async (data: any & { organizationId?: string }) => {
const newPart = new Part({ ...data, organizationId: data.organizationId }); return await Part.create({ ...data, organization_id: data.organizationId });
const saved = await newPart.save();
return { ...saved.toObject(), id: saved._id.toString() };
}; };
export const getPartsByProject = async (projectId: string, organizationId?: string, isGlobalAdmin: boolean = false) => { export const getPartsByProject = async (projectId: string, organizationId?: string, isGlobalAdmin: boolean = false) => {
const query = isGlobalAdmin const filter: any = { project_id: projectId };
? { projectId } if (!isGlobalAdmin && organizationId) {
: { projectId, $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] }; filter.organization_id = organizationId;
const parts = await Part.find(query).lean(); }
return parts.map(p => ({ ...p, id: p._id.toString() })); 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) => { export const updatePart = async (id: string, data: any, organizationId?: string, isGlobalAdmin: boolean = false) => {
const existing = await Part.findById(id); const existing = await Part.findById(id);
if (!existing) return null; if (!existing) return null;
if (!isGlobalAdmin && organizationId && existing.organizationId && existing.organizationId !== organizationId) { if (!isGlobalAdmin && organizationId && existing.organization_id && existing.organization_id !== organizationId) {
console.warn(`Access Denied: Part ${id} belongs to ${existing.organizationId}, user is ${organizationId}`); console.warn(`Access Denied: Part ${id} belongs to ${existing.organization_id}, user is ${organizationId}`);
return null; return null;
} }
if (organizationId && !existing.organizationId) { return await Part.findByIdAndUpdate(id, data);
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;
}; };
export const deletePart = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => { export const deletePart = async (id: string) => {
const part = await Part.findById(id); return await Part.findByIdAndDelete(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 getAllParts = async (organizationId?: string, isGlobalAdmin: boolean = false) => { export const getPartsByOrganization = async (organizationId: string) => {
const query = isGlobalAdmin return await Part.find({ organization_id: organizationId });
? {}
: { $or: [{ organizationId }, { organizationId: { $exists: false } }, { organizationId: null }] };
const parts = await Part.find(query).lean();
return parts.map(p => ({ ...p, id: p._id.toString() }));
}; };
console.log('✅ PartService loaded with compatibility');

View File

@@ -1,8 +1,7 @@
import Project from '../models/Project.js'; import {
import Part from '../models/Part.js'; Project, Part, PaintingScheme, ApplicationRecord, Inspection,
import PaintingScheme from '../models/PaintingScheme.js'; supabase, findOneGpi, queryGpi
import ApplicationRecord from '../models/ApplicationRecord.js'; } from '../lib/compat.js';
import Inspection from '../models/Inspection.js';
interface ProjectData { interface ProjectData {
name: string; name: string;
@@ -15,207 +14,73 @@ interface ProjectData {
} }
export const createProject = async (data: ProjectData & { organizationId?: string }) => { export const createProject = async (data: ProjectData & { organizationId?: string }) => {
const newProject = new Project({ const project = await Project.create({
name: data.name, name: data.name,
client: data.client, client: data.client,
startDate: data.startDate ? new Date(data.startDate) : null, start_date: data.startDate ? new Date(data.startDate).toISOString() : null,
endDate: data.endDate ? new Date(data.endDate) : null, end_date: data.endDate ? new Date(data.endDate).toISOString() : null,
technician: data.technician, technician: data.technician,
environment: data.environment, environment: data.environment,
organizationId: data.organizationId, organization_id: data.organizationId,
weightKg: data.weightKg 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) => { export const getDashboardProjects = async (organizationId?: string) => {
const matchStage = organizationId ? { organizationId } : {}; const filter: any = organizationId ? { organization_id: organizationId } : {};
return await Project.find(filter);
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() }));
}; };
export const getAllProjects = async (organizationId?: string, isGlobalAdmin: boolean = false, status: string = 'active') => { export const archiveProject = async (id: string, organizationId?: string, isGlobalAdmin?: boolean) => {
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) => {
const project = await Project.findById(id); const project = await Project.findById(id);
if (!project) throw new Error('Projeto não encontrado'); if (!project) throw new Error('Projeto não encontrado');
// Check ownership if (!isGlobalAdmin && project.organization_id !== organizationId) {
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) { throw new Error('Acesso negado');
throw new Error('Sem permissão para arquivar este projeto');
} }
const newStatus = project.status === 'active' ? 'archived' : 'active'; return await Project.findByIdAndUpdate(id, { status: 'archived' });
const updated = await Project.findByIdAndUpdate(id, { status: newStatus }, { new: true }).lean();
return updated;
}; };
export const getProjectById = async (id: string, organizationId?: string, isGlobalAdmin: boolean = false) => { export const getProjectById = async (id: string) => {
const project = await Project.findById(id).lean(); 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 export const deleteProject = async (id: string) => {
if (!isGlobalAdmin && organizationId && project.organizationId && project.organizationId !== organizationId) { return await Project.findByIdAndDelete(id);
throw new Error('Acesso negado a este projeto'); };
}
const [parts, schemes, records, inspections] = await Promise.all([
Part.find({ projectId: id }).lean(),
PaintingScheme.find({ projectId: id }).populate('paintId thinnerId').lean(),
ApplicationRecord.find({ projectId: id }).lean(),
Inspection.find({ projectId: id })
.populate({
path: 'stockItemId',
select: 'batchNumber dataSheetId',
populate: { path: 'dataSheetId', select: 'name' }
})
.lean()
]);
export const 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 { return {
...project, project,
id: project._id.toString(), schemesCount: schemes.length,
// eslint-disable-next-line @typescript-eslint/no-explicit-any inspectionsCount: inspections.length,
parts: parts.map((p: any) => ({ ...p, id: p._id.toString() })), partsCount: parts.length
// 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() }))
}; };
}; };
export const updateProject = async (id: string, data: Partial<ProjectData>, organizationId?: string, isGlobalAdmin: boolean = false) => { console.log('✅ ProjectService loaded with compatibility layer');
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 })
]);
};