⚙️ Atualização local para GPI - Sync via Antigravity
This commit is contained in:
293
package-lock.json
generated
293
package-lock.json
generated
@@ -8,13 +8,14 @@
|
|||||||
"name": "gpi-app",
|
"name": "gpi-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/clerk-react": "^5.59.6",
|
|
||||||
"@clerk/localizations": "^3.35.3",
|
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/mongoose": "^5.11.96",
|
"@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",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -22,6 +23,8 @@
|
|||||||
"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",
|
||||||
|
"framer-motion": "^12.36.0",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"mongodb": "^7.0.0",
|
"mongodb": "^7.0.0",
|
||||||
"mongoose": "^9.1.5",
|
"mongoose": "^9.1.5",
|
||||||
@@ -1636,75 +1639,6 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@clerk/clerk-react": {
|
|
||||||
"version": "5.59.6",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@clerk/shared": "^3.43.2",
|
|
||||||
"tslib": "2.8.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.17.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^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": {
|
|
||||||
"version": "3.44.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.44.0.tgz",
|
|
||||||
"integrity": "sha512-kH+chNeZwqml3IDpWLgebWECfOZifyUQO4OISd/96w1EuCY1Bzw6cBq/ZbpsoO8jyG8/6bGr/MGXLhDzTrpPfA==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"csstype": "3.1.3",
|
|
||||||
"dequal": "2.0.3",
|
|
||||||
"glob-to-regexp": "0.4.1",
|
|
||||||
"js-cookie": "3.0.5",
|
|
||||||
"std-env": "^3.9.0",
|
|
||||||
"swr": "2.3.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.17.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^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"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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,
|
||||||
@@ -3403,6 +3337,12 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@babel/types": "^7.28.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/bcryptjs": {
|
||||||
|
"version": "2.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||||
|
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3509,6 +3449,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mongoose": {
|
"node_modules/@types/mongoose": {
|
||||||
"version": "5.11.96",
|
"version": "5.11.96",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3516,6 +3466,12 @@
|
|||||||
"mongoose": "*"
|
"mongoose": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/multer": {
|
"node_modules/@types/multer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3528,7 +3484,6 @@
|
|||||||
"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",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
@@ -5008,6 +4963,15 @@
|
|||||||
"baseline-browser-mapping": "dist/cli.js"
|
"baseline-browser-mapping": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5114,6 +5078,12 @@
|
|||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/buffer-from": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -5516,10 +5486,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
|
||||||
"version": "3.1.3",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/d3-array": {
|
"node_modules/d3-array": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@@ -5843,6 +5809,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/edge-runtime": {
|
"node_modules/edge-runtime": {
|
||||||
"version": "2.5.9",
|
"version": "2.5.9",
|
||||||
"resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz",
|
"resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz",
|
||||||
@@ -6691,6 +6666,33 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.36.0.tgz",
|
||||||
|
"integrity": "sha512-4PqYHAT7gev0ke0wos+PyrcFxI0HScjm3asgU8nSYa8YzJFuwgIvdj3/s3ZaxLq0bUSboIn19A2WS/MHwLCvfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.36.0",
|
||||||
|
"motion-utils": "^12.36.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -6902,10 +6904,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob-to-regexp": {
|
|
||||||
"version": "0.4.1",
|
|
||||||
"license": "BSD-2-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/glob/node_modules/minimatch": {
|
"node_modules/glob/node_modules/minimatch": {
|
||||||
"version": "10.1.1",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
|
||||||
@@ -7744,13 +7742,6 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-cookie": {
|
|
||||||
"version": "3.0.5",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
@@ -7844,6 +7835,61 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsonwebtoken/node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/kareem": {
|
"node_modules/kareem": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -8161,11 +8207,53 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.sortby": {
|
"node_modules/lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||||
@@ -8435,6 +8523,21 @@
|
|||||||
"url": "https://opencollective.com/mongoose"
|
"url": "https://opencollective.com/mongoose"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.36.0.tgz",
|
||||||
|
"integrity": "sha512-Ep1pq8P88rGJ75om8lTCA13zqd7ywPGwCqwuWwin6BKc0hMLkVfcS6qKlRqEo2+t0DwoUcgGJfXwaiFn4AOcQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.36.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.36.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz",
|
||||||
|
"integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mpath": {
|
"node_modules/mpath": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -10120,10 +10223,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/std-env": {
|
|
||||||
"version": "3.10.0",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/stop-iteration-iterator": {
|
"node_modules/stop-iteration-iterator": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||||
@@ -10333,17 +10432,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/swr": {
|
|
||||||
"version": "2.3.4",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"dequal": "^2.0.3",
|
|
||||||
"use-sync-external-store": "^1.4.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -10865,7 +10953,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": {
|
||||||
|
|||||||
@@ -13,13 +13,14 @@
|
|||||||
"start": "node dist/server/index.js"
|
"start": "node dist/server/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/clerk-react": "^5.59.6",
|
|
||||||
"@clerk/localizations": "^3.35.3",
|
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/mongoose": "^5.11.96",
|
"@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",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
"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",
|
||||||
|
"framer-motion": "^12.36.0",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"mongodb": "^7.0.0",
|
"mongodb": "^7.0.0",
|
||||||
"mongoose": "^9.1.5",
|
"mongoose": "^9.1.5",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import React from 'react';
|
||||||
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';
|
||||||
@@ -18,103 +18,107 @@ import { DeveloperDashboard } from './pages/DeveloperDashboard';
|
|||||||
import { CalculatorDashboard } from './pages/CalculatorDashboard';
|
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 Register from './pages/Register';
|
||||||
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 }) => {
|
||||||
const { isDeveloper, isLoading } = useAuth();
|
const { isDeveloper, isLoading } = useAuth();
|
||||||
|
|
||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
if (!isDeveloper()) return <Navigate to="/" replace />;
|
if (!isDeveloper()) return <Navigate to="/" replace />;
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppRoutes: React.FC = () => {
|
||||||
const { organization } = useOrganization();
|
const { isSignedIn, isLoading } = useAuth();
|
||||||
|
|
||||||
console.log('AppContent rendered');
|
if (isLoading) {
|
||||||
console.log('Current organization:', organization);
|
return (
|
||||||
|
<div className="min-h-screen bg-[#0f172a] flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// If user is signed in but has no organization, show org selector
|
if (!isSignedIn) {
|
||||||
if (!organization) {
|
return (
|
||||||
console.log('No organization - showing OrganizationSelector');
|
<Routes>
|
||||||
return <OrganizationSelector />;
|
<Route path="/login" element={<Login />} />
|
||||||
}
|
<Route path="/register" element={<Register />} />
|
||||||
|
<Route path="*" element={<Navigate to="/login" replace />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Organization exists - showing main app');
|
return (
|
||||||
return (
|
<Layout>
|
||||||
<ToastProvider>
|
<Routes>
|
||||||
<AuthProvider>
|
|
||||||
<SystemSettingsProvider>
|
|
||||||
<NotificationProvider>
|
|
||||||
<Layout>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<ProjectList />} />
|
<Route path="/" element={<ProjectList />} />
|
||||||
|
<Route path="/login" element={<Navigate to="/" replace />} />
|
||||||
|
<Route path="/register" element={<Navigate to="/" replace />} />
|
||||||
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
||||||
<Route path="/projects" element={<ProjectList />} />
|
<Route path="/projects" element={<ProjectList />} />
|
||||||
<Route path="/project/:id" element={<ProjectDetails />} />
|
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||||
<Route path="/schemes" element={<SchemesList />} />
|
<Route path="/schemes" element={<SchemesList />} />
|
||||||
<Route path="/inspections" element={<InspectionsList />} />
|
<Route path="/inspections" element={<InspectionsList />} />
|
||||||
<Route path="/library" element={
|
<Route path="/library" element={
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
<DataSheetLibrary />
|
<DataSheetLibrary />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/instruments" element={
|
<Route path="/instruments" element={
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
<InstrumentList />
|
<InstrumentList />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/yield-study" element={
|
<Route path="/yield-study" element={
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
<YieldStudyDashboard />
|
<YieldStudyDashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
} />
|
||||||
<Route path="/calculators" element={<CalculatorDashboard />} />
|
<Route path="/calculators" element={<CalculatorDashboard />} />
|
||||||
<Route
|
<Route
|
||||||
path="/admin"
|
path="/admin"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute allowedRoles={['admin']}>
|
<ProtectedRoute allowedRoles={['admin']}>
|
||||||
<AdminDashboard />
|
<AdminDashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/stock"
|
path="/stock"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
<StockDashboard />
|
<StockDashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/developer"
|
path="/developer"
|
||||||
element={
|
element={
|
||||||
<DeveloperRoute>
|
<DeveloperRoute>
|
||||||
<DeveloperDashboard />
|
<DeveloperDashboard />
|
||||||
</DeveloperRoute>
|
</DeveloperRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Routes>
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Layout>
|
</Routes>
|
||||||
</NotificationProvider>
|
</Layout>
|
||||||
</SystemSettingsProvider>
|
);
|
||||||
</AuthProvider>
|
|
||||||
</ToastProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<SignedOut>
|
<ToastProvider>
|
||||||
<Login />
|
<AuthProvider>
|
||||||
</SignedOut>
|
<SystemSettingsProvider>
|
||||||
<SignedIn>
|
<NotificationProvider>
|
||||||
<AppContent />
|
<AppRoutes />
|
||||||
</SignedIn>
|
</NotificationProvider>
|
||||||
|
</SystemSettingsProvider>
|
||||||
|
</AuthProvider>
|
||||||
|
</ToastProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ import React, { useState } from 'react';
|
|||||||
import NotificationBell from './NotificationBell';
|
import NotificationBell from './NotificationBell';
|
||||||
import { TeamPresence } from './TeamPresence';
|
import { TeamPresence } from './TeamPresence';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Menu, X, FolderOpen, Layers, ClipboardCheck, LogOut, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer } from 'lucide-react';
|
import { Menu, X, FolderOpen, Layers, ClipboardCheck, LogOut, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer, User as UserIcon } from 'lucide-react';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { useClerk, UserButton, useUser, OrganizationSwitcher, useOrganization } from '@clerk/clerk-react';
|
|
||||||
import { TechnicalManual } from './TechnicalManual';
|
import { TechnicalManual } from './TechnicalManual';
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
// import { useSystemSettings } from '../context/SystemSettingsContext';
|
// import { useSystemSettings } from '../context/SystemSettingsContext';
|
||||||
import { setApiOrgData } from '../services/api';
|
import { setApiOrganizationId } from '../services/api';
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -22,21 +21,17 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
});
|
});
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { signOut } = useClerk();
|
const { isAdmin, isUser, isDeveloper, appUser, logout } = useAuth();
|
||||||
const { user } = useUser();
|
|
||||||
const { organization } = useOrganization();
|
|
||||||
const { isAdmin, isUser, isDeveloper, appUser } = useAuth();
|
|
||||||
// const { settings } = useSystemSettings();
|
// const { settings } = useSystemSettings();
|
||||||
|
|
||||||
// Sync Organization ID with API client
|
// Sync Organization ID with API client
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (organization?.id) {
|
if (appUser?.organizationId) {
|
||||||
|
setApiOrganizationId(appUser.organizationId);
|
||||||
setApiOrgData(organization.id, organization.name);
|
|
||||||
} else {
|
} else {
|
||||||
setApiOrgData(null);
|
setApiOrganizationId(null);
|
||||||
}
|
}
|
||||||
}, [organization]);
|
}, [appUser?.organizationId]);
|
||||||
|
|
||||||
// Helper to get role display name
|
// Helper to get role display name
|
||||||
const getRoleDisplay = () => {
|
const getRoleDisplay = () => {
|
||||||
@@ -118,44 +113,15 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-6 mb-2">
|
<div className="px-6 mb-2">
|
||||||
{isAdmin() ? (
|
<div className="w-full flex items-center gap-3 p-2 rounded-xl border border-border/50 bg-surface-hover/50 text-text-main opacity-80 cursor-default" title="Apenas visualização">
|
||||||
<OrganizationSwitcher
|
<div className="w-8 h-8 rounded-lg bg-surface-soft flex items-center justify-center font-bold text-xs uppercase">
|
||||||
hidePersonal={true}
|
{appUser?.organizationId ? 'ORG' : 'GPI'}
|
||||||
afterSelectOrganizationUrl="/"
|
|
||||||
afterCreateOrganizationUrl="/"
|
|
||||||
afterLeaveOrganizationUrl="/"
|
|
||||||
appearance={{
|
|
||||||
elements: {
|
|
||||||
rootBox: "w-full",
|
|
||||||
organizationSwitcherTrigger: "w-full justify-between bg-surface-hover/50 hover:bg-surface-hover p-2 rounded-xl border border-border/50 text-text-main transition-all",
|
|
||||||
organizationPreviewTextContainer: "text-text-main",
|
|
||||||
organizationPreviewMainIdentifier: "text-text-main font-semibold",
|
|
||||||
organizationSwitcherPopoverCard: "bg-surface border border-border/40 shadow-2xl",
|
|
||||||
organizationSwitcherPopoverActions: "bg-surface-soft/50",
|
|
||||||
organizationSwitcherPopoverActionButton: "text-text-main hover:bg-surface-hover transition-colors",
|
|
||||||
organizationPreview: "hover:bg-surface-hover cursor-pointer transition-colors px-4 py-3",
|
|
||||||
organizationPreviewSecondaryIdentifier: "text-text-muted",
|
|
||||||
organizationSwitcherPopoverFooter: "hidden",
|
|
||||||
userPreviewMainIdentifier: "text-text-main font-bold",
|
|
||||||
userPreviewSecondaryIdentifier: "text-text-muted",
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex items-center gap-3 p-2 rounded-xl border border-border/50 bg-surface-hover/50 text-text-main opacity-80 cursor-default" title="Apenas visualização">
|
|
||||||
{organization?.imageUrl ? (
|
|
||||||
<img src={organization.imageUrl} alt={organization.name} className="w-8 h-8 rounded-lg object-cover bg-surface-soft" />
|
|
||||||
) : (
|
|
||||||
<div className="w-8 h-8 rounded-lg bg-surface-soft flex items-center justify-center font-bold text-xs">
|
|
||||||
{organization?.name?.substring(0, 2).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-semibold truncate">{organization?.name || 'Carregando...'}</p>
|
|
||||||
<p className="text-[10px] text-text-muted uppercase tracking-wider">Organização</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-semibold truncate">{appUser?.organizationId || 'Padrão'}</p>
|
||||||
|
<p className="text-[10px] text-text-muted uppercase tracking-wider">Organização</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Team Presence - Shows all members with online/offline status */}
|
{/* Team Presence - Shows all members with online/offline status */}
|
||||||
@@ -257,7 +223,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut()}
|
onClick={logout}
|
||||||
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-text-secondary hover:text-error hover:bg-error/5 transition-all w-full"
|
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-text-secondary hover:text-error hover:bg-error/5 transition-all w-full"
|
||||||
>
|
>
|
||||||
<LogOut size={18} />
|
<LogOut size={18} />
|
||||||
@@ -268,10 +234,12 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
<div className="w-px h-6 bg-border/50 mx-1"></div>
|
<div className="w-px h-6 bg-border/50 mx-1"></div>
|
||||||
<UserButton afterSignOutUrl="/" />
|
<div className="w-8 h-8 rounded-full bg-surface-soft flex items-center justify-center">
|
||||||
|
<UserIcon size={16} className="text-text-muted" />
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{user?.firstName || 'Usuário'}</span>
|
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{appUser?.name || 'Usuário'}</span>
|
||||||
<span className="text-[8px] text-text-muted">v2.1.0</span>
|
<span className="text-[8px] text-text-muted">v3.0.0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { usePresence } from '../hooks/usePresence';
|
import { usePresence } from '../hooks/usePresence';
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
import { useOrganization } from '@clerk/clerk-react';
|
|
||||||
import { SendMessageModal } from './SendMessageModal';
|
import { SendMessageModal } from './SendMessageModal';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
import { clsx } from 'clsx';
|
||||||
|
|
||||||
interface OrganizationMember {
|
interface OrganizationMember {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
clerkUserId: string;
|
|
||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,81 +24,58 @@ interface PendingMessage {
|
|||||||
|
|
||||||
export const TeamPresence: React.FC = () => {
|
export const TeamPresence: React.FC = () => {
|
||||||
const { activeUsers } = usePresence();
|
const { activeUsers } = usePresence();
|
||||||
const { appUser } = useAuth();
|
const { appUser, isSignedIn } = useAuth();
|
||||||
const { organization } = useOrganization();
|
|
||||||
const [allMembers, setAllMembers] = React.useState<OrganizationMember[]>([]);
|
const [allMembers, setAllMembers] = React.useState<OrganizationMember[]>([]);
|
||||||
const [pendingMessages, setPendingMessages] = React.useState<PendingMessage[]>([]);
|
const [pendingMessages, setPendingMessages] = React.useState<PendingMessage[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = React.useState<{ id: string; name: string } | null>(null);
|
const [selectedUser, setSelectedUser] = React.useState<{ id: string; name: string } | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||||
|
|
||||||
console.log('TeamPresence rendered');
|
const fetchMembers = React.useCallback(async () => {
|
||||||
console.log('appUser:', appUser);
|
try {
|
||||||
console.log('organization:', organization);
|
const response = await api.get<any[]>('/users');
|
||||||
console.log('activeUsers:', activeUsers);
|
setAllMembers(response.data.map(m => ({ ...m, id: m._id || m.id })));
|
||||||
console.log('allMembers:', allMembers);
|
} catch (error) {
|
||||||
|
console.error('Error fetching members:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPendingMessages = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get<PendingMessage[]>('/messages/pending');
|
||||||
|
setPendingMessages(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching pending messages:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fetch all organization members
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchMembers = async () => {
|
if (isSignedIn) {
|
||||||
console.log('Fetching members...');
|
|
||||||
try {
|
|
||||||
const response = await api.get<OrganizationMember[]>('/users');
|
|
||||||
console.log('Members fetched:', response.data);
|
|
||||||
setAllMembers(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching members:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (organization?.id) {
|
|
||||||
console.log('Organization ID exists, fetching members');
|
|
||||||
fetchMembers();
|
fetchMembers();
|
||||||
// Refresh every minute
|
|
||||||
const interval = setInterval(fetchMembers, 60000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
} else {
|
|
||||||
console.log('No organization ID, skipping fetch');
|
|
||||||
}
|
|
||||||
}, [organization?.id]);
|
|
||||||
|
|
||||||
// Fetch pending messages
|
|
||||||
React.useEffect(() => {
|
|
||||||
const fetchPendingMessages = async () => {
|
|
||||||
try {
|
|
||||||
const response = await api.get<PendingMessage[]>('/messages/pending');
|
|
||||||
setPendingMessages(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching pending messages:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (organization?.id) {
|
|
||||||
fetchPendingMessages();
|
fetchPendingMessages();
|
||||||
const interval = setInterval(fetchPendingMessages, 30000);
|
const intervalMembers = setInterval(fetchMembers, 60000);
|
||||||
return () => clearInterval(interval);
|
const intervalMessages = setInterval(fetchPendingMessages, 30000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(intervalMembers);
|
||||||
|
clearInterval(intervalMessages);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [organization?.id]);
|
}, [isSignedIn, fetchMembers, fetchPendingMessages]);
|
||||||
|
|
||||||
console.log('Rendering with allMembers.length:', allMembers.length);
|
if (allMembers.length === 0) return null;
|
||||||
|
|
||||||
if (allMembers.length === 0) {
|
// Create a Set of active user emails or IDs (depends on what usePresence returns)
|
||||||
console.log('No members, returning null');
|
// Assuming usePresence returns objects with email or id
|
||||||
return null;
|
const activeUserIdentifiers = new Set(activeUsers.map(u => u.email || u.id));
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Set of active user IDs for fast lookup
|
|
||||||
const activeUserIds = new Set(activeUsers.map(u => u.clerkId));
|
|
||||||
|
|
||||||
// Create a map of pending messages by recipient ID
|
|
||||||
const pendingMessagesByRecipient = new Map(
|
const pendingMessagesByRecipient = new Map(
|
||||||
pendingMessages.map(msg => [msg.toUser?.email, msg])
|
pendingMessages.map(msg => [msg.toUser?.email, msg])
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMemberClick = (member: OrganizationMember) => {
|
const handleMemberClick = (member: OrganizationMember) => {
|
||||||
if (member.clerkUserId === appUser?.clerkId) {
|
if (member.email === appUser?.email) {
|
||||||
return; // Don't allow messaging yourself
|
return;
|
||||||
}
|
}
|
||||||
setSelectedUser({ id: member.clerkUserId, name: member.name });
|
setSelectedUser({ id: member.id, name: member.name });
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,14 +84,8 @@ export const TeamPresence: React.FC = () => {
|
|||||||
setSelectedUser(null);
|
setSelectedUser(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMessageSent = async () => {
|
const handleMessageSent = () => {
|
||||||
// Refresh pending messages
|
fetchPendingMessages();
|
||||||
try {
|
|
||||||
const response = await api.get<PendingMessage[]>('/messages/pending');
|
|
||||||
setPendingMessages(response.data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing pending messages:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExistingMessage = (member: OrganizationMember) => {
|
const getExistingMessage = (member: OrganizationMember) => {
|
||||||
@@ -132,48 +103,45 @@ export const TeamPresence: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{allMembers.map((member) => {
|
{allMembers.map((member) => {
|
||||||
const isOnline = activeUserIds.has(member.clerkUserId);
|
const isOnline = activeUserIdentifiers.has(member.email) || activeUserIdentifiers.has(member.id);
|
||||||
const isCurrentUser = member.clerkUserId === appUser?.clerkId;
|
const isCurrentUser = member.email === appUser?.email;
|
||||||
const hasPendingMessage = pendingMessagesByRecipient.has(member.email);
|
const hasPendingMessage = pendingMessagesByRecipient.has(member.email);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={member._id}
|
key={member.id}
|
||||||
className="relative group"
|
className="relative group"
|
||||||
onClick={() => !isCurrentUser && handleMemberClick(member)}
|
onClick={() => !isCurrentUser && handleMemberClick(member)}
|
||||||
>
|
>
|
||||||
<div className={`
|
<div className={clsx(`
|
||||||
w-8 h-8 rounded-full border-2 text-xs font-bold flex items-center justify-center uppercase shadow-sm
|
w-8 h-8 rounded-full border-2 text-xs font-bold flex items-center justify-center uppercase shadow-sm
|
||||||
transition-all duration-300
|
transition-all duration-300
|
||||||
${isOnline
|
`,
|
||||||
|
isOnline
|
||||||
? 'bg-primary/20 border-primary text-primary ring-2 ring-primary/20 shadow-primary/20'
|
? 'bg-primary/20 border-primary text-primary ring-2 ring-primary/20 shadow-primary/20'
|
||||||
: 'bg-surface-soft border-border/30 text-text-muted/40 grayscale opacity-40'
|
: 'bg-surface-soft border-border/30 text-text-muted/40 grayscale opacity-40',
|
||||||
}
|
isCurrentUser ? 'ring-2 ring-amber-500 cursor-default' : 'cursor-pointer hover:scale-110',
|
||||||
${isCurrentUser ? 'ring-2 ring-amber-500 cursor-default' : 'cursor-pointer hover:scale-110'}
|
hasPendingMessage && 'ring-2 ring-blue-500'
|
||||||
${hasPendingMessage ? 'ring-2 ring-blue-500' : ''}
|
)}>
|
||||||
`}>
|
{member.name?.charAt(0) || member.email.charAt(0)}
|
||||||
{member.name.charAt(0)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Online indicator */}
|
|
||||||
{isOnline && (
|
{isOnline && (
|
||||||
<span className="absolute bottom-0 right-0 block h-2.5 w-2.5 rounded-full bg-green-500 ring-2 ring-surface animate-pulse" />
|
<span className="absolute bottom-0 right-0 block h-2.5 w-2.5 rounded-full bg-green-500 ring-2 ring-surface animate-pulse" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pending message indicator */}
|
|
||||||
{hasPendingMessage && !isCurrentUser && (
|
{hasPendingMessage && !isCurrentUser && (
|
||||||
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-blue-500 ring-2 ring-surface" title="Mensagem pendente" />
|
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-blue-500 ring-2 ring-surface" title="Mensagem pendente" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tooltip */}
|
|
||||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1.5 bg-surface border border-border/40 shadow-xl rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
|
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1.5 bg-surface border border-border/40 shadow-xl rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-50">
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
<div className="font-bold text-text-main flex items-center gap-2">
|
<div className="font-bold text-text-main flex items-center gap-2">
|
||||||
{member.name}
|
{member.name || 'Sem nome'}
|
||||||
{isCurrentUser && <span className="text-amber-500 text-[10px]">(Você)</span>}
|
{isCurrentUser && <span className="text-amber-500 text-[10px]">(Você)</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-text-muted text-[10px] mt-0.5">{member.email}</div>
|
<div className="text-text-muted text-[10px] mt-0.5">{member.email}</div>
|
||||||
<div className={`text-[10px] mt-1 font-semibold ${isOnline ? 'text-green-500' : 'text-text-muted'}`}>
|
<div className={clsx("text-[10px] mt-1 font-semibold", isOnline ? 'text-green-500' : 'text-text-muted')}>
|
||||||
{isOnline ? '🟢 Online' : '⚫ Offline'}
|
{isOnline ? '🟢 Online' : '⚫ Offline'}
|
||||||
</div>
|
</div>
|
||||||
{!isCurrentUser && (
|
{!isCurrentUser && (
|
||||||
@@ -181,11 +149,6 @@ export const TeamPresence: React.FC = () => {
|
|||||||
💬 Clique para enviar mensagem
|
💬 Clique para enviar mensagem
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{hasPendingMessage && (
|
|
||||||
<div className="text-blue-400 text-[10px] mt-1 font-semibold">
|
|
||||||
✉️ Mensagem pendente
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,14 +157,13 @@ export const TeamPresence: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Message Modal */}
|
|
||||||
{selectedUser && (
|
{selectedUser && (
|
||||||
<SendMessageModal
|
<SendMessageModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
recipientId={selectedUser.id}
|
recipientId={selectedUser.id}
|
||||||
recipientName={selectedUser.name}
|
recipientName={selectedUser.name}
|
||||||
existingMessage={getExistingMessage(allMembers.find(m => m.clerkUserId === selectedUser.id)!)}
|
existingMessage={getExistingMessage(allMembers.find(m => m.id === selectedUser.id)!)}
|
||||||
onMessageSent={handleMessageSent}
|
onMessageSent={handleMessageSent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
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 api, { getBaseUrl, setApiOrganizationId } from '../services/api';
|
||||||
|
|
||||||
const API_URL = getBaseUrl();
|
const API_URL = getBaseUrl();
|
||||||
|
|
||||||
@@ -11,119 +10,57 @@ interface AuthProviderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 }>({});
|
|
||||||
|
|
||||||
// Set the clerk user ID and organization ID for the API interceptor
|
const logout = useCallback(() => {
|
||||||
useEffect(() => {
|
localStorage.removeItem('gpi_token');
|
||||||
setApiClerkUserId(user?.id || null);
|
setAppUser(null);
|
||||||
setApiOrganizationId(organization?.id || null);
|
setApiOrganizationId(null);
|
||||||
}, [user?.id, organization?.id]);
|
window.location.href = '/login';
|
||||||
|
}, []);
|
||||||
|
|
||||||
const syncUser = useCallback(async () => {
|
const fetchMe = useCallback(async () => {
|
||||||
if (!user) {
|
const token = localStorage.getItem('gpi_token');
|
||||||
|
if (!token) {
|
||||||
setAppUser(null);
|
setAppUser(null);
|
||||||
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 response = await api.get('/auth/me');
|
||||||
const isSameContext =
|
const userData = response.data;
|
||||||
lastContextRef.current.clerkId === user.id &&
|
|
||||||
lastContextRef.current.orgId === (organization?.id || null);
|
|
||||||
|
|
||||||
if (!isSameContext) {
|
|
||||||
setIsLoading(true);
|
|
||||||
}
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
// Sync user with backend, including organization context
|
|
||||||
const response = await fetch(`${API_URL}/users/sync`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'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) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (response.status === 403 && data.error?.includes('bloqueada')) {
|
|
||||||
setError('Sua conta foi bloqueada. Entre em contato com o administrador.');
|
|
||||||
setAppUser(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('Falha ao sincronizar usuário');
|
|
||||||
}
|
|
||||||
|
|
||||||
const syncedUser = await response.json();
|
|
||||||
// Use organizationRole if available (per-org role), otherwise fall back to global role
|
|
||||||
const effectiveRole = syncedUser.organizationRole || syncedUser.role || 'guest';
|
|
||||||
setAppUser({
|
setAppUser({
|
||||||
...syncedUser,
|
...userData,
|
||||||
id: syncedUser._id || syncedUser.id,
|
id: userData._id || userData.id
|
||||||
role: effectiveRole, // Override with organization-specific role
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update last context ref
|
if (userData.organizationId) {
|
||||||
lastContextRef.current = { clerkId: user.id, orgId: organization?.id || null };
|
setApiOrganizationId(userData.organizationId);
|
||||||
} catch (err) {
|
}
|
||||||
console.error('Error syncing user:', err);
|
} catch (err: any) {
|
||||||
setError('Erro ao carregar dados do usuário');
|
console.error('Error fetching current user:', err);
|
||||||
|
if (err.response?.status === 401) {
|
||||||
|
logout();
|
||||||
|
} else {
|
||||||
|
setError('Erro ao carregar dados do usuário');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, organization?.id, membership?.role]);
|
}, [logout]);
|
||||||
|
|
||||||
const refetchUser = useCallback(async () => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}/users/me`, {
|
|
||||||
headers: {
|
|
||||||
'x-clerk-user-id': user.id,
|
|
||||||
...(organization?.id && { 'x-organization-id': organization.id }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const userData = await response.json();
|
|
||||||
const effectiveRole = userData.organizationRole || userData.role || 'guest';
|
|
||||||
setAppUser({
|
|
||||||
...userData,
|
|
||||||
id: userData._id || userData.id,
|
|
||||||
role: effectiveRole,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error refetching user:', err);
|
|
||||||
}
|
|
||||||
}, [user, organization?.id]);
|
|
||||||
|
|
||||||
// Re-sync when organization changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoaded && user) {
|
fetchMe();
|
||||||
syncUser();
|
}, [fetchMe]);
|
||||||
}
|
|
||||||
}, [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,14 +72,15 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||||||
value={{
|
value={{
|
||||||
appUser,
|
appUser,
|
||||||
isLoading,
|
isLoading,
|
||||||
isSignedIn: !!user,
|
isSignedIn: !!appUser,
|
||||||
error,
|
error,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isUser,
|
isUser,
|
||||||
isGuest,
|
isGuest,
|
||||||
isDeveloper,
|
isDeveloper,
|
||||||
canEdit,
|
canEdit,
|
||||||
refetchUser,
|
refetchUser: fetchMe,
|
||||||
|
logout, // Added logout to context
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface AuthContextType {
|
|||||||
isDeveloper: () => boolean;
|
isDeveloper: () => boolean;
|
||||||
canEdit: () => boolean;
|
canEdit: () => boolean;
|
||||||
refetchUser: () => Promise<void>;
|
refetchUser: () => Promise<void>;
|
||||||
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useAuth } from '@clerk/clerk-react';
|
import { useAuth } from '../context/useAuth';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
import type { INotification } from '../types';
|
import type { INotification } from '../types';
|
||||||
import { NotificationContext } from './NotificationContextState';
|
import { NotificationContext } from './NotificationContextState';
|
||||||
|
|
||||||
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const { orgId, isSignedIn } = useAuth();
|
const { appUser, isSignedIn } = useAuth();
|
||||||
const [notifications, setNotifications] = useState<INotification[]>([]);
|
const [notifications, setNotifications] = useState<INotification[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const fetchNotifications = useCallback(async () => {
|
const fetchNotifications = useCallback(async () => {
|
||||||
if (!orgId || !isSignedIn) return;
|
if (!isSignedIn) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (notifications.length === 0) setLoading(true);
|
if (notifications.length === 0) setLoading(true);
|
||||||
@@ -21,7 +21,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [orgId, isSignedIn, notifications.length]);
|
}, [isSignedIn, notifications.length]);
|
||||||
|
|
||||||
const markAsRead = async (id: string) => {
|
const markAsRead = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -70,7 +70,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
|
|
||||||
// Polling effect
|
// Polling effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSignedIn && orgId) {
|
if (isSignedIn) {
|
||||||
fetchNotifications(); // Initial fetch
|
fetchNotifications(); // Initial fetch
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@@ -81,7 +81,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
} else {
|
} else {
|
||||||
setNotifications([]);
|
setNotifications([]);
|
||||||
}
|
}
|
||||||
}, [isSignedIn, orgId, fetchNotifications]);
|
}, [isSignedIn, fetchNotifications]);
|
||||||
|
|
||||||
const unreadCount = notifications.filter(n => !n.isRead).length;
|
const unreadCount = notifications.filter(n => !n.isRead).length;
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { useAuth } from '../context/useAuth';
|
|||||||
|
|
||||||
export interface ActiveUser {
|
export interface ActiveUser {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
clerkId: string;
|
|
||||||
lastSeenAt: string;
|
lastSeenAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,7 @@
|
|||||||
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
|
|
||||||
|
|
||||||
if (!PUBLISHABLE_KEY) {
|
|
||||||
throw new Error("Missing Publishable Key")
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<ClerkProvider
|
|
||||||
publishableKey={PUBLISHABLE_KEY}
|
|
||||||
afterSignOutUrl="/"
|
|
||||||
localization={ptBR}
|
|
||||||
appearance={{
|
|
||||||
variables: {
|
|
||||||
colorPrimary: '#fb923c', // Cor primária do GPI (Laranja)
|
|
||||||
colorBackground: '#ffffff',
|
|
||||||
colorText: '#1c1917',
|
|
||||||
colorTextSecondary: '#57534e',
|
|
||||||
borderRadius: '0.75rem',
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
card: "shadow-none border-0 bg-transparent", // Deixamos o container da página controlar o card
|
|
||||||
navbar: "hidden",
|
|
||||||
headerTitle: "text-2xl font-bold tracking-tight",
|
|
||||||
headerSubtitle: "text-text-muted font-medium",
|
|
||||||
formButtonPrimary: "bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20",
|
|
||||||
socialButtonsBlockButton: "bg-white hover:bg-surface-hover border-border/40 text-text-main font-semibold transition-all duration-300 rounded-xl",
|
|
||||||
footerActionLink: "text-primary hover:text-primary/80 font-bold",
|
|
||||||
formFieldInput: "bg-surface-soft border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl",
|
|
||||||
organizationSwitcherTrigger: "hover:bg-surface-hover transition-colors rounded-xl",
|
|
||||||
organizationPreviewMainIdentifier: "font-bold",
|
|
||||||
// Personalização específica para a lista de organizações que aparece na imagem
|
|
||||||
organizationListPreview: "hover:bg-surface-soft rounded-xl transition-all p-3",
|
|
||||||
organizationListCreateOrganizationButton: "text-primary font-bold hover:text-primary/80",
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<App />
|
<App />
|
||||||
</ClerkProvider>,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { useUser, useOrganization } from '@clerk/clerk-react';
|
import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Info, Image as ImageIcon, Box, Database } from 'lucide-react';
|
||||||
import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database } from 'lucide-react';
|
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import type { AppUser, UserRole } from '../types';
|
import type { AppUser, UserRole } from '../types';
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
@@ -15,200 +14,65 @@ const roleLabels: Record<UserRole, { label: string; color: string; icon: React.R
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AdminDashboard: React.FC = () => {
|
export const AdminDashboard: React.FC = () => {
|
||||||
const { user } = useUser();
|
const { isAdmin, appUser } = useAuth();
|
||||||
const { organization } = useOrganization();
|
|
||||||
const { isAdmin } = useAuth();
|
|
||||||
const [users, setUsers] = useState<AppUser[]>([]);
|
const [users, setUsers] = useState<AppUser[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [filterRole, setFilterRole] = useState<UserRole | 'all'>('all');
|
const [filterRole, setFilterRole] = useState<UserRole | 'all'>('all');
|
||||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState<'users' | 'organization' | 'settings' | 'stock' | 'backup'>('users');
|
const [activeTab, setActiveTab] = useState<'users' | 'organization' | 'settings' | 'stock' | 'backup'>('users');
|
||||||
const [logoLoading, setLogoLoading] = useState(false);
|
|
||||||
|
|
||||||
const fetchUsers = useCallback(async () => {
|
const fetchUsers = useCallback(async () => {
|
||||||
if (!user || !organization?.id) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await api.get('/users');
|
const response = await api.get('/users');
|
||||||
setUsers(response.data.map((u: AppUser) => ({ ...u, id: u._id || u.id })));
|
setUsers(response.data.map((u: any) => ({ ...u, id: u._id || u.id })));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching users:', error);
|
console.error('Error fetching users:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, organization?.id]);
|
}, []);
|
||||||
|
|
||||||
const syncOrganizationMembers = useCallback(async () => {
|
|
||||||
if (!organization) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
// Fetch ALL members from Clerk (handle pagination)
|
|
||||||
console.log('Fetching members from Clerk organization:', organization.id);
|
|
||||||
let allMembers: any[] = [];
|
|
||||||
let hasMore = true;
|
|
||||||
|
|
||||||
// Fetch all pages
|
|
||||||
while (hasMore) {
|
|
||||||
const clerkMembers = await organization.getMemberships();
|
|
||||||
console.log(`Fetched members:`, clerkMembers.data.length);
|
|
||||||
|
|
||||||
if (clerkMembers.data.length === 0) {
|
|
||||||
hasMore = false;
|
|
||||||
} else {
|
|
||||||
allMembers = clerkMembers.data;
|
|
||||||
hasMore = false; // Clerk retorna todos de uma vez normalmente
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Total Clerk members fetched:', allMembers.length, allMembers);
|
|
||||||
|
|
||||||
// Get current users from database
|
|
||||||
const currentUsersResponse = await api.get('/users');
|
|
||||||
const currentUsers = currentUsersResponse.data;
|
|
||||||
console.log('Current users in database:', currentUsers.length, currentUsers);
|
|
||||||
|
|
||||||
// Create a Set of Clerk user IDs for fast lookup
|
|
||||||
const clerkUserIds = new Set(
|
|
||||||
allMembers
|
|
||||||
.map(m => m.publicUserData?.userId)
|
|
||||||
.filter(id => id != null)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('Clerk user IDs:', Array.from(clerkUserIds));
|
|
||||||
|
|
||||||
// Step 1: Add/Update users from Clerk
|
|
||||||
for (const membership of allMembers) {
|
|
||||||
const clerkUser = membership.publicUserData;
|
|
||||||
console.log('Processing membership:', membership);
|
|
||||||
console.log('Public user data:', clerkUser);
|
|
||||||
|
|
||||||
if (clerkUser) {
|
|
||||||
const syncData = {
|
|
||||||
clerkId: clerkUser.userId,
|
|
||||||
email: clerkUser.identifier || '',
|
|
||||||
name: `${clerkUser.firstName || ''} ${clerkUser.lastName || ''}`.trim() || clerkUser.identifier || 'Usuário',
|
|
||||||
organizationId: organization.id,
|
|
||||||
clerkRole: membership.role
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Syncing user:', syncData);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await api.post('/users/sync', syncData);
|
|
||||||
console.log('Sync success for', clerkUser.userId, ':', response.data);
|
|
||||||
} catch (syncError) {
|
|
||||||
console.error('Error syncing member:', clerkUser.userId, syncError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Remove users from database that don't exist in Clerk anymore
|
|
||||||
let removedCount = 0;
|
|
||||||
for (const dbUser of currentUsers) {
|
|
||||||
const clerkUserId = dbUser.clerkUserId || dbUser.clerkId;
|
|
||||||
if (!clerkUserIds.has(clerkUserId)) {
|
|
||||||
console.log(`User ${dbUser.name} (${clerkUserId}) is in DB but not in Clerk - removing...`);
|
|
||||||
try {
|
|
||||||
await api.delete(`/users/${dbUser._id}`);
|
|
||||||
console.log(`Removed user ${dbUser.name} from database`);
|
|
||||||
removedCount++;
|
|
||||||
} catch (deleteError) {
|
|
||||||
console.error(`Error removing user ${dbUser.name}:`, deleteError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload users after sync
|
|
||||||
console.log('Reloading users from database...');
|
|
||||||
await fetchUsers();
|
|
||||||
|
|
||||||
const message = `Sincronização concluída!\n✅ ${allMembers.length} membros atualizados\n${removedCount > 0 ? `🗑️ ${removedCount} membros removidos` : ''}`;
|
|
||||||
alert(message);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error syncing organization members:', error);
|
|
||||||
alert('Erro ao sincronizar membros. Verifique o console para mais detalhes.');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}, [organization, fetchUsers]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
}, [fetchUsers]);
|
}, [fetchUsers]);
|
||||||
|
|
||||||
const handleRoleChange = async (userId: string, newRole: UserRole) => {
|
const handleRoleChange = async (userId: string, newRole: UserRole) => {
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
setActionLoading(userId);
|
setActionLoading(userId);
|
||||||
try {
|
try {
|
||||||
const response = await api.patch(`/users/${userId}/role`, { role: newRole });
|
const response = await api.patch(`/users/${userId}/role`, { role: newRole });
|
||||||
const updated = response.data;
|
const updated = response.data;
|
||||||
setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u));
|
setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u));
|
||||||
} catch (error: unknown) {
|
} catch (error: any) {
|
||||||
const err = error as { response?: { data?: { error?: string } } };
|
|
||||||
console.error('Error updating role:', error);
|
console.error('Error updating role:', error);
|
||||||
alert(err.response?.data?.error || 'Erro ao atualizar role');
|
alert(error.response?.data?.error || 'Erro ao atualizar role');
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(null);
|
setActionLoading(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleBan = async (userId: string, isBanned: boolean) => {
|
const handleToggleBan = async (userId: string, isBanned: boolean) => {
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
setActionLoading(userId);
|
setActionLoading(userId);
|
||||||
try {
|
try {
|
||||||
const response = await api.patch(`/users/${userId}/ban`, { isBanned });
|
const response = await api.patch(`/users/${userId}/ban`, { isBanned });
|
||||||
const updated = response.data;
|
const updated = response.data;
|
||||||
setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u));
|
setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u));
|
||||||
} catch (error: unknown) {
|
} catch (error: any) {
|
||||||
const err = error as { response?: { data?: { error?: string } } };
|
|
||||||
console.error('Error toggling ban:', error);
|
console.error('Error toggling ban:', error);
|
||||||
alert(err.response?.data?.error || 'Erro ao alterar status');
|
alert(error.response?.data?.error || 'Erro ao alterar status');
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(null);
|
setActionLoading(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredUsers = users.filter(u => {
|
const filteredUsers = users.filter(u => {
|
||||||
const matchesSearch = u.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
const matchesSearch = (u.name || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
u.email.toLowerCase().includes(searchTerm.toLowerCase());
|
(u.email || '').toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
const matchesRole = filterRole === 'all' || u.role === filterRole;
|
const matchesRole = filterRole === 'all' || u.role === filterRole;
|
||||||
return matchesSearch && matchesRole;
|
return matchesSearch && matchesRole;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file || !organization) return;
|
|
||||||
|
|
||||||
// Validations
|
|
||||||
const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml'];
|
|
||||||
if (!validTypes.includes(file.type)) {
|
|
||||||
alert('Por favor, selecione uma imagem PNG, JPG ou SVG.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.size > 500 * 1024) {
|
|
||||||
alert('O arquivo deve ter no máximo 500KB.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLogoLoading(true);
|
|
||||||
try {
|
|
||||||
await organization.setLogo({ file });
|
|
||||||
alert('Logo atualizado com sucesso!');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading logo:', error);
|
|
||||||
alert('Erro ao atualizar o logo.');
|
|
||||||
} finally {
|
|
||||||
setLogoLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isAdmin()) {
|
if (!isAdmin()) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
|
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-4">
|
||||||
@@ -230,22 +94,14 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
Administração
|
Administração
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-text-muted mt-2">Configurações globais e gerenciamento de usuários</p>
|
<p className="text-text-muted mt-2">Gestão de usuários e configurações do sistema</p>
|
||||||
</div>
|
</div>
|
||||||
{activeTab === 'users' && (
|
{activeTab === 'users' && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
|
||||||
onClick={syncOrganizationMembers}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="flex items-center gap-2 px-4 py-2.5 bg-primary hover:bg-primary-dark text-white border border-primary-dark rounded-xl font-semibold transition-all disabled:opacity-50"
|
|
||||||
>
|
|
||||||
<Users size={18} className={isLoading ? 'animate-spin' : ''} />
|
|
||||||
Sincronizar Clerk
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={fetchUsers}
|
onClick={fetchUsers}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="flex items-center gap-2 px-4 py-2.5 bg-surface hover:bg-surface-hover border border-border/40 rounded-xl text-text-main font-semibold transition-all disabled:opacity-50"
|
className="flex items-center gap-2 px-4 py-2.5 bg-primary hover:bg-primary-dark text-white border border-primary-dark rounded-xl font-semibold transition-all disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
|
<RefreshCw size={18} className={isLoading ? 'animate-spin' : ''} />
|
||||||
Atualizar
|
Atualizar
|
||||||
@@ -268,18 +124,6 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
Usuários
|
Usuários
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('organization')}
|
|
||||||
className={clsx(
|
|
||||||
"flex items-center gap-2 px-6 py-2.5 text-sm font-bold rounded-lg transition-all",
|
|
||||||
activeTab === 'organization'
|
|
||||||
? "bg-surface text-primary shadow-sm border border-border/40"
|
|
||||||
: "text-text-muted hover:text-text-main hover:bg-surface-hover"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Upload size={16} />
|
|
||||||
Organização
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('settings')}
|
onClick={() => setActiveTab('settings')}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -407,18 +251,18 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
<tbody className="divide-y divide-border/40">
|
<tbody className="divide-y divide-border/40">
|
||||||
{filteredUsers.map((u) => {
|
{filteredUsers.map((u) => {
|
||||||
const roleInfo = roleLabels[u.role];
|
const roleInfo = roleLabels[u.role];
|
||||||
const isCurrentUser = u.clerkId === user?.id;
|
const isCurrentUser = u.email === appUser?.email;
|
||||||
const isActionDisabled = actionLoading === u.id;
|
const isActionDisabled = actionLoading === u.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={u.id} className={`hover:bg-surface-hover transition-colors ${u.isBanned ? 'opacity-60' : ''}`}>
|
<tr key={u.id} className={clsx("hover:bg-surface-hover transition-colors", u.isBanned && "opacity-60")}>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-bold">
|
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-bold">
|
||||||
{u.name.charAt(0).toUpperCase()}
|
{(u.name || u.email || '?').charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-text-main">{u.name}</p>
|
<p className="font-semibold text-text-main">{u.name || 'Sem nome'}</p>
|
||||||
{isCurrentUser && (
|
{isCurrentUser && (
|
||||||
<span className="text-xs text-primary font-medium">(Você)</span>
|
<span className="text-xs text-primary font-medium">(Você)</span>
|
||||||
)}
|
)}
|
||||||
@@ -432,7 +276,7 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
onChange={(e) => handleRoleChange(u.id, e.target.value as UserRole)}
|
onChange={(e) => handleRoleChange(u.id, e.target.value as UserRole)}
|
||||||
disabled={isCurrentUser || isActionDisabled || u.isBanned}
|
disabled={isCurrentUser || isActionDisabled || u.isBanned}
|
||||||
aria-label={`Alterar role de ${u.name}`}
|
aria-label={`Alterar role de ${u.name}`}
|
||||||
className={`px-3 py-1.5 rounded-lg border text-sm font-semibold transition-all ${roleInfo.color} disabled:opacity-50 disabled:cursor-not-allowed bg-transparent`}
|
className={clsx("px-3 py-1.5 rounded-lg border text-sm font-semibold transition-all bg-transparent disabled:opacity-50 disabled:cursor-not-allowed", roleInfo.color)}
|
||||||
>
|
>
|
||||||
<option value="guest">Convidado</option>
|
<option value="guest">Convidado</option>
|
||||||
<option value="user">Usuário</option>
|
<option value="user">Usuário</option>
|
||||||
@@ -457,10 +301,11 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleToggleBan(u.id, !u.isBanned)}
|
onClick={() => handleToggleBan(u.id, !u.isBanned)}
|
||||||
disabled={isActionDisabled}
|
disabled={isActionDisabled}
|
||||||
className={`px-4 py-2 rounded-xl text-sm font-semibold transition-all ${u.isBanned
|
className={clsx("px-4 py-2 rounded-xl text-sm font-semibold transition-all shadow-sm active:scale-95 disabled:opacity-50",
|
||||||
? 'bg-green-500/20 text-green-400 hover:bg-green-500/30'
|
u.isBanned
|
||||||
: 'bg-red-500/20 text-red-400 hover:bg-red-500/30'
|
? 'bg-green-500/10 text-green-500 hover:bg-green-500/20 border border-green-500/20'
|
||||||
} disabled:opacity-50`}
|
: 'bg-red-500/10 text-red-500 hover:bg-red-500/20 border border-red-500/20'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isActionDisabled ? (
|
{isActionDisabled ? (
|
||||||
<RefreshCw size={16} className="animate-spin" />
|
<RefreshCw size={16} className="animate-spin" />
|
||||||
@@ -481,132 +326,20 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : activeTab === 'organization' ? (
|
|
||||||
<div className="space-y-6 animate-in fade-in slide-in-from-left-4 duration-300">
|
|
||||||
{/* Organization Settings Panel */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
||||||
<div className="bg-surface rounded-2xl p-6 border border-border/40 space-y-6">
|
|
||||||
<div className="flex items-center gap-3 pb-4 border-b border-border/20">
|
|
||||||
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
|
||||||
<Upload size={20} className="text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-bold text-text-main">Identidade Visual</h2>
|
|
||||||
<p className="text-xs text-text-muted">Gerencie o logo da sua organização</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-6 py-4">
|
|
||||||
{organization?.imageUrl ? (
|
|
||||||
<div className="relative group">
|
|
||||||
<div className="w-32 h-32 rounded-2xl border-2 border-primary/20 p-2 bg-white overflow-hidden shadow-xl">
|
|
||||||
<img
|
|
||||||
src={organization.imageUrl}
|
|
||||||
alt={organization.name}
|
|
||||||
className="w-full h-full object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="absolute -bottom-2 -right-2 bg-primary text-white p-2 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<ImageIcon size={14} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-32 h-32 rounded-2xl border-2 border-dashed border-border/40 flex flex-col items-center justify-center bg-surface-soft text-text-muted gap-2">
|
|
||||||
<ImageIcon size={32} className="opacity-20" />
|
|
||||||
<span className="text-[10px] font-bold uppercase tracking-widest">Sem Logo</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="w-full space-y-4">
|
|
||||||
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-border/40 rounded-2xl cursor-pointer hover:bg-surface-hover hover:border-primary/50 transition-all group">
|
|
||||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
||||||
<Upload className="w-8 h-8 text-text-muted group-hover:text-primary transition-colors mb-2" />
|
|
||||||
<p className="text-sm text-text-main font-bold">Clique para alterar o logo</p>
|
|
||||||
<p className="text-xs text-text-muted">ou arraste e solte o arquivo</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
accept="image/png,image/jpeg,image/jpg,image/svg+xml"
|
|
||||||
onChange={handleLogoUpload}
|
|
||||||
disabled={logoLoading}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{logoLoading && (
|
|
||||||
<div className="flex items-center justify-center gap-2 text-primary font-bold animate-pulse">
|
|
||||||
<RefreshCw size={16} className="animate-spin" />
|
|
||||||
<span>Enviando logo...</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-surface rounded-2xl p-6 border border-border/40 space-y-6">
|
|
||||||
<div className="flex items-center gap-3 pb-4 border-b border-border/20">
|
|
||||||
<div className="w-10 h-10 rounded-xl bg-amber-500/10 flex items-center justify-center">
|
|
||||||
<Info size={20} className="text-amber-500" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-bold text-text-main">Requisitos & Dicas</h2>
|
|
||||||
<p className="text-xs text-text-muted">Regras para um visual impecável</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
|
|
||||||
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
|
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-primary"></div>
|
|
||||||
Formatos Suportados
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-text-muted leading-relaxed">
|
|
||||||
Aceitamos arquivos nos formatos <strong>PNG, JPG ou SVG</strong>. O formato SVG é recomendado para máxima nitidez em qualquer tamanho.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
|
|
||||||
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
|
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div>
|
|
||||||
Dimensões Recomendadas
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-text-muted leading-relaxed">
|
|
||||||
Recomendamos uma imagem quadrada de no mínimo <strong>512x512 pixels</strong>. Logos horizontais podem não aparecer corretamente em todas as áreas.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-4 bg-surface-soft rounded-xl border border-border/20">
|
|
||||||
<h3 className="text-sm font-bold text-text-main flex items-center gap-2 mb-2">
|
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-red-500"></div>
|
|
||||||
Limite de Tamanho
|
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-text-muted leading-relaxed">
|
|
||||||
O arquivo não deve ultrapassar <strong>500 KB</strong>. Arquivos maiores serão rejeitados automaticamente para garantir rapidez no carregamento.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : activeTab === 'settings' ? (
|
) : activeTab === 'settings' ? (
|
||||||
<GeometrySettings />
|
<GeometrySettings />
|
||||||
) : activeTab === 'backup' ? (
|
) : activeTab === 'backup' ? (
|
||||||
<BackupRestore />
|
<BackupRestore />
|
||||||
) : (
|
) : (
|
||||||
// Lazily load or direct render StockDashboard (Need to import it)
|
|
||||||
<div className="bg-surface rounded-2xl border border-border/40 p-6">
|
<div className="bg-surface rounded-2xl border border-border/40 p-6">
|
||||||
<div className="text-center py-10">
|
<div className="text-center py-10">
|
||||||
<h2 className="text-xl font-bold text-text-main">Gestão de Estoque</h2>
|
<h2 className="text-xl font-bold text-text-main">Em breve</h2>
|
||||||
<p className="text-text-muted mt-2">Acesse a nova página dedicada ao controle de estoque.</p>
|
<p className="text-text-muted mt-2">Novas configurações serão adicionadas aqui.</p>
|
||||||
<a
|
|
||||||
href="/stock"
|
|
||||||
className="mt-6 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
|
||||||
>
|
|
||||||
Ir para Estoque
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default AdminDashboard;
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ export const DeveloperDashboard: React.FC = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{admins.map(admin => (
|
{admins.map(admin => (
|
||||||
<div key={admin.clerkUserId} className="flex items-center gap-3 p-2 bg-surface rounded-lg border border-indigo-500/20">
|
<div key={admin.id || admin.email} className="flex items-center gap-3 p-2 bg-surface rounded-lg border border-indigo-500/20">
|
||||||
<div className="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center text-indigo-500 font-bold text-xs">
|
<div className="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center text-indigo-500 font-bold text-xs">
|
||||||
{admin.name.charAt(0).toUpperCase()}
|
{admin.name.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
@@ -441,7 +441,7 @@ export const DeveloperDashboard: React.FC = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{commonUsers.map(user => (
|
{commonUsers.map(user => (
|
||||||
<div key={user.clerkUserId} className="flex items-center gap-3 p-2 bg-surface rounded-lg border border-border/40">
|
<div key={user.id || user.email} className="flex items-center gap-3 p-2 bg-surface rounded-lg border border-border/40">
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs",
|
"w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs",
|
||||||
user.role === 'user' ? "bg-green-500/10 text-green-500" : "bg-gray-500/10 text-gray-500"
|
user.role === 'user' ? "bg-green-500/10 text-green-500" : "bg-gray-500/10 text-gray-500"
|
||||||
|
|||||||
@@ -1,37 +1,116 @@
|
|||||||
import { SignIn } from "@clerk/clerk-react";
|
import React, { useState } from 'react';
|
||||||
import { Hammer } from "lucide-react";
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import api from '../services/api';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const Login: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { refetchUser } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post('/auth/login', { email, password });
|
||||||
|
const { token } = response.data;
|
||||||
|
|
||||||
|
localStorage.setItem('gpi_token', token);
|
||||||
|
await refetchUser();
|
||||||
|
navigate('/');
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.error || 'Erro ao realizar login. Verifique suas credenciais.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const Login = () => {
|
|
||||||
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 bg-[#0f172a] flex items-center justify-center p-4">
|
||||||
{/* Background decorative elements */}
|
<motion.div
|
||||||
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-primary/10 rounded-full blur-[120px] animate-pulse" />
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-primary/5 rounded-full blur-[120px]" />
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="max-w-md w-full"
|
||||||
<div className="relative z-10 w-full max-w-md px-6 flex flex-col items-center">
|
>
|
||||||
{/* Logo Area */}
|
<div className="bg-slate-800/50 backdrop-blur-xl border border-slate-700/50 p-8 rounded-2xl shadow-2xl">
|
||||||
<div className="mb-8 flex flex-col items-center text-center">
|
<div className="text-center mb-8">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-primary flex items-center justify-center text-white font-bold text-3xl shadow-2xl shadow-primary/40 mb-4 animate-in zoom-in duration-700">
|
<h1 className="text-3xl font-bold text-white mb-2">GPI</h1>
|
||||||
G
|
<p className="text-slate-400">Gestão de Pintura Industrial</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/50 text-red-500 p-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
||||||
|
E-mail
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full bg-slate-900/50 border border-slate-700 text-white rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all"
|
||||||
|
placeholder="exemplo@gmail.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
||||||
|
Senha
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full bg-slate-900/50 border border-slate-700 text-white rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all"
|
||||||
|
placeholder="••••••••"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold py-3 rounded-lg shadow-lg shadow-blue-500/20 transition-all active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Entrando...
|
||||||
|
</span>
|
||||||
|
) : 'Entrar'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center text-sm">
|
||||||
|
<p className="text-slate-500">
|
||||||
|
Não tem uma conta? <Link to="/register" className="text-blue-400 hover:text-blue-300 font-medium">Contate o administrador</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-text-main tracking-tight mb-1">GPI</h1>
|
|
||||||
<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 */}
|
<div className="mt-8 text-center">
|
||||||
<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">
|
<p className="text-slate-600 text-xs">
|
||||||
<SignIn
|
© 2026 GPI - Sistema de Gestão Industrial
|
||||||
afterSignInUrl="/"
|
</p>
|
||||||
afterSignUpUrl="/"
|
|
||||||
forceRedirectUrl="/"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">
|
|
||||||
<Hammer size={14} />
|
|
||||||
<span>© 2026 GPI - Eficiência Industrial</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
|
|||||||
118
src/client/pages/Register.tsx
Normal file
118
src/client/pages/Register.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate, Link } from 'react-router-dom';
|
||||||
|
import api from '../services/api';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
|
const Register: React.FC = () => {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { refetchUser } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post('/auth/register', { email, password, name });
|
||||||
|
const { token } = response.data;
|
||||||
|
|
||||||
|
localStorage.setItem('gpi_token', token);
|
||||||
|
await refetchUser();
|
||||||
|
navigate('/');
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.response?.data?.error || 'Erro ao realizar cadastro.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#0f172a] flex items-center justify-center p-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="max-w-md w-full"
|
||||||
|
>
|
||||||
|
<div className="bg-slate-800/50 backdrop-blur-xl border border-slate-700/50 p-8 rounded-2xl shadow-2xl">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">GPI</h1>
|
||||||
|
<p className="text-slate-400">Novo Cadastro de Usuário</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-500/10 border border-red-500/50 text-red-500 p-3 rounded-lg text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
||||||
|
Nome Completo
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full bg-slate-900/50 border border-slate-700 text-white rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all"
|
||||||
|
placeholder="Seu nome"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
||||||
|
E-mail
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full bg-slate-900/50 border border-slate-700 text-white rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all"
|
||||||
|
placeholder="exemplo@gmail.com"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-slate-300 mb-1.5">
|
||||||
|
Senha
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="w-full bg-slate-900/50 border border-slate-700 text-white rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all"
|
||||||
|
placeholder="Mínimo 6 caracteres"
|
||||||
|
minLength={6}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold py-3 rounded-lg shadow-lg shadow-emerald-500/20 transition-all active:scale-[0.98]"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Cadastrando...' : 'Criar Conta'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-8 text-center text-sm">
|
||||||
|
<p className="text-slate-500">
|
||||||
|
Já tem uma conta? <Link to="/login" className="text-blue-400 hover:text-blue-300 font-medium">Voltar para o login</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Register;
|
||||||
@@ -41,23 +41,17 @@ export const setApiOrgId = (orgId: string | null) => {
|
|||||||
// Alias for consistency
|
// Alias for consistency
|
||||||
export const setApiOrganizationId = setApiOrgId;
|
export const setApiOrganizationId = setApiOrgId;
|
||||||
|
|
||||||
// Request interceptor to add clerk user ID and Org ID headers
|
// Request interceptor to add JWT token
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, {
|
const token = localStorage.getItem('gpi_token');
|
||||||
clerkId: currentClerkUserId,
|
if (token) {
|
||||||
orgId: currentOrgId
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
});
|
|
||||||
if (currentClerkUserId) {
|
|
||||||
config.headers['x-clerk-user-id'] = currentClerkUserId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentOrgId) {
|
if (currentOrgId) {
|
||||||
config.headers['x-organization-id'] = currentOrgId;
|
config.headers['x-organization-id'] = currentOrgId;
|
||||||
}
|
}
|
||||||
if (currentOrgName) {
|
|
||||||
// Encode to handle special characters
|
|
||||||
config.headers['x-organization-name'] = encodeURIComponent(currentOrgName);
|
|
||||||
}
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
@@ -65,12 +59,18 @@ api.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Response interceptor to handle 403 errors (guest access denied)
|
// Response interceptor to handle 401 (Unauthorized) and 403 errors
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// Token expired or invalid
|
||||||
|
localStorage.removeItem('gpi_token');
|
||||||
|
if (!window.location.pathname.includes('/login')) {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
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();
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const systemSettingsService = {
|
|||||||
|
|
||||||
export interface GlobalUser {
|
export interface GlobalUser {
|
||||||
_id: string;
|
_id: string;
|
||||||
clerkId: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
@@ -66,10 +66,10 @@ export interface GlobalOrganization {
|
|||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
name?: string; // Added
|
name?: string; // Added
|
||||||
members: {
|
members: {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: 'admin' | 'user' | 'guest';
|
role: 'admin' | 'user' | 'guest';
|
||||||
clerkUserId: string;
|
|
||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,29 +16,24 @@ import stockRoutes from './routes/stockRoutes.js';
|
|||||||
import notificationRoutes from './routes/notificationRoutes.js';
|
import notificationRoutes from './routes/notificationRoutes.js';
|
||||||
import instrumentRoutes from './routes/instrumentRoutes.js';
|
import instrumentRoutes from './routes/instrumentRoutes.js';
|
||||||
import messageRoutes from './routes/messageRoutes.js';
|
import messageRoutes from './routes/messageRoutes.js';
|
||||||
|
import authRoutes from './routes/authRoutes.js';
|
||||||
import backupRoutes from './routes/backupRoutes.js';
|
import backupRoutes from './routes/backupRoutes.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { authenticateJWT } from './middleware/authMiddleware.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: '*', // Be more specific in production
|
origin: '*', // Be more specific in production
|
||||||
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']
|
||||||
}));
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
import { extractUser } from './middleware/roleMiddleware.js';
|
// JWT Authentication Middleware
|
||||||
|
app.use(authenticateJWT);
|
||||||
// LOG DE DEPURAÇÃO PARA CONEXÃO
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ClerkID: ${req.headers['x-clerk-user-id'] || 'None'}`);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(extractUser);
|
|
||||||
|
|
||||||
// Static Uploads
|
// Static Uploads
|
||||||
import fs from 'fs';
|
|
||||||
const uploadsPath = path.join(process.cwd(), 'uploads');
|
const uploadsPath = path.join(process.cwd(), 'uploads');
|
||||||
|
|
||||||
// Ensure uploads directory exists
|
// Ensure uploads directory exists
|
||||||
@@ -49,6 +44,7 @@ if (!fs.existsSync(uploadsPath)) {
|
|||||||
app.use('/uploads', express.static(uploadsPath));
|
app.use('/uploads', express.static(uploadsPath));
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/users', userRoutes);
|
app.use('/api/users', userRoutes);
|
||||||
app.use('/api/projects', projectRoutes);
|
app.use('/api/projects', projectRoutes);
|
||||||
app.use('/api/parts', partRoutes);
|
app.use('/api/parts', partRoutes);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as appRecordService from '../services/applicationRecordService.js';
|
import * as appRecordService from '../services/applicationRecordService.js';
|
||||||
import '../middleware/roleMiddleware.js'; // Ensure type augmentation
|
import { AuthRequest } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
export const createApplicationRecord = async (req: Request, res: Response) => {
|
export const createApplicationRecord = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const createdBy = req.appUser?.clerkId;
|
const createdBy = req.appUser?._id?.toString();
|
||||||
const record = await appRecordService.createApplicationRecord({ ...req.body, organizationId, createdBy });
|
const record = await appRecordService.createApplicationRecord({ ...req.body, organizationId, createdBy });
|
||||||
res.status(201).json(record);
|
res.status(201).json(record);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -14,7 +14,7 @@ export const createApplicationRecord = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApplicationRecordsByProject = async (req: Request, res: Response) => {
|
export const getApplicationRecordsByProject = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
@@ -26,10 +26,10 @@ export const getApplicationRecordsByProject = async (req: Request, res: Response
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateApplicationRecord = async (req: Request, res: Response) => {
|
export const updateApplicationRecord = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
@@ -49,10 +49,10 @@ export const updateApplicationRecord = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteApplicationRecord = async (req: Request, res: Response) => {
|
export const deleteApplicationRecord = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
|
|||||||
105
src/server/controllers/authController.ts
Normal file
105
src/server/controllers/authController.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import User from '../models/User.js';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||||
|
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
|
||||||
|
|
||||||
|
export const login = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
return res.status(400).json({ error: 'E-mail e senha são obrigatórios' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({ email }).select('+password');
|
||||||
|
|
||||||
|
if (!user || !user.password) {
|
||||||
|
return res.status(401).json({ error: 'Credenciais inválidas' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(password, user.password);
|
||||||
|
|
||||||
|
if (!isMatch) {
|
||||||
|
return res.status(401).json({ error: 'Credenciais inválidas' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isBanned) {
|
||||||
|
return res.status(403).json({ error: 'Sua conta está bloqueada' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: user._id, email: user.email, role: user.role },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: JWT_EXPIRES_IN }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove password from user object
|
||||||
|
const userObj = user.toObject();
|
||||||
|
delete userObj.password;
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
token,
|
||||||
|
user: userObj
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
res.status(500).json({ error: 'Erro interno no servidor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const register = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { email, password, name, organizationId } = req.body;
|
||||||
|
|
||||||
|
if (!email || !password || !name) {
|
||||||
|
return res.status(400).json({ error: 'Campos obrigatórios ausentes' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await User.findOne({ email });
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({ error: 'E-mail já cadastrado' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 12);
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
email,
|
||||||
|
password: hashedPassword,
|
||||||
|
name,
|
||||||
|
organizationId,
|
||||||
|
role: 'user' // Default role
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ id: user._id, email: user.email, role: user.role },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: JWT_EXPIRES_IN }
|
||||||
|
);
|
||||||
|
|
||||||
|
const userObj = user.toObject();
|
||||||
|
delete userObj.password;
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
token,
|
||||||
|
user: userObj
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
res.status(500).json({ error: 'Erro ao criar usuário' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMe = async (req: any, res: Response) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(req.user.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'Usuário não encontrado' });
|
||||||
|
}
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: 'Erro ao obter dados do usuário' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,7 +6,7 @@ import '../middleware/roleMiddleware.js'; // Ensure type augmentation
|
|||||||
export const createInspection = async (req: Request, res: Response) => {
|
export const createInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const createdBy = req.appUser?.clerkId;
|
const createdBy = req.appUser?._id?.toString();
|
||||||
const inspection = await inspectionService.createInspection({
|
const inspection = await inspectionService.createInspection({
|
||||||
...req.body,
|
...req.body,
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -46,7 +46,7 @@ export const getInspectionsByProject = async (req: Request, res: Response) => {
|
|||||||
export const updateInspection = async (req: Request, res: Response) => {
|
export const updateInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export const updateInspection = async (req: Request, res: Response) => {
|
|||||||
export const deleteInspection = async (req: Request, res: Response) => {
|
export const deleteInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export const getAllInspections = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadPhoto = async (req: Request, res: Response) => {
|
export const uploadPhoto = async (req: Request & { file?: any }, res: Response) => {
|
||||||
try {
|
try {
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({ error: 'No file uploaded' });
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import Message from '../models/Message.js';
|
import Message from '../models/Message.js';
|
||||||
import OrganizationMember from '../models/OrganizationMember.js';
|
import OrganizationMember from '../models/OrganizationMember.js';
|
||||||
|
import { AuthRequest } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
export const sendMessage = async (req: Request, res: Response) => {
|
export const sendMessage = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { toUserId, message } = req.body;
|
const { toUserId, message } = req.body;
|
||||||
const fromUserId = req.appUser?.clerkId;
|
const fromUserId = req.appUser?._id?.toString();
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
@@ -58,9 +59,9 @@ export const sendMessage = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get unread messages for current user
|
// Get unread messages for current user
|
||||||
export const getUnreadMessages = async (req: Request, res: Response) => {
|
export const getUnreadMessages = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const toUserId = req.appUser?.clerkId;
|
const toUserId = req.appUser?._id?.toString();
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
@@ -82,7 +83,7 @@ export const getUnreadMessages = async (req: Request, res: Response) => {
|
|||||||
// Populate sender info
|
// Populate sender info
|
||||||
const messagesWithSender = await Promise.all(
|
const messagesWithSender = await Promise.all(
|
||||||
messages.map(async (msg) => {
|
messages.map(async (msg) => {
|
||||||
const sender = await OrganizationMember.findOne({ clerkUserId: msg.fromUserId });
|
const sender = await OrganizationMember.findOne({ userId: msg.fromUserId });
|
||||||
return {
|
return {
|
||||||
...msg.toObject(),
|
...msg.toObject(),
|
||||||
fromUser: sender ? { name: sender.name, email: sender.email } : null,
|
fromUser: sender ? { name: sender.name, email: sender.email } : null,
|
||||||
@@ -98,10 +99,10 @@ export const getUnreadMessages = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mark message as read
|
// Mark message as read
|
||||||
export const markMessageAsRead = async (req: Request, res: Response) => {
|
export const markMessageAsRead = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
@@ -134,9 +135,9 @@ export const markMessageAsRead = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get my pending (unread) sent messages
|
// Get my pending (unread) sent messages
|
||||||
export const getMyPendingMessages = async (req: Request, res: Response) => {
|
export const getMyPendingMessages = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const fromUserId = req.appUser?.clerkId;
|
const fromUserId = req.appUser?._id?.toString();
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
@@ -156,7 +157,7 @@ export const getMyPendingMessages = async (req: Request, res: Response) => {
|
|||||||
// Populate recipient info
|
// Populate recipient info
|
||||||
const messagesWithRecipient = await Promise.all(
|
const messagesWithRecipient = await Promise.all(
|
||||||
messages.map(async (msg) => {
|
messages.map(async (msg) => {
|
||||||
const recipient = await OrganizationMember.findOne({ clerkUserId: msg.toUserId });
|
const recipient = await OrganizationMember.findOne({ userId: msg.toUserId });
|
||||||
return {
|
return {
|
||||||
...msg.toObject(),
|
...msg.toObject(),
|
||||||
toUser: recipient ? { name: recipient.name, email: recipient.email } : null,
|
toUser: recipient ? { name: recipient.name, email: recipient.email } : null,
|
||||||
@@ -172,10 +173,10 @@ export const getMyPendingMessages = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Delete a message (only if unread and sender is the current user)
|
// Delete a message (only if unread and sender is the current user)
|
||||||
export const deleteMessage = async (req: Request, res: Response) => {
|
export const deleteMessage = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
@@ -206,10 +207,10 @@ export const deleteMessage = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Recipient deletes/archives a message
|
// Recipient deletes/archives a message
|
||||||
export const archiveMessage = async (req: Request, res: Response) => {
|
export const archiveMessage = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
|
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
|
||||||
@@ -225,10 +226,10 @@ export const archiveMessage = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const recipientDeleteMessage = async (req: Request, res: Response) => {
|
export const recipientDeleteMessage = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.clerkId;
|
const userId = req.appUser?._id?.toString();
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
|
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { notificationService } from '../services/notificationService.js';
|
import { notificationService } from '../services/notificationService.js';
|
||||||
|
import { AuthRequest } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
export const notificationController = {
|
export const notificationController = {
|
||||||
getUserNotifications: async (req: Request, res: Response) => {
|
getUserNotifications: 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 userId = req.headers['x-user-id'] as string; // Assumindo que temos o ID do usuário (clerkId ou email)
|
const userId = req.appUser?._id?.toString() || '';
|
||||||
|
|
||||||
// Se não tiver userId no header (ainda não implementado auth full), tentar pegar do query ou usar um fallback
|
|
||||||
// Nota: Idealmente o middleware de auth popula req.user. Vamos assumir que passamos x-user-id no frontend por enquanto.
|
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return res.status(400).json({ error: 'Organization ID is required' });
|
return res.status(400).json({ error: 'Organization ID is required' });
|
||||||
@@ -26,7 +24,7 @@ export const notificationController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
markAsRead: async (req: Request, res: Response) => {
|
markAsRead: async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const notification = await notificationService.markAsRead(id as string);
|
const notification = await notificationService.markAsRead(id as string);
|
||||||
@@ -37,10 +35,10 @@ export const notificationController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
markAllAsRead: async (req: Request, res: Response) => {
|
markAllAsRead: 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 userId = req.headers['x-user-id'] as string;
|
const userId = req.appUser?._id?.toString() || '';
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return res.status(400).json({ error: 'Organization ID is required' });
|
return res.status(400).json({ error: 'Organization ID is required' });
|
||||||
@@ -54,10 +52,10 @@ export const notificationController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
clearAll: async (req: Request, res: Response) => {
|
clearAll: 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 userId = req.headers['x-user-id'] as string;
|
const userId = req.appUser?._id?.toString() || '';
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return res.status(400).json({ error: 'Organization ID is required' });
|
return res.status(400).json({ error: 'Organization ID is required' });
|
||||||
@@ -71,10 +69,10 @@ export const notificationController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
archive: async (req: Request, res: Response) => {
|
archive: async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.headers['x-user-id'] as string;
|
const userId = req.appUser?._id?.toString() || '';
|
||||||
const notification = await notificationService.archive(id as string, userId);
|
const notification = await notificationService.archive(id as string, userId);
|
||||||
res.json(notification);
|
res.json(notification);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -83,10 +81,10 @@ export const notificationController = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (req: Request, res: Response) => {
|
delete: async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.headers['x-user-id'] as string;
|
const userId = req.appUser?._id?.toString() || '';
|
||||||
await notificationService.softDelete(id as string, userId);
|
await notificationService.softDelete(id as string, userId);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const createStockItem = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
const newItem = new StockItem({
|
const newItem = new StockItem({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?._id,
|
||||||
dataSheetId,
|
dataSheetId,
|
||||||
rrNumber,
|
rrNumber,
|
||||||
batchNumber,
|
batchNumber,
|
||||||
@@ -86,7 +86,7 @@ export const createStockItem = async (req: AuthRequest, res: Response) => {
|
|||||||
// Create Initial Movement (ENTRY)
|
// Create Initial Movement (ENTRY)
|
||||||
await StockMovement.create({
|
await StockMovement.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?._id,
|
||||||
stockItemId: savedItem._id,
|
stockItemId: savedItem._id,
|
||||||
movementNumber: 1,
|
movementNumber: 1,
|
||||||
type: 'ENTRY',
|
type: 'ENTRY',
|
||||||
@@ -195,7 +195,7 @@ export const adjustStock = async (req: AuthRequest, res: Response) => {
|
|||||||
// Register Movement
|
// Register Movement
|
||||||
await StockMovement.create({
|
await StockMovement.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?._id,
|
||||||
stockItemId: item._id,
|
stockItemId: item._id,
|
||||||
movementNumber,
|
movementNumber,
|
||||||
type: 'ADJUSTMENT',
|
type: 'ADJUSTMENT',
|
||||||
@@ -241,7 +241,7 @@ export const consumeStock = async (req: AuthRequest, res: Response) => {
|
|||||||
// Register Movement (Negative quantity for consumption)
|
// Register Movement (Negative quantity for consumption)
|
||||||
await StockMovement.create({
|
await StockMovement.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy: req.appUser?.clerkId,
|
createdBy: req.appUser?._id,
|
||||||
stockItemId: item._id,
|
stockItemId: item._id,
|
||||||
movementNumber,
|
movementNumber,
|
||||||
type: 'CONSUMPTION',
|
type: 'CONSUMPTION',
|
||||||
@@ -348,7 +348,7 @@ export const updateStockMovement = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params; // Movement ID
|
const { id } = req.params; // Movement ID
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
||||||
const userId = req.appUser?.clerkId || 'system';
|
const userId = req.appUser?._id || 'system';
|
||||||
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
|
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
@@ -431,7 +431,7 @@ export const deleteStockMovement = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
||||||
const userId = req.appUser?.clerkId || 'system';
|
const userId = req.appUser?._id || 'system';
|
||||||
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
|
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import SystemSettings from '../models/SystemSettings.js';
|
import SystemSettings from '../models/SystemSettings.js';
|
||||||
import User from '../models/User.js';
|
import User from '../models/User.js';
|
||||||
import OrganizationMember from '../models/OrganizationMember.js';
|
import OrganizationMember from '../models/OrganizationMember.js';
|
||||||
@@ -6,8 +6,9 @@ import Organization from '../models/Organization.js';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
import { AuthRequest } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
export const getSettings = async (req: Request, res: Response) => {
|
export const getSettings = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
let settings = await SystemSettings.findOne({ settingsId: 'global' });
|
let settings = await SystemSettings.findOne({ settingsId: 'global' });
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export const getSettings = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateSettings = async (req: Request, res: Response) => {
|
export const updateSettings = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { appName, appSubtitle, appLogoUrl } = req.body;
|
const { appName, appSubtitle, appLogoUrl } = req.body;
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ export const updateSettings = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const serveLogo = async (req: Request, res: Response) => {
|
export const serveLogo = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { filename } = req.params as { filename: string };
|
const { filename } = req.params as { filename: string };
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ export const serveLogo = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadLogo = async (req: Request, res: Response) => {
|
export const uploadLogo = async (req: AuthRequest & { file?: any }, res: Response) => {
|
||||||
try {
|
try {
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
|
return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
|
||||||
@@ -92,7 +93,7 @@ export const uploadLogo = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Global Admin Functions
|
// Global Admin Functions
|
||||||
export const getGlobalUsers = async (req: Request, res: Response) => {
|
export const getGlobalUsers = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const users = await User.find({}).sort({ createdAt: -1 });
|
const users = await User.find({}).sort({ createdAt: -1 });
|
||||||
res.json(users);
|
res.json(users);
|
||||||
@@ -102,7 +103,7 @@ export const getGlobalUsers = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
export const getGlobalOrganizations = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Aggregate members to group by org and get full member lists
|
// Aggregate members to group by org and get full member lists
|
||||||
const organizations = await OrganizationMember.aggregate([
|
const organizations = await OrganizationMember.aggregate([
|
||||||
@@ -111,10 +112,10 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
|||||||
_id: '$organizationId',
|
_id: '$organizationId',
|
||||||
members: {
|
members: {
|
||||||
$push: {
|
$push: {
|
||||||
|
id: '$userId',
|
||||||
name: '$name',
|
name: '$name',
|
||||||
email: '$email',
|
email: '$email',
|
||||||
role: '$role',
|
role: '$role',
|
||||||
clerkUserId: '$clerkUserId',
|
|
||||||
isBanned: '$isBanned'
|
isBanned: '$isBanned'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -125,7 +126,7 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
|||||||
$lookup: {
|
$lookup: {
|
||||||
from: 'organizations', // Ensure this matches the collection name of Organization model
|
from: 'organizations', // Ensure this matches the collection name of Organization model
|
||||||
localField: '_id',
|
localField: '_id',
|
||||||
foreignField: 'clerkId',
|
foreignField: 'organizationId', // We should rename clerkId in Organization model too
|
||||||
as: 'orgDetails'
|
as: 'orgDetails'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -155,7 +156,7 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleOrganizationBan = async (req: Request, res: Response) => {
|
export const toggleOrganizationBan = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { organizationId, isBanned } = req.body;
|
const { organizationId, isBanned } = req.body;
|
||||||
|
|
||||||
@@ -165,7 +166,7 @@ export const toggleOrganizationBan = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// Upsert the Organization record
|
// Upsert the Organization record
|
||||||
const org = await Organization.findOneAndUpdate(
|
const org = await Organization.findOneAndUpdate(
|
||||||
{ clerkId: organizationId },
|
{ organizationId: organizationId },
|
||||||
{ isBanned: isBanned },
|
{ isBanned: isBanned },
|
||||||
{ new: true, upsert: true }
|
{ new: true, upsert: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,101 +1,9 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import User, { IUser } from '../models/User.js';
|
import User, { IUser } from '../models/User.js';
|
||||||
import OrganizationMember, { OrgRole } from '../models/OrganizationMember.js';
|
import OrganizationMember, { OrgRole } from '../models/OrganizationMember.js';
|
||||||
|
import { AuthRequest } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
// Define locally to avoid import cycle risks
|
// Get current user data with organization context
|
||||||
interface IAppUser extends IUser {
|
|
||||||
organizationId?: string;
|
|
||||||
organizationRole?: OrgRole;
|
|
||||||
organizationBanned?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
|
||||||
appUser?: IAppUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync user from Clerk to MongoDB
|
|
||||||
* Creates user if doesn't exist, updates if exists
|
|
||||||
* Also creates/updates OrganizationMember for the current organization
|
|
||||||
*/
|
|
||||||
export const syncUser = async (req: Request, res: Response) => {
|
|
||||||
console.log('--- syncUser called ---', req.body);
|
|
||||||
try {
|
|
||||||
const { clerkId, email, name, organizationId, clerkRole } = req.body;
|
|
||||||
|
|
||||||
if (!clerkId || !email || !name) {
|
|
||||||
return res.status(400).json({ error: 'clerkId, email e name são obrigatórios.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Upsert the global User record
|
|
||||||
let user = await User.findOne({ clerkId });
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
user.email = email;
|
|
||||||
user.name = name;
|
|
||||||
await user.save();
|
|
||||||
} else {
|
|
||||||
user = await User.create({
|
|
||||||
clerkId,
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
role: 'guest', // Default global role
|
|
||||||
isBanned: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organizationId) {
|
|
||||||
|
|
||||||
// Map Clerk role to our app role
|
|
||||||
let appRole: OrgRole = 'guest';
|
|
||||||
if (clerkRole === 'org:admin') {
|
|
||||||
appRole = 'admin';
|
|
||||||
} else if (clerkRole === 'org:member') {
|
|
||||||
appRole = 'user';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use findOneAndUpdate with upsert to handle race conditions atomically
|
|
||||||
// This avoids the need for try/catch on create and handles existing members too
|
|
||||||
const member = await OrganizationMember.findOneAndUpdate(
|
|
||||||
{ clerkUserId: clerkId, organizationId },
|
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
name,
|
|
||||||
email,
|
|
||||||
// Only update role if it's the first time (creation)
|
|
||||||
// Or we can optionally update it if needed.
|
|
||||||
// For now, let's NOT overwrite role on update to preserve local changes,
|
|
||||||
// UNLESS we want to force sync with Clerk.
|
|
||||||
// Let's use $setOnInsert for fields we only want to set on creation.
|
|
||||||
},
|
|
||||||
$setOnInsert: {
|
|
||||||
role: appRole,
|
|
||||||
isBanned: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true, setDefaultsOnInsert: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return combined info
|
|
||||||
return res.json({
|
|
||||||
...user.toObject(),
|
|
||||||
organizationRole: member.role,
|
|
||||||
organizationBanned: member.isBanned
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(user);
|
|
||||||
} catch (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,
|
|
||||||
// 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) {
|
||||||
@@ -106,7 +14,7 @@ export const getCurrentUser = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
const member = await OrganizationMember.findOne({
|
const member = await OrganizationMember.findOne({
|
||||||
clerkUserId: req.appUser.clerkId,
|
userId: req.appUser._id.toString(),
|
||||||
organizationId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -127,21 +35,16 @@ export const getCurrentUser = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Get all users for the current organization (admin only)
|
||||||
* 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 members = await OrganizationMember.find({ organizationId }).sort({ createdAt: -1 });
|
||||||
console.log(`Found ${members.length} members for org ${organizationId}:`, members.map(m => ({ name: m.name, email: m.email, clerkId: m.clerkUserId })));
|
|
||||||
res.json(members);
|
res.json(members);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting users:', error);
|
console.error('Error getting users:', error);
|
||||||
@@ -149,9 +52,7 @@ export const getAllUsers = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Update user role within organization (admin only)
|
||||||
* 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;
|
||||||
@@ -184,14 +85,12 @@ export const updateUserRole = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
res.json(member);
|
res.json(member);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling ban:', error);
|
console.error('Error updating role:', error);
|
||||||
res.status(500).json({ error: 'Erro ao alterar status de banimento.' });
|
res.status(500).json({ error: 'Erro ao atualizar role.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Ban or unban user within organization (admin only)
|
||||||
* 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;
|
||||||
@@ -208,12 +107,12 @@ export const toggleBanUser = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prevent banning yourself
|
// Prevent banning yourself
|
||||||
if (req.appUser && member.clerkUserId === req.appUser.clerkId) {
|
if (req.appUser && member.userId === req.appUser._id.toString()) {
|
||||||
return res.status(400).json({ error: 'Você não pode banir a si mesmo.' });
|
return res.status(400).json({ error: 'Você não pode banir a si mesmo.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent banning another admin
|
// Prevent banning another admin
|
||||||
if (member.role === 'admin') {
|
if (member.role === 'admin' && isBanned) {
|
||||||
return res.status(400).json({ error: 'Não é possível banir um administrador.' });
|
return res.status(400).json({ error: 'Não é possível banir um administrador.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,38 +126,22 @@ export const toggleBanUser = async (req: AuthRequest, res: Response) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Update current user's lastSeenAt timestamp
|
||||||
* 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 User.findByIdAndUpdate(req.appUser._id, { lastSeenAt: new Date() });
|
await User.findByIdAndUpdate(req.appUser._id, { lastSeenAt: new Date() });
|
||||||
|
|
||||||
// Also update Organization Member for tighter query
|
|
||||||
// But for now User model is enough if we join correctly, or just use User model for presence.
|
|
||||||
// Actually, since we want to show users per organization, we should filter by Org.
|
|
||||||
// Our 'User.ts' has organizationId, but it might be just the 'default' one.
|
|
||||||
// Let's rely on OrganizationMember for the list, but we need to update lastSeenAt there too?
|
|
||||||
// Strategy: Update User (global), and when querying active users, join or filter.
|
|
||||||
// Better: Update OrganizationMember too if we want org-specific presence?
|
|
||||||
// Simpler: Just update User. When fetching active users, we fetch OrganizationMembers and populate User details, filtering by User.lastSeenAt.
|
|
||||||
|
|
||||||
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)
|
||||||
* 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;
|
||||||
@@ -268,20 +151,15 @@ export const getActiveUsers = async (req: AuthRequest, res: Response) => {
|
|||||||
return res.status(400).json([]);
|
return res.status(400).json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find members of this org
|
|
||||||
const members = await OrganizationMember.find({ organizationId });
|
const members = await OrganizationMember.find({ organizationId });
|
||||||
|
const userIds = members.map(m => m.userId);
|
||||||
|
|
||||||
// Get their Clerk IDs
|
|
||||||
const clerkIds = members.map(m => m.clerkUserId);
|
|
||||||
|
|
||||||
// Find Users who were seen recently (2 minutes)
|
|
||||||
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
|
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
|
||||||
|
|
||||||
const activeUsers = await User.find({
|
const activeUsers = await User.find({
|
||||||
clerkId: { $in: clerkIds },
|
_id: { $in: userIds, $ne: currentUserId },
|
||||||
lastSeenAt: { $gte: twoMinutesAgo },
|
lastSeenAt: { $gte: twoMinutesAgo }
|
||||||
_id: { $ne: currentUserId } // Optional: exclude self
|
}).select('name email lastSeenAt');
|
||||||
}).select('name email lastSeenAt clerkId'); // Only needed fields
|
|
||||||
|
|
||||||
res.json(activeUsers);
|
res.json(activeUsers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -300,20 +178,20 @@ export const deleteUser = async (req: Request, res: Response) => {
|
|||||||
return res.status(400).json({ error: 'Organização não selecionada.' });
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Deleting member ${id} from organization ${organizationId}`);
|
const result = await OrganizationMember.findOneAndDelete({ _id: id, organizationId });
|
||||||
|
|
||||||
// Delete from OrganizationMember collection
|
|
||||||
const result = await OrganizationMember.findByIdAndDelete(id);
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return res.status(404).json({ error: 'Membro não encontrado.' });
|
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 });
|
res.json({ message: 'Membro removido com sucesso.', deletedMember: result });
|
||||||
} catch (error) {
|
} 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.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Placeholder for sync (not needed with custom auth but keeping to avoid breaks if called)
|
||||||
|
export const syncUser = async (req: Request, res: Response) => {
|
||||||
|
res.json({ message: 'Sync no longer required with JWT auth.' });
|
||||||
|
};
|
||||||
|
|||||||
38
src/server/middleware/authMiddleware.ts
Normal file
38
src/server/middleware/authMiddleware.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import User from '../models/User.js';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'secret';
|
||||||
|
|
||||||
|
export interface AuthRequest extends Request {
|
||||||
|
user?: any;
|
||||||
|
appUser?: any; // For backward compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authenticateJWT = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
// Keep guest access if allowed by specific routes
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
const decoded: any = jwt.verify(token, JWT_SECRET);
|
||||||
|
|
||||||
|
const user = await User.findById(decoded.id);
|
||||||
|
|
||||||
|
if (!user || user.isBanned) {
|
||||||
|
return res.status(401).json({ error: 'Sessão inválida ou usuário bloqueado' });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user;
|
||||||
|
req.appUser = user; // Map to appUser for existing controllers
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JWT validation error:', error);
|
||||||
|
return res.status(401).json({ error: 'Token inválido' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -17,93 +17,6 @@ declare module 'express-serve-static-core' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware to extract and verify user from Clerk ID header
|
|
||||||
* Also loads organization-specific role if organization context is provided
|
|
||||||
*/
|
|
||||||
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const clerkId = req.headers['x-clerk-user-id'] as string;
|
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
|
||||||
|
|
||||||
if (!clerkId) {
|
|
||||||
return next(); // No user, continue without
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findOne({ clerkId });
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
if (user.isBanned) {
|
|
||||||
return res.status(403).json({ error: 'Conta bloqueada. Entre em contato com o administrador.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create extended user object
|
|
||||||
const appUser: IAppUser = user.toObject() as IAppUser;
|
|
||||||
appUser.organizationId = organizationId;
|
|
||||||
|
|
||||||
// If organization context, get org-specific role
|
|
||||||
if (organizationId) {
|
|
||||||
// Check if Organization is globally banned (subscription specific, etc.)
|
|
||||||
const orgStatus = await Organization.findOne({ clerkId: organizationId });
|
|
||||||
const orgName = req.headers['x-organization-name'] ? decodeURIComponent(req.headers['x-organization-name'] as string) : undefined;
|
|
||||||
|
|
||||||
if (orgStatus) {
|
|
||||||
// Update name if different and present
|
|
||||||
if (orgName && orgStatus.name !== orgName) {
|
|
||||||
try {
|
|
||||||
await Organization.updateOne(
|
|
||||||
{ clerkId: organizationId },
|
|
||||||
{ name: orgName }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to update organization name', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orgStatus.isBanned) {
|
|
||||||
return res.status(403).json({
|
|
||||||
error: 'Acesso bloqueado: Esta organização está suspensa. Entre em contato com o suporte.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create new org with name if present
|
|
||||||
try {
|
|
||||||
await Organization.create({
|
|
||||||
clerkId: organizationId,
|
|
||||||
name: orgName
|
|
||||||
});
|
|
||||||
} catch (_e) {
|
|
||||||
console.warn('Organization auto-create race condition', _e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const member = await OrganizationMember.findOne({ clerkUserId: clerkId, organizationId });
|
|
||||||
if (member) {
|
|
||||||
if (member.isBanned) {
|
|
||||||
return res.status(403).json({ error: 'Acesso bloqueado nesta organização.' });
|
|
||||||
}
|
|
||||||
appUser.organizationRole = member.role;
|
|
||||||
appUser.role = member.role; // Override global role with org role
|
|
||||||
} else {
|
|
||||||
// User exists but is not a member of this org yet
|
|
||||||
appUser.organizationRole = 'guest';
|
|
||||||
appUser.role = 'guest';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.appUser = appUser;
|
|
||||||
// console.log(`✅ Request authenticated as: ${appUser.name} (${appUser.role})`);
|
|
||||||
} else {
|
|
||||||
console.warn(`⚠️ User with Clerk ID ${clerkId} not found in MongoDB. Sync required.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error extracting user:', error);
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware to require specific roles for a route
|
* Middleware to require specific roles for a route
|
||||||
* @param allowedRoles Array of roles that can access the route
|
* @param allowedRoles Array of roles that can access the route
|
||||||
@@ -119,6 +32,7 @@ export const requireRole = (allowedRoles: OrgRole[]) => {
|
|||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to global role if organizationRole is not set
|
||||||
const effectiveRole = req.appUser.organizationRole || req.appUser.role;
|
const effectiveRole = req.appUser.organizationRole || req.appUser.role;
|
||||||
|
|
||||||
if (!allowedRoles.includes(effectiveRole as OrgRole)) {
|
if (!allowedRoles.includes(effectiveRole as OrgRole)) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import mongoose, { Schema, Document } from 'mongoose';
|
|||||||
|
|
||||||
export interface IMessage extends Document {
|
export interface IMessage extends Document {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
fromUserId: string; // clerkId do remetente
|
fromUserId: string; // ID do remetente
|
||||||
toUserId: string; // clerkId do destinatário
|
toUserId: string; // ID do destinatário
|
||||||
message: string;
|
message: string;
|
||||||
isRead: boolean;
|
isRead: boolean;
|
||||||
readAt?: Date;
|
readAt?: Date;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import mongoose, { Schema, Document } from 'mongoose';
|
import mongoose, { Schema, Document } from 'mongoose';
|
||||||
|
|
||||||
export interface IOrganization extends Document {
|
export interface IOrganization extends Document {
|
||||||
clerkId: string;
|
organizationId: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
@@ -9,7 +9,7 @@ export interface IOrganization extends Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OrganizationSchema: Schema = new Schema({
|
const OrganizationSchema: Schema = new Schema({
|
||||||
clerkId: { type: String, required: true, unique: true, index: true },
|
organizationId: { type: String, required: true, unique: true, index: true },
|
||||||
name: { type: String },
|
name: { type: String },
|
||||||
isBanned: { type: Boolean, default: false },
|
isBanned: { type: Boolean, default: false },
|
||||||
}, { timestamps: true });
|
}, { timestamps: true });
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import mongoose, { Schema, Document } from 'mongoose';
|
|||||||
export type OrgRole = 'guest' | 'user' | 'admin';
|
export type OrgRole = 'guest' | 'user' | 'admin';
|
||||||
|
|
||||||
export interface IOrganizationMember extends Document {
|
export interface IOrganizationMember extends Document {
|
||||||
clerkUserId: string;
|
userId: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
role: OrgRole;
|
role: OrgRole;
|
||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
@@ -15,7 +15,7 @@ export interface IOrganizationMember extends Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OrganizationMemberSchema: Schema = new Schema({
|
const OrganizationMemberSchema: Schema = new Schema({
|
||||||
clerkUserId: {
|
userId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
index: true
|
index: true
|
||||||
@@ -47,6 +47,6 @@ const OrganizationMemberSchema: Schema = new Schema({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Compound index for unique user per organization
|
// Compound index for unique user per organization
|
||||||
OrganizationMemberSchema.index({ clerkUserId: 1, organizationId: 1 }, { unique: true });
|
OrganizationMemberSchema.index({ userId: 1, organizationId: 1 }, { unique: true });
|
||||||
|
|
||||||
export default mongoose.models.OrganizationMember || mongoose.model<IOrganizationMember>('OrganizationMember', OrganizationMemberSchema);
|
export default mongoose.models.OrganizationMember || mongoose.model<IOrganizationMember>('OrganizationMember', OrganizationMemberSchema);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import mongoose, { Schema, Document } from 'mongoose';
|
|||||||
export type UserRole = 'guest' | 'user' | 'admin';
|
export type UserRole = 'guest' | 'user' | 'admin';
|
||||||
|
|
||||||
export interface IUser extends Document {
|
export interface IUser extends Document {
|
||||||
clerkId: string;
|
clerkId?: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
password?: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
@@ -17,17 +18,23 @@ export interface IUser extends Document {
|
|||||||
const UserSchema: Schema = new Schema({
|
const UserSchema: Schema = new Schema({
|
||||||
clerkId: {
|
clerkId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
|
||||||
unique: true,
|
unique: true,
|
||||||
|
sparse: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
select: false // Password shouldn't be returned by default
|
||||||
|
},
|
||||||
organizationId: {
|
organizationId: {
|
||||||
type: String,
|
type: String,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
|
unique: true,
|
||||||
|
index: true
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
11
src/server/routes/authRoutes.ts
Normal file
11
src/server/routes/authRoutes.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import * as authController from '../controllers/authController.js';
|
||||||
|
import { authenticateJWT } from '../middleware/authMiddleware.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/login', authController.login);
|
||||||
|
router.post('/register', authController.register);
|
||||||
|
router.get('/me', authenticateJWT, authController.getMe);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { syncUser, getCurrentUser, getAllUsers, updateUserRole, toggleBanUser, heartbeat, getActiveUsers, deleteUser } from '../controllers/userController.js';
|
import { syncUser, getCurrentUser, getAllUsers, updateUserRole, toggleBanUser, heartbeat, getActiveUsers, deleteUser } from '../controllers/userController.js';
|
||||||
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
import { requireAdmin, requireUser } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Sync user from Clerk (public - called on login)
|
// Sync user (placeholder)
|
||||||
router.post('/sync', syncUser);
|
router.post('/sync', syncUser);
|
||||||
|
|
||||||
// Get current user (requires extractUser middleware)
|
// Get current user
|
||||||
router.get('/me', extractUser, getCurrentUser);
|
router.get('/me', requireUser, getCurrentUser);
|
||||||
|
|
||||||
// Heartbeat & Presence
|
// Heartbeat & Presence
|
||||||
router.post('/heartbeat', extractUser, heartbeat);
|
router.post('/heartbeat', requireUser, heartbeat);
|
||||||
router.get('/active', extractUser, getActiveUsers);
|
router.get('/active', requireUser, getActiveUsers);
|
||||||
|
|
||||||
// Admin-only routes
|
// Admin-only routes
|
||||||
router.get('/', extractUser, requireAdmin, getAllUsers);
|
router.get('/', requireUser, requireAdmin, getAllUsers);
|
||||||
router.patch('/:id/role', extractUser, requireAdmin, updateUserRole);
|
router.patch('/:id/role', requireUser, requireAdmin, updateUserRole);
|
||||||
router.patch('/:id/ban', extractUser, requireAdmin, toggleBanUser);
|
router.patch('/:id/ban', requireUser, requireAdmin, toggleBanUser);
|
||||||
router.delete('/:id', extractUser, requireAdmin, deleteUser);
|
router.delete('/:id', requireUser, requireAdmin, deleteUser);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
53
src/server/scripts/migrate-admin.ts
Normal file
53
src/server/scripts/migrate-admin.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.join(process.cwd(), '.env') });
|
||||||
|
|
||||||
|
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb+srv://admtracksteel:29OHAHpKTI8XcCNt@cluster0.a4xiilu.mongodb.net/ts_gpi?retryWrites=true&w=majority&appName=Cluster0';
|
||||||
|
|
||||||
|
const UserSchema = new mongoose.Schema({
|
||||||
|
email: { type: String, required: true, unique: true },
|
||||||
|
password: { type: String, required: true },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
role: { type: String, enum: ['guest', 'user', 'admin'], default: 'guest' },
|
||||||
|
isBanned: { type: Boolean, default: false }
|
||||||
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
async function migrateAdmin() {
|
||||||
|
try {
|
||||||
|
await mongoose.connect(MONGODB_URI);
|
||||||
|
console.log('✅ Conectado ao MongoDB');
|
||||||
|
|
||||||
|
const User = mongoose.models.User || mongoose.model('User', UserSchema);
|
||||||
|
|
||||||
|
const adminEmail = 'admtracksteel@gmail.com';
|
||||||
|
const defaultPassword = 'admin_gpi_2026'; // SENHA TEMPORÁRIA
|
||||||
|
const hashedPassword = await bcrypt.hash(defaultPassword, 12);
|
||||||
|
|
||||||
|
const admin = await User.findOneAndUpdate(
|
||||||
|
{ email: adminEmail },
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
password: hashedPassword,
|
||||||
|
role: 'admin',
|
||||||
|
name: 'Administrador Global',
|
||||||
|
isBanned: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ Administrador ${adminEmail} migrado com sucesso!`);
|
||||||
|
console.log(`🔑 Senha temporária definida: ${defaultPassword}`);
|
||||||
|
console.log(`⚠️ Por favor, altere sua senha após o primeiro login.`);
|
||||||
|
|
||||||
|
await mongoose.disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro na migração:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateAdmin();
|
||||||
Reference in New Issue
Block a user