diff --git a/package-lock.json b/package-lock.json index 99d3bcf..d348930 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,14 @@ "name": "gpi-app", "version": "1.0.0", "dependencies": { - "@clerk/clerk-react": "^5.59.6", - "@clerk/localizations": "^3.35.3", "@tailwindcss/postcss": "^4.1.18", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", "@types/mongoose": "^5.11.96", "@types/uuid": "^10.0.0", "@vercel/speed-insights": "^1.3.1", "axios": "^1.13.2", + "bcryptjs": "^3.0.3", "clsx": "^2.1.1", "cors": "^2.8.5", "date-fns": "^4.1.0", @@ -22,6 +23,8 @@ "dotenv": "^17.2.3", "enhanced-resolve": "^5.18.4", "express": "^5.2.1", + "framer-motion": "^12.36.0", + "jsonwebtoken": "^9.0.3", "lucide-react": "^0.562.0", "mongodb": "^7.0.0", "mongoose": "^9.1.5", @@ -1636,75 +1639,6 @@ "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": { "version": "0.8.1", "dev": true, @@ -3403,6 +3337,12 @@ "@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": { "version": "1.19.6", "dev": true, @@ -3509,6 +3449,16 @@ "dev": true, "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": { "version": "5.11.96", "license": "MIT", @@ -3516,6 +3466,12 @@ "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": { "version": "2.0.0", "dev": true, @@ -3528,7 +3484,6 @@ "version": "24.10.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -5008,6 +4963,15 @@ "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": { "version": "2.3.0", "dev": true, @@ -5114,6 +5078,12 @@ "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": { "version": "1.1.2", "license": "MIT" @@ -5516,10 +5486,6 @@ "node": ">=8" } }, - "node_modules/csstype": { - "version": "3.1.3", - "license": "MIT" - }, "node_modules/d3-array": { "version": "3.2.4", "license": "ISC", @@ -5843,6 +5809,15 @@ "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": { "version": "2.5.9", "resolved": "https://registry.npmjs.org/edge-runtime/-/edge-runtime-2.5.9.tgz", @@ -6691,6 +6666,33 @@ "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": { "version": "2.0.0", "license": "MIT", @@ -6902,10 +6904,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "license": "BSD-2-Clause" - }, "node_modules/glob/node_modules/minimatch": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", @@ -7744,13 +7742,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-cookie": { - "version": "3.0.5", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -7844,6 +7835,61 @@ "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": { "version": "3.0.0", "license": "Apache-2.0", @@ -8161,11 +8207,53 @@ "dev": true, "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": { "version": "4.6.2", "dev": true, "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": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8435,6 +8523,21 @@ "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": { "version": "0.9.0", "license": "MIT", @@ -10120,10 +10223,6 @@ "node": ">= 0.8" } }, - "node_modules/std-env": { - "version": "3.10.0", - "license": "MIT" - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "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" } }, - "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": { "version": "3.4.0", "license": "MIT", @@ -10865,7 +10953,6 @@ }, "node_modules/undici-types": { "version": "7.16.0", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/package.json b/package.json index 61f5004..d728952 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,14 @@ "start": "node dist/server/index.js" }, "dependencies": { - "@clerk/clerk-react": "^5.59.6", - "@clerk/localizations": "^3.35.3", "@tailwindcss/postcss": "^4.1.18", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.10", "@types/mongoose": "^5.11.96", "@types/uuid": "^10.0.0", "@vercel/speed-insights": "^1.3.1", "axios": "^1.13.2", + "bcryptjs": "^3.0.3", "clsx": "^2.1.1", "cors": "^2.8.5", "date-fns": "^4.1.0", @@ -27,6 +28,8 @@ "dotenv": "^17.2.3", "enhanced-resolve": "^5.18.4", "express": "^5.2.1", + "framer-motion": "^12.36.0", + "jsonwebtoken": "^9.0.3", "lucide-react": "^0.562.0", "mongodb": "^7.0.0", "mongoose": "^9.1.5", diff --git a/src/client/App.tsx b/src/client/App.tsx index eceb3a3..705945b 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,5 +1,5 @@ +import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import { SignedIn, SignedOut, useOrganization } from '@clerk/clerk-react'; import { AuthProvider } from './context/AuthContext'; import { useAuth } from './context/useAuth'; import { SystemSettingsProvider } from './context/SystemSettingsContext'; @@ -18,103 +18,107 @@ import { DeveloperDashboard } from './pages/DeveloperDashboard'; import { CalculatorDashboard } from './pages/CalculatorDashboard'; import { StockDashboard } from './pages/StockDashboard'; import { GuestDashboard } from './pages/GuestDashboard'; -import { Login } from './pages/Login'; -import { OrganizationSelector } from './pages/OrganizationSelector'; +import Login from './pages/Login'; +import Register from './pages/Register'; import InstrumentList from './pages/InstrumentList'; const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { isDeveloper, isLoading } = useAuth(); - if (isLoading) return null; if (!isDeveloper()) return ; - return <>{children}; }; -const AppContent: React.FC = () => { - const { organization } = useOrganization(); +const AppRoutes: React.FC = () => { + const { isSignedIn, isLoading } = useAuth(); - console.log('AppContent rendered'); - console.log('Current organization:', organization); + if (isLoading) { + return ( +
+
+
+ ); + } - // If user is signed in but has no organization, show org selector - if (!organization) { - console.log('No organization - showing OrganizationSelector'); - return ; - } + if (!isSignedIn) { + return ( + + } /> + } /> + } /> + + ); + } - console.log('Organization exists - showing main app'); - return ( - - - - - - + return ( + + } /> + } /> + } /> } /> } /> } /> } /> } /> - - + + + } /> - - + + + } /> - - + + + } /> } /> - - - } + path="/admin" + element={ + + + + } /> - - - } + path="/stock" + element={ + + + + } /> - - - } + path="/developer" + element={ + + + + } /> - - - - - - - ); + } /> + + + ); }; function App() { return ( - - - - - - + + + + + + + + + ); } diff --git a/src/client/components/Layout.tsx b/src/client/components/Layout.tsx index 1d6c29b..77db3d0 100644 --- a/src/client/components/Layout.tsx +++ b/src/client/components/Layout.tsx @@ -2,13 +2,12 @@ import React, { useState } from 'react'; import NotificationBell from './NotificationBell'; import { TeamPresence } from './TeamPresence'; 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 { useClerk, UserButton, useUser, OrganizationSwitcher, useOrganization } from '@clerk/clerk-react'; import { TechnicalManual } from './TechnicalManual'; import { useAuth } from '../context/useAuth'; // import { useSystemSettings } from '../context/SystemSettingsContext'; -import { setApiOrgData } from '../services/api'; +import { setApiOrganizationId } from '../services/api'; interface LayoutProps { children: React.ReactNode; @@ -22,21 +21,17 @@ export const Layout: React.FC = ({ children }) => { return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches); }); const location = useLocation(); - const { signOut } = useClerk(); - const { user } = useUser(); - const { organization } = useOrganization(); - const { isAdmin, isUser, isDeveloper, appUser } = useAuth(); + const { isAdmin, isUser, isDeveloper, appUser, logout } = useAuth(); // const { settings } = useSystemSettings(); // Sync Organization ID with API client React.useEffect(() => { - if (organization?.id) { - - setApiOrgData(organization.id, organization.name); + if (appUser?.organizationId) { + setApiOrganizationId(appUser.organizationId); } else { - setApiOrgData(null); + setApiOrganizationId(null); } - }, [organization]); + }, [appUser?.organizationId]); // Helper to get role display name const getRoleDisplay = () => { @@ -118,44 +113,15 @@ export const Layout: React.FC = ({ children }) => {
- {isAdmin() ? ( - - ) : ( -
- {organization?.imageUrl ? ( - {organization.name} - ) : ( -
- {organization?.name?.substring(0, 2).toUpperCase()} -
- )} -
-

{organization?.name || 'Carregando...'}

-

Organização

-
+
+
+ {appUser?.organizationId ? 'ORG' : 'GPI'}
- )} +
+

{appUser?.organizationId || 'Padrão'}

+

Organização

+
+
{/* Team Presence - Shows all members with online/offline status */} @@ -257,7 +223,7 @@ export const Layout: React.FC = ({ children }) => {
- {/* Message Modal */} {selectedUser && ( m.clerkUserId === selectedUser.id)!)} + existingMessage={getExistingMessage(allMembers.find(m => m.id === selectedUser.id)!)} onMessageSent={handleMessageSent} /> )} diff --git a/src/client/context/AuthContext.tsx b/src/client/context/AuthContext.tsx index 75a8094..2ec5b59 100644 --- a/src/client/context/AuthContext.tsx +++ b/src/client/context/AuthContext.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { useUser, useOrganization } from '@clerk/clerk-react'; +import React, { useState, useEffect, useCallback } from 'react'; import type { AppUser } from '../types'; import { AuthContext } from './AuthContextType'; -import { setApiClerkUserId, setApiOrganizationId, getBaseUrl } from '../services/api'; +import api, { getBaseUrl, setApiOrganizationId } from '../services/api'; const API_URL = getBaseUrl(); @@ -11,119 +10,57 @@ interface AuthProviderProps { } export const AuthProvider: React.FC = ({ children }) => { - const { user, isLoaded } = useUser(); - const { organization, membership } = useOrganization(); const [appUser, setAppUser] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const lastContextRef = useRef<{ clerkId?: string, orgId?: string | null }>({}); - // Set the clerk user ID and organization ID for the API interceptor - useEffect(() => { - setApiClerkUserId(user?.id || null); - setApiOrganizationId(organization?.id || null); - }, [user?.id, organization?.id]); + const logout = useCallback(() => { + localStorage.removeItem('gpi_token'); + setAppUser(null); + setApiOrganizationId(null); + window.location.href = '/login'; + }, []); - const syncUser = useCallback(async () => { - if (!user) { + const fetchMe = useCallback(async () => { + const token = localStorage.getItem('gpi_token'); + if (!token) { setAppUser(null); setIsLoading(false); return; } try { - // Only set loading if the context has changed (new user or new organization) - // This prevents unmounting/remounting components on window focus revalidations - const isSameContext = - lastContextRef.current.clerkId === user.id && - lastContextRef.current.orgId === (organization?.id || null); - - if (!isSameContext) { - setIsLoading(true); - } - setError(null); - - // Sync user with backend, including organization context - const response = await fetch(`${API_URL}/users/sync`, { - method: 'POST', - 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'; + setIsLoading(true); + const response = await api.get('/auth/me'); + const userData = response.data; + setAppUser({ - ...syncedUser, - id: syncedUser._id || syncedUser.id, - role: effectiveRole, // Override with organization-specific role + ...userData, + id: userData._id || userData.id }); - - // Update last context ref - lastContextRef.current = { clerkId: user.id, orgId: organization?.id || null }; - } catch (err) { - console.error('Error syncing user:', err); - setError('Erro ao carregar dados do usuário'); + + if (userData.organizationId) { + setApiOrganizationId(userData.organizationId); + } + } catch (err: any) { + console.error('Error fetching current user:', err); + if (err.response?.status === 401) { + logout(); + } else { + setError('Erro ao carregar dados do usuário'); + } } finally { 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(() => { - if (isLoaded && user) { - syncUser(); - } - }, [isLoaded, user, organization?.id, syncUser]); + fetchMe(); + }, [fetchMe]); const isDeveloper = useCallback(() => { - return user?.primaryEmailAddress?.emailAddress === 'admtracksteel@gmail.com'; - }, [user]); + return appUser?.email === 'admtracksteel@gmail.com'; + }, [appUser]); const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]); const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]); @@ -135,14 +72,15 @@ export const AuthProvider: React.FC = ({ children }) => { value={{ appUser, isLoading, - isSignedIn: !!user, + isSignedIn: !!appUser, error, isAdmin, isUser, isGuest, isDeveloper, canEdit, - refetchUser, + refetchUser: fetchMe, + logout, // Added logout to context }} > {children} diff --git a/src/client/context/AuthContextType.ts b/src/client/context/AuthContextType.ts index 7529430..cc35939 100644 --- a/src/client/context/AuthContextType.ts +++ b/src/client/context/AuthContextType.ts @@ -12,6 +12,7 @@ export interface AuthContextType { isDeveloper: () => boolean; canEdit: () => boolean; refetchUser: () => Promise; + logout: () => void; } export const AuthContext = createContext(undefined); diff --git a/src/client/contexts/NotificationContext.tsx b/src/client/contexts/NotificationContext.tsx index 8bbc293..9877406 100644 --- a/src/client/contexts/NotificationContext.tsx +++ b/src/client/contexts/NotificationContext.tsx @@ -1,16 +1,16 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { useAuth } from '@clerk/clerk-react'; +import { useAuth } from '../context/useAuth'; import api from '../services/api'; import type { INotification } from '../types'; import { NotificationContext } from './NotificationContextState'; export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const { orgId, isSignedIn } = useAuth(); + const { appUser, isSignedIn } = useAuth(); const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(false); const fetchNotifications = useCallback(async () => { - if (!orgId || !isSignedIn) return; + if (!isSignedIn) return; try { if (notifications.length === 0) setLoading(true); @@ -21,7 +21,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ } finally { setLoading(false); } - }, [orgId, isSignedIn, notifications.length]); + }, [isSignedIn, notifications.length]); const markAsRead = async (id: string) => { try { @@ -70,7 +70,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ // Polling effect useEffect(() => { - if (isSignedIn && orgId) { + if (isSignedIn) { fetchNotifications(); // Initial fetch const interval = setInterval(() => { @@ -81,7 +81,7 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ } else { setNotifications([]); } - }, [isSignedIn, orgId, fetchNotifications]); + }, [isSignedIn, fetchNotifications]); const unreadCount = notifications.filter(n => !n.isRead).length; diff --git a/src/client/hooks/usePresence.ts b/src/client/hooks/usePresence.ts index 4344053..788bd06 100644 --- a/src/client/hooks/usePresence.ts +++ b/src/client/hooks/usePresence.ts @@ -4,9 +4,9 @@ import { useAuth } from '../context/useAuth'; export interface ActiveUser { _id: string; + id: string; name: string; email: string; - clerkId: string; lastSeenAt: string; } diff --git a/src/client/main.tsx b/src/client/main.tsx index bfdebef..b5a7356 100644 --- a/src/client/main.tsx +++ b/src/client/main.tsx @@ -1,47 +1,7 @@ import { createRoot } from 'react-dom/client' - -import { ClerkProvider } from '@clerk/clerk-react' -import { ptBR } from '@clerk/localizations' import './index.css' import App from './App.tsx' -const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY - -if (!PUBLISHABLE_KEY) { - throw new Error("Missing Publishable Key") -} - createRoot(document.getElementById('root')!).render( - - , ) - diff --git a/src/client/pages/AdminDashboard.tsx b/src/client/pages/AdminDashboard.tsx index 4aa8cf3..9b3d3cc 100644 --- a/src/client/pages/AdminDashboard.tsx +++ b/src/client/pages/AdminDashboard.tsx @@ -1,6 +1,5 @@ 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, Upload, Info, Image as ImageIcon, Box, Database } from 'lucide-react'; +import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Info, Image as ImageIcon, Box, Database } from 'lucide-react'; import { clsx } from 'clsx'; import type { AppUser, UserRole } from '../types'; import { useAuth } from '../context/useAuth'; @@ -15,200 +14,65 @@ const roleLabels: Record { - const { user } = useUser(); - const { organization } = useOrganization(); - const { isAdmin } = useAuth(); + const { isAdmin, appUser } = useAuth(); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [filterRole, setFilterRole] = useState('all'); const [actionLoading, setActionLoading] = useState(null); const [activeTab, setActiveTab] = useState<'users' | 'organization' | 'settings' | 'stock' | 'backup'>('users'); - const [logoLoading, setLogoLoading] = useState(false); const fetchUsers = useCallback(async () => { - if (!user || !organization?.id) return; - try { setIsLoading(true); 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) { console.error('Error fetching users:', error); } finally { 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(() => { fetchUsers(); }, [fetchUsers]); const handleRoleChange = async (userId: string, newRole: UserRole) => { - if (!user) return; - setActionLoading(userId); try { const response = await api.patch(`/users/${userId}/role`, { role: newRole }); const updated = response.data; setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u)); - } catch (error: unknown) { - const err = error as { response?: { data?: { error?: string } } }; + } catch (error: any) { 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 { setActionLoading(null); } }; const handleToggleBan = async (userId: string, isBanned: boolean) => { - if (!user) return; - setActionLoading(userId); try { const response = await api.patch(`/users/${userId}/ban`, { isBanned }); const updated = response.data; setUsers(users.map(u => u.id === userId ? { ...updated, id: updated._id || updated.id } : u)); - } catch (error: unknown) { - const err = error as { response?: { data?: { error?: string } } }; + } catch (error: any) { 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 { setActionLoading(null); } }; const filteredUsers = users.filter(u => { - const matchesSearch = u.name.toLowerCase().includes(searchTerm.toLowerCase()) || - u.email.toLowerCase().includes(searchTerm.toLowerCase()); + const matchesSearch = (u.name || '').toLowerCase().includes(searchTerm.toLowerCase()) || + (u.email || '').toLowerCase().includes(searchTerm.toLowerCase()); const matchesRole = filterRole === 'all' || u.role === filterRole; return matchesSearch && matchesRole; }); - const handleLogoUpload = async (e: React.ChangeEvent) => { - 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()) { return (
@@ -230,22 +94,14 @@ export const AdminDashboard: React.FC = () => {
Administração -

Configurações globais e gerenciamento de usuários

+

Gestão de usuários e configurações do sistema

{activeTab === 'users' && (
- -
- ) : activeTab === 'organization' ? ( -
- {/* Organization Settings Panel */} -
-
-
-
- -
-
-

Identidade Visual

-

Gerencie o logo da sua organização

-
-
- -
- {organization?.imageUrl ? ( -
-
- {organization.name} -
-
- -
-
- ) : ( -
- - Sem Logo -
- )} - -
- - - {logoLoading && ( -
- - Enviando logo... -
- )} -
-
-
- -
-
-
- -
-
-

Requisitos & Dicas

-

Regras para um visual impecável

-
-
- -
-
-

-
- Formatos Suportados -

-

- Aceitamos arquivos nos formatos PNG, JPG ou SVG. O formato SVG é recomendado para máxima nitidez em qualquer tamanho. -

-
- -
-

-
- Dimensões Recomendadas -

-

- Recomendamos uma imagem quadrada de no mínimo 512x512 pixels. Logos horizontais podem não aparecer corretamente em todas as áreas. -

-
- -
-

-
- Limite de Tamanho -

-

- O arquivo não deve ultrapassar 500 KB. Arquivos maiores serão rejeitados automaticamente para garantir rapidez no carregamento. -

-
-
-
-
-
) : activeTab === 'settings' ? ( ) : activeTab === 'backup' ? ( ) : ( - // Lazily load or direct render StockDashboard (Need to import it)
-

Gestão de Estoque

-

Acesse a nova página dedicada ao controle de estoque.

- - Ir para Estoque - +

Em breve

+

Novas configurações serão adicionadas aqui.

)} ); }; + +export default AdminDashboard; diff --git a/src/client/pages/DeveloperDashboard.tsx b/src/client/pages/DeveloperDashboard.tsx index 0987d15..1ad1053 100644 --- a/src/client/pages/DeveloperDashboard.tsx +++ b/src/client/pages/DeveloperDashboard.tsx @@ -419,7 +419,7 @@ export const DeveloperDashboard: React.FC = () => {
{admins.map(admin => ( -
+
{admin.name.charAt(0).toUpperCase()}
@@ -441,7 +441,7 @@ export const DeveloperDashboard: React.FC = () => {
{commonUsers.map(user => ( -
+
{ + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(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 ( -
- {/* Background decorative elements */} -
-
- -
- {/* Logo Area */} -
-
- G +
+ +
+
+

GPI

+

Gestão de Pintura Industrial

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + 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 + /> +
+ +
+ + 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 + /> +
+ + +
+ +
+

+ Não tem uma conta? Contate o administrador +

-

GPI

-

Gestão de Pintura Industrial

- {/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */} -
- +
+

+ © 2026 GPI - Sistema de Gestão Industrial +

- -
- - © 2026 GPI - Eficiência Industrial -
-
+
); }; + +export default Login; diff --git a/src/client/pages/Register.tsx b/src/client/pages/Register.tsx new file mode 100644 index 0000000..799515f --- /dev/null +++ b/src/client/pages/Register.tsx @@ -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(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 ( +
+ +
+
+

GPI

+

Novo Cadastro de Usuário

+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + 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 + /> +
+ +
+ + 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 + /> +
+ +
+ + 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 + /> +
+ + +
+ +
+

+ Já tem uma conta? Voltar para o login +

+
+
+
+
+ ); +}; + +export default Register; diff --git a/src/client/services/api.ts b/src/client/services/api.ts index 7b61baa..7ab0ce5 100644 --- a/src/client/services/api.ts +++ b/src/client/services/api.ts @@ -41,23 +41,17 @@ export const setApiOrgId = (orgId: string | null) => { // Alias for consistency 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( (config) => { - console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, { - clerkId: currentClerkUserId, - orgId: currentOrgId - }); - if (currentClerkUserId) { - config.headers['x-clerk-user-id'] = currentClerkUserId; + const token = localStorage.getItem('gpi_token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; } + if (currentOrgId) { config.headers['x-organization-id'] = currentOrgId; } - if (currentOrgName) { - // Encode to handle special characters - config.headers['x-organization-name'] = encodeURIComponent(currentOrgName); - } return config; }, (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( (response) => response, (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) { - // Check if it's a guest permission error const errorMessage = error.response?.data?.error || ''; if (errorMessage.includes('Convidados') || errorMessage.includes('guest') || errorMessage.includes('permissão')) { triggerGuestWarning(); diff --git a/src/client/services/systemSettingsService.ts b/src/client/services/systemSettingsService.ts index c7e18b0..60353ee 100644 --- a/src/client/services/systemSettingsService.ts +++ b/src/client/services/systemSettingsService.ts @@ -51,7 +51,7 @@ export const systemSettingsService = { export interface GlobalUser { _id: string; - clerkId: string; + id: string; name: string; email: string; role: string; @@ -66,10 +66,10 @@ export interface GlobalOrganization { isBanned: boolean; name?: string; // Added members: { + id: string; name: string; email: string; role: 'admin' | 'user' | 'guest'; - clerkUserId: string; isBanned: boolean; }[]; } diff --git a/src/server/app.ts b/src/server/app.ts index 7a2016c..5501481 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -16,29 +16,24 @@ import stockRoutes from './routes/stockRoutes.js'; import notificationRoutes from './routes/notificationRoutes.js'; import instrumentRoutes from './routes/instrumentRoutes.js'; import messageRoutes from './routes/messageRoutes.js'; +import authRoutes from './routes/authRoutes.js'; import backupRoutes from './routes/backupRoutes.js'; import path from 'path'; +import fs from 'fs'; +import { authenticateJWT } from './middleware/authMiddleware.js'; const app = express(); app.use(cors({ origin: '*', // Be more specific in production 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()); -import { extractUser } from './middleware/roleMiddleware.js'; - -// 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); +// JWT Authentication Middleware +app.use(authenticateJWT); // Static Uploads -import fs from 'fs'; const uploadsPath = path.join(process.cwd(), 'uploads'); // Ensure uploads directory exists @@ -49,6 +44,7 @@ if (!fs.existsSync(uploadsPath)) { app.use('/uploads', express.static(uploadsPath)); // Routes +app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); app.use('/api/projects', projectRoutes); app.use('/api/parts', partRoutes); diff --git a/src/server/controllers/applicationRecordController.ts b/src/server/controllers/applicationRecordController.ts index 5898005..2a56012 100644 --- a/src/server/controllers/applicationRecordController.ts +++ b/src/server/controllers/applicationRecordController.ts @@ -1,11 +1,11 @@ -import { Request, Response } from 'express'; +import { Response } from 'express'; 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 { 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 }); res.status(201).json(record); } 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 { const { projectId } = req.params; 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 { 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 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 { 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 isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com'; diff --git a/src/server/controllers/authController.ts b/src/server/controllers/authController.ts new file mode 100644 index 0000000..31898b6 --- /dev/null +++ b/src/server/controllers/authController.ts @@ -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' }); + } +}; diff --git a/src/server/controllers/inspectionController.ts b/src/server/controllers/inspectionController.ts index 7683d6e..7bee7d1 100644 --- a/src/server/controllers/inspectionController.ts +++ b/src/server/controllers/inspectionController.ts @@ -6,7 +6,7 @@ import '../middleware/roleMiddleware.js'; // Ensure type augmentation export const createInspection = async (req: Request, res: Response) => { try { const organizationId = req.appUser?.organizationId; - const createdBy = req.appUser?.clerkId; + const createdBy = req.appUser?._id?.toString(); const inspection = await inspectionService.createInspection({ ...req.body, organizationId, @@ -46,7 +46,7 @@ export const getInspectionsByProject = async (req: Request, res: Response) => { export const updateInspection = async (req: Request, res: Response) => { try { 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 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) => { try { 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 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 { if (!req.file) { return res.status(400).json({ error: 'No file uploaded' }); diff --git a/src/server/controllers/messageController.ts b/src/server/controllers/messageController.ts index cec2795..794ecbc 100644 --- a/src/server/controllers/messageController.ts +++ b/src/server/controllers/messageController.ts @@ -1,12 +1,13 @@ -import { Request, Response } from 'express'; +import { Response } from 'express'; import Message from '../models/Message.js'; import OrganizationMember from '../models/OrganizationMember.js'; +import { AuthRequest } from '../middleware/authMiddleware.js'; // Send a message -export const sendMessage = async (req: Request, res: Response) => { +export const sendMessage = async (req: AuthRequest, res: Response) => { try { 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; if (!organizationId) { @@ -58,9 +59,9 @@ export const sendMessage = async (req: Request, res: Response) => { }; // Get unread messages for current user -export const getUnreadMessages = async (req: Request, res: Response) => { +export const getUnreadMessages = async (req: AuthRequest, res: Response) => { try { - const toUserId = req.appUser?.clerkId; + const toUserId = req.appUser?._id?.toString(); const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { @@ -82,7 +83,7 @@ export const getUnreadMessages = async (req: Request, res: Response) => { // Populate sender info const messagesWithSender = await Promise.all( messages.map(async (msg) => { - const sender = await OrganizationMember.findOne({ clerkUserId: msg.fromUserId }); + const sender = await OrganizationMember.findOne({ userId: msg.fromUserId }); return { ...msg.toObject(), 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 -export const markMessageAsRead = async (req: Request, res: Response) => { +export const markMessageAsRead = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const userId = req.appUser?.clerkId; + const userId = req.appUser?._id?.toString(); const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { @@ -134,9 +135,9 @@ export const markMessageAsRead = async (req: Request, res: Response) => { }; // Get my pending (unread) sent messages -export const getMyPendingMessages = async (req: Request, res: Response) => { +export const getMyPendingMessages = async (req: AuthRequest, res: Response) => { try { - const fromUserId = req.appUser?.clerkId; + const fromUserId = req.appUser?._id?.toString(); const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { @@ -156,7 +157,7 @@ export const getMyPendingMessages = async (req: Request, res: Response) => { // Populate recipient info const messagesWithRecipient = await Promise.all( messages.map(async (msg) => { - const recipient = await OrganizationMember.findOne({ clerkUserId: msg.toUserId }); + const recipient = await OrganizationMember.findOne({ userId: msg.toUserId }); return { ...msg.toObject(), 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) -export const deleteMessage = async (req: Request, res: Response) => { +export const deleteMessage = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; - const userId = req.appUser?.clerkId; + const userId = req.appUser?._id?.toString(); const organizationId = req.headers['x-organization-id'] as string; if (!organizationId) { @@ -206,10 +207,10 @@ export const deleteMessage = async (req: Request, res: Response) => { }; // Recipient deletes/archives a message -export const archiveMessage = async (req: Request, res: Response) => { +export const archiveMessage = async (req: AuthRequest, res: Response) => { try { 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 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 { 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 message = await Message.findOne({ _id: id, toUserId: userId, organizationId }); diff --git a/src/server/controllers/notificationController.ts b/src/server/controllers/notificationController.ts index 4c066df..6060af1 100644 --- a/src/server/controllers/notificationController.ts +++ b/src/server/controllers/notificationController.ts @@ -1,14 +1,12 @@ import { Request, Response } from 'express'; import { notificationService } from '../services/notificationService.js'; +import { AuthRequest } from '../middleware/authMiddleware.js'; export const notificationController = { - getUserNotifications: async (req: Request, res: Response) => { + getUserNotifications: async (req: AuthRequest, res: Response) => { try { 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) - - // 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. + const userId = req.appUser?._id?.toString() || ''; if (!organizationId) { 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 { const { id } = req.params; 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 { 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) { 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 { 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) { 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 { 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); res.json(notification); } catch (error) { @@ -83,10 +81,10 @@ export const notificationController = { } }, - delete: async (req: Request, res: Response) => { + delete: async (req: AuthRequest, res: Response) => { try { 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); res.json({ success: true }); } catch (error) { diff --git a/src/server/controllers/stockController.ts b/src/server/controllers/stockController.ts index 7109802..f835681 100644 --- a/src/server/controllers/stockController.ts +++ b/src/server/controllers/stockController.ts @@ -67,7 +67,7 @@ export const createStockItem = async (req: AuthRequest, res: Response) => { const newItem = new StockItem({ organizationId, - createdBy: req.appUser?.clerkId, + createdBy: req.appUser?._id, dataSheetId, rrNumber, batchNumber, @@ -86,7 +86,7 @@ export const createStockItem = async (req: AuthRequest, res: Response) => { // Create Initial Movement (ENTRY) await StockMovement.create({ organizationId, - createdBy: req.appUser?.clerkId, + createdBy: req.appUser?._id, stockItemId: savedItem._id, movementNumber: 1, type: 'ENTRY', @@ -195,7 +195,7 @@ export const adjustStock = async (req: AuthRequest, res: Response) => { // Register Movement await StockMovement.create({ organizationId, - createdBy: req.appUser?.clerkId, + createdBy: req.appUser?._id, stockItemId: item._id, movementNumber, type: 'ADJUSTMENT', @@ -241,7 +241,7 @@ export const consumeStock = async (req: AuthRequest, res: Response) => { // Register Movement (Negative quantity for consumption) await StockMovement.create({ organizationId, - createdBy: req.appUser?.clerkId, + createdBy: req.appUser?._id, stockItemId: item._id, movementNumber, type: 'CONSUMPTION', @@ -348,7 +348,7 @@ export const updateStockMovement = async (req: AuthRequest, res: Response) => { const { id } = req.params; // Movement ID const organizationId = req.appUser?.organizationId; 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'; if (!isAdmin) { @@ -431,7 +431,7 @@ export const deleteStockMovement = async (req: AuthRequest, res: Response) => { const { id } = req.params; const organizationId = req.appUser?.organizationId; 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'; if (!isAdmin) { diff --git a/src/server/controllers/systemSettingsController.ts b/src/server/controllers/systemSettingsController.ts index f92efbf..9dec403 100644 --- a/src/server/controllers/systemSettingsController.ts +++ b/src/server/controllers/systemSettingsController.ts @@ -1,4 +1,4 @@ -import { Request, Response } from 'express'; +import { Response } from 'express'; import SystemSettings from '../models/SystemSettings.js'; import User from '../models/User.js'; import OrganizationMember from '../models/OrganizationMember.js'; @@ -6,8 +6,9 @@ import Organization from '../models/Organization.js'; import path from 'path'; import fs from 'fs'; 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 { 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 { 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 { 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 { if (!req.file) { return res.status(400).json({ error: 'Nenhum arquivo enviado.' }); @@ -92,7 +93,7 @@ export const uploadLogo = async (req: Request, res: Response) => { }; // Global Admin Functions -export const getGlobalUsers = async (req: Request, res: Response) => { +export const getGlobalUsers = async (req: AuthRequest, res: Response) => { try { const users = await User.find({}).sort({ createdAt: -1 }); 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 { // Aggregate members to group by org and get full member lists const organizations = await OrganizationMember.aggregate([ @@ -111,10 +112,10 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => { _id: '$organizationId', members: { $push: { + id: '$userId', name: '$name', email: '$email', role: '$role', - clerkUserId: '$clerkUserId', isBanned: '$isBanned' } }, @@ -125,7 +126,7 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => { $lookup: { from: 'organizations', // Ensure this matches the collection name of Organization model localField: '_id', - foreignField: 'clerkId', + foreignField: 'organizationId', // We should rename clerkId in Organization model too 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 { const { organizationId, isBanned } = req.body; @@ -165,7 +166,7 @@ export const toggleOrganizationBan = async (req: Request, res: Response) => { // Upsert the Organization record const org = await Organization.findOneAndUpdate( - { clerkId: organizationId }, + { organizationId: organizationId }, { isBanned: isBanned }, { new: true, upsert: true } ); diff --git a/src/server/controllers/userController.ts b/src/server/controllers/userController.ts index b62cb7e..cb3e204 100644 --- a/src/server/controllers/userController.ts +++ b/src/server/controllers/userController.ts @@ -1,101 +1,9 @@ import { Request, Response } from 'express'; import User, { IUser } from '../models/User.js'; import OrganizationMember, { OrgRole } from '../models/OrganizationMember.js'; +import { AuthRequest } from '../middleware/authMiddleware.js'; -// Define locally to avoid import cycle risks -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 - */ +// Get current user data with organization context export const getCurrentUser = async (req: AuthRequest, res: Response) => { try { if (!req.appUser) { @@ -106,7 +14,7 @@ export const getCurrentUser = async (req: AuthRequest, res: Response) => { if (organizationId) { const member = await OrganizationMember.findOne({ - clerkUserId: req.appUser.clerkId, + userId: req.appUser._id.toString(), 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) => { try { const organizationId = req.headers['x-organization-id'] as string; - console.log('getAllUsers called with organizationId:', organizationId); - if (!organizationId) { return res.status(400).json({ error: 'Organização não selecionada.' }); } const members = await OrganizationMember.find({ organizationId }).sort({ createdAt: -1 }); - console.log(`Found ${members.length} members for org ${organizationId}:`, members.map(m => ({ name: m.name, email: m.email, clerkId: m.clerkUserId }))); res.json(members); } catch (error) { 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) => { try { const { id } = req.params; @@ -184,14 +85,12 @@ export const updateUserRole = async (req: AuthRequest, res: Response) => { res.json(member); } catch (error) { - console.error('Error toggling ban:', error); - res.status(500).json({ error: 'Erro ao alterar status de banimento.' }); + console.error('Error updating role:', error); + 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) => { try { const { id } = req.params; @@ -208,12 +107,12 @@ export const toggleBanUser = async (req: AuthRequest, res: Response) => { } // 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.' }); } // 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.' }); } @@ -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) => { try { if (!req.appUser) { return res.status(401).json({ error: 'Não autenticado.' }); } - // Update User model await User.findByIdAndUpdate(req.appUser._id, { lastSeenAt: new Date() }); - - // Also update Organization Member for tighter query - // But for now User model is enough if we join correctly, or just use User model for presence. - // Actually, since we want to show users per organization, we should filter by Org. - // Our 'User.ts' has organizationId, but it might be just the 'default' one. - // Let's rely on OrganizationMember for the list, but we need to update lastSeenAt there too? - // Strategy: Update User (global), and when querying active users, join or filter. - // Better: Update OrganizationMember too if we want org-specific presence? - // Simpler: Just update User. When fetching active users, we fetch OrganizationMembers and populate User details, filtering by User.lastSeenAt. - res.status(200).send(); } catch (error) { - // Silent fail for heartbeat console.error('Heartbeat error:', error); res.status(500).send(); } }; -/** - * Get active users in the same organization (seen in last 2 mins) - */ +// Get active users in the same organization (seen in last 2 mins) export const getActiveUsers = async (req: AuthRequest, res: Response) => { try { const organizationId = req.headers['x-organization-id'] as string; @@ -268,20 +151,15 @@ export const getActiveUsers = async (req: AuthRequest, res: Response) => { return res.status(400).json([]); } - // Find members of this org 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 activeUsers = await User.find({ - clerkId: { $in: clerkIds }, - lastSeenAt: { $gte: twoMinutesAgo }, - _id: { $ne: currentUserId } // Optional: exclude self - }).select('name email lastSeenAt clerkId'); // Only needed fields + _id: { $in: userIds, $ne: currentUserId }, + lastSeenAt: { $gte: twoMinutesAgo } + }).select('name email lastSeenAt'); res.json(activeUsers); } 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.' }); } - console.log(`Deleting member ${id} from organization ${organizationId}`); - - // Delete from OrganizationMember collection - const result = await OrganizationMember.findByIdAndDelete(id); + const result = await OrganizationMember.findOneAndDelete({ _id: id, organizationId }); if (!result) { return res.status(404).json({ error: 'Membro não encontrado.' }); } - console.log(`Member ${result.name} deleted successfully`); - res.json({ message: 'Membro removido com sucesso.', deletedMember: result }); } catch (error) { console.error('Error deleting user:', error); 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.' }); +}; diff --git a/src/server/middleware/authMiddleware.ts b/src/server/middleware/authMiddleware.ts new file mode 100644 index 0000000..331d1aa --- /dev/null +++ b/src/server/middleware/authMiddleware.ts @@ -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' }); + } +}; diff --git a/src/server/middleware/roleMiddleware.ts b/src/server/middleware/roleMiddleware.ts index 957b9ae..501557d 100644 --- a/src/server/middleware/roleMiddleware.ts +++ b/src/server/middleware/roleMiddleware.ts @@ -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 * @param allowedRoles Array of roles that can access the route @@ -119,6 +32,7 @@ export const requireRole = (allowedRoles: OrgRole[]) => { return next(); } + // Fallback to global role if organizationRole is not set const effectiveRole = req.appUser.organizationRole || req.appUser.role; if (!allowedRoles.includes(effectiveRole as OrgRole)) { diff --git a/src/server/models/Message.ts b/src/server/models/Message.ts index 0dd8324..3362df2 100644 --- a/src/server/models/Message.ts +++ b/src/server/models/Message.ts @@ -2,8 +2,8 @@ import mongoose, { Schema, Document } from 'mongoose'; export interface IMessage extends Document { organizationId: string; - fromUserId: string; // clerkId do remetente - toUserId: string; // clerkId do destinatário + fromUserId: string; // ID do remetente + toUserId: string; // ID do destinatário message: string; isRead: boolean; readAt?: Date; diff --git a/src/server/models/Organization.ts b/src/server/models/Organization.ts index 6e83f02..c06ebfa 100644 --- a/src/server/models/Organization.ts +++ b/src/server/models/Organization.ts @@ -1,7 +1,7 @@ import mongoose, { Schema, Document } from 'mongoose'; export interface IOrganization extends Document { - clerkId: string; + organizationId: string; name?: string; isBanned: boolean; createdAt: Date; @@ -9,7 +9,7 @@ export interface IOrganization extends Document { } 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 }, isBanned: { type: Boolean, default: false }, }, { timestamps: true }); diff --git a/src/server/models/OrganizationMember.ts b/src/server/models/OrganizationMember.ts index 04565d6..7742d03 100644 --- a/src/server/models/OrganizationMember.ts +++ b/src/server/models/OrganizationMember.ts @@ -3,7 +3,7 @@ import mongoose, { Schema, Document } from 'mongoose'; export type OrgRole = 'guest' | 'user' | 'admin'; export interface IOrganizationMember extends Document { - clerkUserId: string; + userId: string; organizationId: string; role: OrgRole; isBanned: boolean; @@ -15,7 +15,7 @@ export interface IOrganizationMember extends Document { } const OrganizationMemberSchema: Schema = new Schema({ - clerkUserId: { + userId: { type: String, required: true, index: true @@ -47,6 +47,6 @@ const OrganizationMemberSchema: Schema = new Schema({ }); // 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('OrganizationMember', OrganizationMemberSchema); diff --git a/src/server/models/User.ts b/src/server/models/User.ts index f3d5663..fd088db 100644 --- a/src/server/models/User.ts +++ b/src/server/models/User.ts @@ -3,8 +3,9 @@ import mongoose, { Schema, Document } from 'mongoose'; export type UserRole = 'guest' | 'user' | 'admin'; export interface IUser extends Document { - clerkId: string; + clerkId?: string; email: string; + password?: string; name: string; role: UserRole; isBanned: boolean; @@ -17,17 +18,23 @@ export interface IUser extends Document { const UserSchema: Schema = new Schema({ clerkId: { type: String, - required: true, unique: true, + sparse: true, index: true }, + password: { + type: String, + select: false // Password shouldn't be returned by default + }, organizationId: { type: String, index: true }, email: { type: String, - required: true + required: true, + unique: true, + index: true }, name: { type: String, diff --git a/src/server/routes/authRoutes.ts b/src/server/routes/authRoutes.ts new file mode 100644 index 0000000..6183402 --- /dev/null +++ b/src/server/routes/authRoutes.ts @@ -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; diff --git a/src/server/routes/userRoutes.ts b/src/server/routes/userRoutes.ts index 5486b0e..543f789 100644 --- a/src/server/routes/userRoutes.ts +++ b/src/server/routes/userRoutes.ts @@ -1,23 +1,23 @@ import express from 'express'; 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(); -// Sync user from Clerk (public - called on login) +// Sync user (placeholder) router.post('/sync', syncUser); -// Get current user (requires extractUser middleware) -router.get('/me', extractUser, getCurrentUser); +// Get current user +router.get('/me', requireUser, getCurrentUser); // Heartbeat & Presence -router.post('/heartbeat', extractUser, heartbeat); -router.get('/active', extractUser, getActiveUsers); +router.post('/heartbeat', requireUser, heartbeat); +router.get('/active', requireUser, getActiveUsers); // Admin-only routes -router.get('/', extractUser, requireAdmin, getAllUsers); -router.patch('/:id/role', extractUser, requireAdmin, updateUserRole); -router.patch('/:id/ban', extractUser, requireAdmin, toggleBanUser); -router.delete('/:id', extractUser, requireAdmin, deleteUser); +router.get('/', requireUser, requireAdmin, getAllUsers); +router.patch('/:id/role', requireUser, requireAdmin, updateUserRole); +router.patch('/:id/ban', requireUser, requireAdmin, toggleBanUser); +router.delete('/:id', requireUser, requireAdmin, deleteUser); export default router; diff --git a/src/server/scripts/migrate-admin.ts b/src/server/scripts/migrate-admin.ts new file mode 100644 index 0000000..682dd2c --- /dev/null +++ b/src/server/scripts/migrate-admin.ts @@ -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();