Compare commits
2 Commits
c6f69e1c1d
...
e126b0d3f9
| Author | SHA1 | Date | |
|---|---|---|---|
| e126b0d3f9 | |||
| 286867739d |
@@ -12,6 +12,7 @@ import userRoutes from '../src/server/routes/userRoutes.js';
|
|||||||
import systemSettingsRoutes from '../src/server/routes/systemSettingsRoutes.js';
|
import systemSettingsRoutes from '../src/server/routes/systemSettingsRoutes.js';
|
||||||
import geometryTypeRoutes from '../src/server/routes/geometryTypeRoutes.js';
|
import geometryTypeRoutes from '../src/server/routes/geometryTypeRoutes.js';
|
||||||
import stockRoutes from '../src/server/routes/stockRoutes.js';
|
import stockRoutes from '../src/server/routes/stockRoutes.js';
|
||||||
|
import authRoutes from '../src/server/routes/authRoutes.js';
|
||||||
import notificationRoutes from '../src/server/routes/notificationRoutes.js';
|
import notificationRoutes from '../src/server/routes/notificationRoutes.js';
|
||||||
import instrumentRoutes from '../src/server/routes/instrumentRoutes.js';
|
import instrumentRoutes from '../src/server/routes/instrumentRoutes.js';
|
||||||
import { extractUser } from '../src/server/middleware/roleMiddleware.js';
|
import { extractUser } from '../src/server/middleware/roleMiddleware.js';
|
||||||
@@ -22,17 +23,20 @@ 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', 'x-organization-name']
|
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-organization-name']
|
||||||
}));
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Global Middleware
|
// Global Middleware
|
||||||
|
import { authMiddleware } from '../src/server/middleware/auth.js';
|
||||||
|
app.use(authMiddleware);
|
||||||
app.use(extractUser);
|
app.use(extractUser);
|
||||||
|
|
||||||
// Static Uploads
|
// Static Uploads
|
||||||
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
|
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
|
||||||
|
|
||||||
// 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);
|
||||||
|
|||||||
133
package-lock.json
generated
133
package-lock.json
generated
@@ -8,8 +8,6 @@
|
|||||||
"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/mongoose": "^5.11.96",
|
"@types/mongoose": "^5.11.96",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
@@ -41,8 +39,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
@@ -1636,75 +1636,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 +3334,13 @@
|
|||||||
"@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==",
|
||||||
|
"dev": true,
|
||||||
|
"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 +3447,17 @@
|
|||||||
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"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 +3465,13 @@
|
|||||||
"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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/multer": {
|
"node_modules/@types/multer": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3546,7 +3502,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.2.9",
|
"version": "19.2.9",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -3562,7 +3518,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/react/node_modules/csstype": {
|
"node_modules/@types/react/node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
@@ -5516,10 +5472,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",
|
||||||
@@ -6902,10 +6854,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 +7692,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"
|
||||||
@@ -10120,10 +10061,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 +10270,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",
|
||||||
@@ -10640,6 +10566,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
|
"devOptional": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/tsx": {
|
"node_modules/tsx": {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
"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/mongoose": "^5.11.96",
|
"@types/mongoose": "^5.11.96",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
@@ -46,8 +44,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
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, useAuth } from './context/AuthContext';
|
||||||
import { AuthProvider } from './context/AuthContext';
|
|
||||||
import { useAuth } from './context/useAuth';
|
|
||||||
import { SystemSettingsProvider } from './context/SystemSettingsContext';
|
import { SystemSettingsProvider } from './context/SystemSettingsContext';
|
||||||
import { NotificationProvider } from './contexts/NotificationContext';
|
import { NotificationProvider } from './contexts/NotificationContext';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
@@ -32,90 +30,98 @@ const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
const { organization } = useOrganization();
|
const { appUser, isLoading } = useAuth();
|
||||||
|
|
||||||
console.log('AppContent rendered');
|
if (isLoading) return <div className="flex h-screen items-center justify-center">Carregando...</div>;
|
||||||
console.log('Current organization:', organization);
|
|
||||||
|
|
||||||
// If user is signed in but has no organization, show org selector
|
// AppUser exists but hasn't selected an org yet (if your business logic requires orgs)
|
||||||
if (!organization) {
|
if (appUser && !appUser.organizationId) {
|
||||||
console.log('No organization - showing OrganizationSelector');
|
|
||||||
return <OrganizationSelector />;
|
return <OrganizationSelector />;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Organization exists - showing main app');
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<Layout>
|
||||||
<AuthProvider>
|
<Routes>
|
||||||
<SystemSettingsProvider>
|
<Route path="/" element={<ProjectList />} />
|
||||||
<NotificationProvider>
|
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
||||||
<Layout>
|
<Route path="/projects" element={<ProjectList />} />
|
||||||
<Routes>
|
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||||
<Route path="/" element={<ProjectList />} />
|
<Route path="/schemes" element={<SchemesList />} />
|
||||||
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
<Route path="/inspections" element={<InspectionsList />} />
|
||||||
<Route path="/projects" element={<ProjectList />} />
|
<Route path="/library" element={
|
||||||
<Route path="/project/:id" element={<ProjectDetails />} />
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
<Route path="/schemes" element={<SchemesList />} />
|
<DataSheetLibrary />
|
||||||
<Route path="/inspections" element={<InspectionsList />} />
|
</ProtectedRoute>
|
||||||
<Route path="/library" element={
|
} />
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<Route path="/instruments" element={
|
||||||
<DataSheetLibrary />
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
</ProtectedRoute>
|
<InstrumentList />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/instruments" element={
|
} />
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
<Route path="/yield-study" element={
|
||||||
<InstrumentList />
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
</ProtectedRoute>
|
<YieldStudyDashboard />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/yield-study" element={
|
} />
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<Route path="/calculators" element={<CalculatorDashboard />} />
|
||||||
<YieldStudyDashboard />
|
<Route
|
||||||
</ProtectedRoute>
|
path="/admin"
|
||||||
} />
|
element={
|
||||||
<Route path="/calculators" element={<CalculatorDashboard />} />
|
<ProtectedRoute allowedRoles={['admin']}>
|
||||||
<Route
|
<AdminDashboard />
|
||||||
path="/admin"
|
</ProtectedRoute>
|
||||||
element={
|
}
|
||||||
<ProtectedRoute allowedRoles={['admin']}>
|
/>
|
||||||
<AdminDashboard />
|
<Route
|
||||||
</ProtectedRoute>
|
path="/stock"
|
||||||
}
|
element={
|
||||||
/>
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
<Route
|
<StockDashboard />
|
||||||
path="/stock"
|
</ProtectedRoute>
|
||||||
element={
|
}
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
/>
|
||||||
<StockDashboard />
|
<Route
|
||||||
</ProtectedRoute>
|
path="/developer"
|
||||||
}
|
element={
|
||||||
/>
|
<DeveloperRoute>
|
||||||
<Route
|
<DeveloperDashboard />
|
||||||
path="/developer"
|
</DeveloperRoute>
|
||||||
element={
|
}
|
||||||
<DeveloperRoute>
|
/>
|
||||||
<DeveloperDashboard />
|
</Routes>
|
||||||
</DeveloperRoute>
|
</Layout>
|
||||||
}
|
);
|
||||||
/>
|
};
|
||||||
</Routes>
|
|
||||||
</Layout>
|
const MainRouter: React.FC = () => {
|
||||||
</NotificationProvider>
|
const { appUser, isLoading } = useAuth();
|
||||||
</SystemSettingsProvider>
|
|
||||||
</AuthProvider>
|
if (isLoading) {
|
||||||
</ToastProvider>
|
return <div className="flex h-screen items-center justify-center">Verificando sessão...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
{!appUser ? (
|
||||||
|
<Login />
|
||||||
|
) : (
|
||||||
|
<AppContent />
|
||||||
|
)}
|
||||||
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<ToastProvider>
|
||||||
<SignedOut>
|
<AuthProvider>
|
||||||
<Login />
|
<SystemSettingsProvider>
|
||||||
</SignedOut>
|
<NotificationProvider>
|
||||||
<SignedIn>
|
<MainRouter />
|
||||||
<AppContent />
|
</NotificationProvider>
|
||||||
</SignedIn>
|
</SystemSettingsProvider>
|
||||||
</Router>
|
</AuthProvider>
|
||||||
|
</ToastProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,9 @@ 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 } 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 { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
|
import { TechnicalManual } from './TechnicalManual';
|
||||||
// import { useSystemSettings } from '../context/SystemSettingsContext';
|
// import { useSystemSettings } from '../context/SystemSettingsContext';
|
||||||
import { setApiOrgData } from '../services/api';
|
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -22,22 +19,9 @@ 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
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (organization?.id) {
|
|
||||||
|
|
||||||
setApiOrgData(organization.id, organization.name);
|
|
||||||
} else {
|
|
||||||
setApiOrgData(null);
|
|
||||||
}
|
|
||||||
}, [organization]);
|
|
||||||
|
|
||||||
// Helper to get role display name
|
// Helper to get role display name
|
||||||
const getRoleDisplay = () => {
|
const getRoleDisplay = () => {
|
||||||
switch (appUser?.role) {
|
switch (appUser?.role) {
|
||||||
@@ -118,44 +102,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="Organização">
|
||||||
<OrganizationSwitcher
|
<div className="w-8 h-8 rounded-lg bg-surface-soft flex items-center justify-center font-bold text-xs">
|
||||||
hidePersonal={true}
|
ORG
|
||||||
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">Organização Matriz</p>
|
||||||
|
<p className="text-[10px] text-text-muted uppercase tracking-wider">Conta</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 +212,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,9 +223,11 @@ 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 bg-primary text-white rounded-full flex items-center justify-center font-bold text-xs">
|
||||||
|
{appUser?.name?.substring(0, 2).toUpperCase() || 'US'}
|
||||||
|
</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">v2.1.0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,142 +1,108 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { createContext, useContext, 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 { setApiToken, setApiOrganizationId, getBaseUrl } from '../services/api';
|
||||||
import { setApiClerkUserId, setApiOrganizationId, getBaseUrl } from '../services/api';
|
|
||||||
|
|
||||||
const API_URL = getBaseUrl();
|
const API_URL = getBaseUrl();
|
||||||
|
|
||||||
interface AuthProviderProps {
|
export interface AuthContextType {
|
||||||
children: React.ReactNode;
|
appUser: AppUser | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
token: string | null;
|
||||||
|
login: (token: string, user: AppUser) => void;
|
||||||
|
logout: () => void;
|
||||||
|
isAdmin: () => boolean;
|
||||||
|
isUser: () => boolean;
|
||||||
|
isGuest: () => boolean;
|
||||||
|
isDeveloper: () => boolean;
|
||||||
|
canEdit: () => boolean;
|
||||||
|
refetchUser: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
const { user, isLoaded } = useUser();
|
|
||||||
const { organization, membership } = useOrganization();
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||||
|
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
|
||||||
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
|
// Initial load: se tem token, setar no interceptor e buscar dados do usuário
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setApiClerkUserId(user?.id || null);
|
if (token) {
|
||||||
setApiOrganizationId(organization?.id || null);
|
setApiToken(token);
|
||||||
}, [user?.id, organization?.id]);
|
refetchUser();
|
||||||
|
} else {
|
||||||
const syncUser = useCallback(async () => {
|
|
||||||
if (!user) {
|
|
||||||
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';
|
|
||||||
setAppUser({
|
|
||||||
...syncedUser,
|
|
||||||
id: syncedUser._id || syncedUser.id,
|
|
||||||
role: effectiveRole, // Override with organization-specific role
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update last context ref
|
|
||||||
lastContextRef.current = { clerkId: user.id, orgId: organization?.id || null };
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error syncing user:', err);
|
|
||||||
setError('Erro ao carregar dados do usuário');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, organization?.id, membership?.role]);
|
}, [token]);
|
||||||
|
|
||||||
|
const login = useCallback((newToken: string, user: AppUser) => {
|
||||||
|
localStorage.setItem('jwt_token', newToken);
|
||||||
|
setToken(newToken);
|
||||||
|
setAppUser(user);
|
||||||
|
setApiToken(newToken);
|
||||||
|
|
||||||
|
// Se a organização existir, setar o header
|
||||||
|
if (user.organizationId) {
|
||||||
|
setApiOrganizationId(user.organizationId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
localStorage.removeItem('jwt_token');
|
||||||
|
setToken(null);
|
||||||
|
setAppUser(null);
|
||||||
|
setApiToken(null);
|
||||||
|
setApiOrganizationId(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const refetchUser = useCallback(async () => {
|
const refetchUser = useCallback(async () => {
|
||||||
if (!user) return;
|
if (!token) return;
|
||||||
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_URL}/users/me`, {
|
const response = await fetch(`${API_URL}/auth/me`, {
|
||||||
headers: {
|
headers: {
|
||||||
'x-clerk-user-id': user.id,
|
'Authorization': `Bearer ${token}`
|
||||||
...(organization?.id && { 'x-organization-id': organization.id }),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const userData = await response.json();
|
const userData = await response.json();
|
||||||
const effectiveRole = userData.organizationRole || userData.role || 'guest';
|
setAppUser(userData);
|
||||||
setAppUser({
|
if (userData.organizationId) {
|
||||||
...userData,
|
setApiOrganizationId(userData.organizationId);
|
||||||
id: userData._id || userData.id,
|
}
|
||||||
role: effectiveRole,
|
} else {
|
||||||
});
|
// Token inválido ou expirado
|
||||||
|
logout();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error refetching user:', err);
|
console.error('Error refetching user:', err);
|
||||||
|
setError('Falha na comunicação de autenticação.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [user, organization?.id]);
|
}, [token, logout]);
|
||||||
|
|
||||||
// Re-sync when organization changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoaded && user) {
|
|
||||||
syncUser();
|
|
||||||
}
|
|
||||||
}, [isLoaded, user, organization?.id, syncUser]);
|
|
||||||
|
|
||||||
const isDeveloper = useCallback(() => {
|
const isDeveloper = useCallback(() => {
|
||||||
return user?.primaryEmailAddress?.emailAddress === 'admtracksteel@gmail.com';
|
return appUser?.email === 'admtracksteel@gmail.com';
|
||||||
}, [user]);
|
}, [appUser]);
|
||||||
|
|
||||||
const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]);
|
const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]);
|
||||||
const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]);
|
const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]);
|
||||||
const isGuest = useCallback(() => appUser?.role === 'guest' && !isDeveloper(), [appUser, isDeveloper]);
|
const isGuest = useCallback(() => appUser?.role === 'guest' && !isDeveloper(), [appUser, isDeveloper]);
|
||||||
const canEdit = useCallback(() => (appUser?.role !== 'guest' && appUser?.role !== undefined) || isDeveloper(), [appUser, isDeveloper]);
|
const canEdit = useCallback(() => (appUser?.role !== 'guest' && appUser !== null) || isDeveloper(), [appUser, isDeveloper]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
value={{
|
value={{
|
||||||
appUser,
|
appUser,
|
||||||
isLoading,
|
isLoading,
|
||||||
isSignedIn: !!user,
|
|
||||||
error,
|
error,
|
||||||
|
token,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isUser,
|
isUser,
|
||||||
isGuest,
|
isGuest,
|
||||||
@@ -149,3 +115,11 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,47 +1,7 @@
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
import { ClerkProvider } from '@clerk/clerk-react'
|
import App from './App.tsx';
|
||||||
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(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<ClerkProvider
|
<App />
|
||||||
publishableKey={PUBLISHABLE_KEY}
|
);
|
||||||
afterSignOutUrl="/"
|
|
||||||
localization={ptBR}
|
|
||||||
appearance={{
|
|
||||||
variables: {
|
|
||||||
colorPrimary: '#fb923c', // Cor primária do GPI (Laranja)
|
|
||||||
colorBackground: '#ffffff',
|
|
||||||
colorText: '#1c1917',
|
|
||||||
colorTextSecondary: '#57534e',
|
|
||||||
borderRadius: '0.75rem',
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
card: "shadow-none border-0 bg-transparent", // Deixamos o container da página controlar o card
|
|
||||||
navbar: "hidden",
|
|
||||||
headerTitle: "text-2xl font-bold tracking-tight",
|
|
||||||
headerSubtitle: "text-text-muted font-medium",
|
|
||||||
formButtonPrimary: "bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20",
|
|
||||||
socialButtonsBlockButton: "bg-white hover:bg-surface-hover border-border/40 text-text-main font-semibold transition-all duration-300 rounded-xl",
|
|
||||||
footerActionLink: "text-primary hover:text-primary/80 font-bold",
|
|
||||||
formFieldInput: "bg-surface-soft border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl",
|
|
||||||
organizationSwitcherTrigger: "hover:bg-surface-hover transition-colors rounded-xl",
|
|
||||||
organizationPreviewMainIdentifier: "font-bold",
|
|
||||||
// Personalização específica para a lista de organizações que aparece na imagem
|
|
||||||
organizationListPreview: "hover:bg-surface-soft rounded-xl transition-all p-3",
|
|
||||||
organizationListCreateOrganizationButton: "text-primary font-bold hover:text-primary/80",
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<App />
|
|
||||||
</ClerkProvider>,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,45 @@
|
|||||||
import { SignIn } from "@clerk/clerk-react";
|
import React, { useState } from "react";
|
||||||
import { Hammer } from "lucide-react";
|
import { Hammer } from "lucide-react";
|
||||||
|
import { useAuth } from "../context/useAuth";
|
||||||
|
import { getBaseUrl } from "../services/api";
|
||||||
|
|
||||||
|
const API_URL = getBaseUrl();
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setErrorMsg("");
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/auth/login`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ email, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setErrorMsg(data.error || "Erro ao efetuar login");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
login(data.token, data.user);
|
||||||
|
} catch (err) {
|
||||||
|
setErrorMsg("Falha na conexão com o servidor.");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden">
|
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft relative overflow-hidden">
|
||||||
{/* Background decorative elements */}
|
{/* Background decorative elements */}
|
||||||
@@ -18,13 +56,53 @@ export const Login = () => {
|
|||||||
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
|
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */}
|
{/* Custom Login Form */}
|
||||||
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-4 animate-in slide-in-from-bottom-8 duration-1000">
|
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-8 animate-in slide-in-from-bottom-8 duration-1000">
|
||||||
<SignIn
|
<h2 className="text-xl font-bold text-text-main mb-6 text-center">Entrar na sua conta</h2>
|
||||||
afterSignInUrl="/"
|
|
||||||
afterSignUpUrl="/"
|
{errorMsg && (
|
||||||
forceRedirectUrl="/"
|
<div className="mb-4 p-3 rounded-lg bg-error/10 border border-error/20 text-error text-sm text-center">
|
||||||
/>
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-semibold text-text-secondary" htmlFor="email">Email</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all"
|
||||||
|
placeholder="seu@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<label className="text-sm font-semibold text-text-secondary" htmlFor="password">Senha</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="mt-4 bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? "Entrando..." : "Entrar"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">
|
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ const api = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store the current user's clerk ID and Organization ID/Name
|
let currentToken: string | null = null;
|
||||||
let currentClerkUserId: string | null = null;
|
|
||||||
let currentOrgId: string | null = null;
|
let currentOrgId: string | null = null;
|
||||||
let currentOrgName: string | null = null;
|
let currentOrgName: string | null = null;
|
||||||
|
|
||||||
// Function to set the clerk user ID (called from AuthContext)
|
// Function to set the JWT token
|
||||||
export const setApiClerkUserId = (clerkId: string | null) => {
|
export const setApiToken = (token: string | null) => {
|
||||||
currentClerkUserId = clerkId;
|
currentToken = token;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to set the organization ID and Name (called from Layout/Context)
|
// Function to set the organization ID and Name (called from Layout/Context)
|
||||||
@@ -45,11 +44,10 @@ export const setApiOrganizationId = setApiOrgId;
|
|||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, {
|
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, {
|
||||||
clerkId: currentClerkUserId,
|
|
||||||
orgId: currentOrgId
|
orgId: currentOrgId
|
||||||
});
|
});
|
||||||
if (currentClerkUserId) {
|
if (currentToken) {
|
||||||
config.headers['x-clerk-user-id'] = currentClerkUserId;
|
config.headers['Authorization'] = `Bearer ${currentToken}`;
|
||||||
}
|
}
|
||||||
if (currentOrgId) {
|
if (currentOrgId) {
|
||||||
config.headers['x-organization-id'] = currentOrgId;
|
config.headers['x-organization-id'] = currentOrgId;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import yieldStudyRoutes from './routes/yieldStudyRoutes.js';
|
|||||||
import userRoutes from './routes/userRoutes.js';
|
import userRoutes from './routes/userRoutes.js';
|
||||||
import systemSettingsRoutes from './routes/systemSettingsRoutes.js';
|
import systemSettingsRoutes from './routes/systemSettingsRoutes.js';
|
||||||
import geometryTypeRoutes from './routes/geometryTypeRoutes.js';
|
import geometryTypeRoutes from './routes/geometryTypeRoutes.js';
|
||||||
|
import authRoutes from './routes/authRoutes.js';
|
||||||
|
|
||||||
import stockRoutes from './routes/stockRoutes.js';
|
import stockRoutes from './routes/stockRoutes.js';
|
||||||
import notificationRoutes from './routes/notificationRoutes.js';
|
import notificationRoutes from './routes/notificationRoutes.js';
|
||||||
@@ -24,17 +25,19 @@ 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', 'x-organization-id']
|
||||||
}));
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
import { extractUser } from './middleware/roleMiddleware.js';
|
import { extractUser } from './middleware/roleMiddleware.js';
|
||||||
|
|
||||||
// LOG DE DEPURAÇÃO PARA CONEXÃO
|
// LOG DE DEPURAÇÃO PARA CONEXÃO
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ClerkID: ${req.headers['x-clerk-user-id'] || 'None'}`);
|
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
import { authMiddleware } from './middleware/auth.js';
|
||||||
|
app.use(authMiddleware);
|
||||||
app.use(extractUser);
|
app.use(extractUser);
|
||||||
|
|
||||||
// Static Uploads
|
// Static Uploads
|
||||||
@@ -49,6 +52,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);
|
||||||
|
|||||||
99
src/server/controllers/authController.ts
Normal file
99
src/server/controllers/authController.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import User from '../models/User.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_change_in_prod';
|
||||||
|
|
||||||
|
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { name, email, password } = req.body;
|
||||||
|
|
||||||
|
if (!name || !email || !password) {
|
||||||
|
res.status(400).json({ error: 'Todos os campos são obrigatórios' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await User.findOne({ email });
|
||||||
|
if (existingUser) {
|
||||||
|
res.status(400).json({ error: 'Email já cadastrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const passwordHash = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
// Gere um clerkId falso apenas para manter retrocompatibilidade no banco
|
||||||
|
const fakeClerkId = `user_${uuidv4().replace(/-/g, '')}`;
|
||||||
|
|
||||||
|
const newUser = new User({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
passwordHash,
|
||||||
|
clerkId: fakeClerkId,
|
||||||
|
role: 'member',
|
||||||
|
isBanned: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await newUser.save();
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: newUser._id.toString(), clerkId: newUser.clerkId, role: newUser.role, organizationId: newUser.organizationId },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Usuário criado com sucesso',
|
||||||
|
token,
|
||||||
|
user: { id: newUser._id, name: newUser.name, email: newUser.email, role: newUser.role, clerkId: newUser.clerkId }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register Error:', error);
|
||||||
|
res.status(500).json({ error: 'Erro no servidor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
res.status(400).json({ error: 'Email e senha são obrigatórios' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({ email });
|
||||||
|
if (!user) {
|
||||||
|
res.status(400).json({ error: 'Usuário não encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.passwordHash) {
|
||||||
|
res.status(400).json({ error: 'Usuário do sistema antigo. Por favor, solicite a redefinição de senha ou recrie sua conta se possível.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(password, user.passwordHash);
|
||||||
|
if (!isMatch) {
|
||||||
|
res.status(400).json({ error: 'Credenciais inválidas' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: user._id.toString(), clerkId: user.clerkId, role: user.role, organizationId: user.organizationId },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Login realizado com sucesso',
|
||||||
|
token,
|
||||||
|
user: { id: user._id, name: user.name, email: user.email, role: user.role, clerkId: user.clerkId }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login Error:', error);
|
||||||
|
res.status(500).json({ error: 'Erro no servidor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
26
src/server/middleware/auth.ts
Normal file
26
src/server/middleware/auth.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_change_in_prod';
|
||||||
|
|
||||||
|
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
// Se não houver token autêntico JWT, prossegue limpo
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||||
|
|
||||||
|
// Injeta o clerkId no header para que o extractUser (roleMiddleware)
|
||||||
|
// continue seu trabalho de carregar o usuário do banco instanciado e popular req.appUser
|
||||||
|
req.headers['x-clerk-user-id'] = decoded.clerkId;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth Middleware Error:', error);
|
||||||
|
res.status(401).json({ error: 'Token inválido ou expirado' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ export interface IUser extends Document {
|
|||||||
name: string;
|
name: string;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
isBanned: boolean;
|
isBanned: boolean;
|
||||||
|
passwordHash?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -21,6 +22,10 @@ const UserSchema: Schema = new Schema({
|
|||||||
unique: true,
|
unique: true,
|
||||||
index: true
|
index: true
|
||||||
},
|
},
|
||||||
|
passwordHash: {
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
organizationId: {
|
organizationId: {
|
||||||
type: String,
|
type: String,
|
||||||
index: true
|
index: true
|
||||||
|
|||||||
9
src/server/routes/authRoutes.ts
Normal file
9
src/server/routes/authRoutes.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { login, register } from '../controllers/authController.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/login', login);
|
||||||
|
router.post('/register', register);
|
||||||
|
|
||||||
|
export default router;
|
||||||
Reference in New Issue
Block a user