diff --git a/api/app.ts b/api/app.ts
index 77dc1cf..17a3600 100644
--- a/api/app.ts
+++ b/api/app.ts
@@ -12,6 +12,7 @@ import userRoutes from '../src/server/routes/userRoutes.js';
import systemSettingsRoutes from '../src/server/routes/systemSettingsRoutes.js';
import geometryTypeRoutes from '../src/server/routes/geometryTypeRoutes.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 instrumentRoutes from '../src/server/routes/instrumentRoutes.js';
import { extractUser } from '../src/server/middleware/roleMiddleware.js';
@@ -22,17 +23,20 @@ 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', 'x-organization-name']
+ allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-organization-name']
}));
app.use(express.json());
// Global Middleware
+import { authMiddleware } from '../src/server/middleware/auth.js';
+app.use(authMiddleware);
app.use(extractUser);
// Static Uploads
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
// Routes
+app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/parts', partRoutes);
diff --git a/package-lock.json b/package-lock.json
index 99d3bcf..1ae7244 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,6 @@
"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/mongoose": "^5.11.96",
"@types/uuid": "^10.0.0",
@@ -41,8 +39,10 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
+ "@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
+ "@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
@@ -1636,75 +1636,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 +3334,13 @@
"@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": {
"version": "1.19.6",
"dev": true,
@@ -3509,6 +3447,17 @@
"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==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*",
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/mongoose": {
"version": "5.11.96",
"license": "MIT",
@@ -3516,6 +3465,13 @@
"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": {
"version": "2.0.0",
"dev": true,
@@ -3546,7 +3502,7 @@
},
"node_modules/@types/react": {
"version": "19.2.9",
- "devOptional": true,
+ "dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -3562,7 +3518,7 @@
},
"node_modules/@types/react/node_modules/csstype": {
"version": "3.2.3",
- "devOptional": true,
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/resolve": {
@@ -5516,10 +5472,6 @@
"node": ">=8"
}
},
- "node_modules/csstype": {
- "version": "3.1.3",
- "license": "MIT"
- },
"node_modules/d3-array": {
"version": "3.2.4",
"license": "ISC",
@@ -6902,10 +6854,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 +7692,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"
@@ -10120,10 +10061,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 +10270,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",
@@ -10640,6 +10566,7 @@
},
"node_modules/tslib": {
"version": "2.8.1",
+ "devOptional": true,
"license": "0BSD"
},
"node_modules/tsx": {
diff --git a/package.json b/package.json
index 61f5004..b0ba1d0 100644
--- a/package.json
+++ b/package.json
@@ -13,8 +13,6 @@
"start": "node dist/server/index.js"
},
"dependencies": {
- "@clerk/clerk-react": "^5.59.6",
- "@clerk/localizations": "^3.35.3",
"@tailwindcss/postcss": "^4.1.18",
"@types/mongoose": "^5.11.96",
"@types/uuid": "^10.0.0",
@@ -46,8 +44,10 @@
},
"devDependencies": {
"@eslint/js": "^9.39.1",
+ "@types/bcryptjs": "^2.4.6",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
+ "@types/jsonwebtoken": "^9.0.10",
"@types/multer": "^2.0.0",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
diff --git a/src/client/App.tsx b/src/client/App.tsx
index eceb3a3..ea7047b 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -1,7 +1,5 @@
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 { AuthProvider, useAuth } from './context/AuthContext';
import { SystemSettingsProvider } from './context/SystemSettingsContext';
import { NotificationProvider } from './contexts/NotificationContext';
import { Layout } from './components/Layout';
@@ -32,90 +30,98 @@ const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
};
const AppContent: React.FC = () => {
- const { organization } = useOrganization();
+ const { appUser, isLoading } = useAuth();
+
+ if (isLoading) return
Carregando...
;
- console.log('AppContent rendered');
- console.log('Current organization:', organization);
-
- // If user is signed in but has no organization, show org selector
- if (!organization) {
- console.log('No organization - showing OrganizationSelector');
+ // AppUser exists but hasn't selected an org yet (if your business logic requires orgs)
+ if (appUser && !appUser.organizationId) {
return ;
}
- console.log('Organization exists - showing main app');
return (
-
-
-
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
-
-
-
- } />
-
-
-
- } />
-
-
-
- } />
- } />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
-
-
-
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+ } />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+
+
+ );
+};
+
+const MainRouter: React.FC = () => {
+ const { appUser, isLoading } = useAuth();
+
+ if (isLoading) {
+ return Verificando sessão...
;
+ }
+
+ return (
+
+ {!appUser ? (
+
+ ) : (
+
+ )}
+
);
};
function App() {
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/client/context/AuthContext.tsx b/src/client/context/AuthContext.tsx
index 75a8094..3f2105c 100644
--- a/src/client/context/AuthContext.tsx
+++ b/src/client/context/AuthContext.tsx
@@ -1,142 +1,108 @@
-import React, { useState, useEffect, useCallback, useRef } from 'react';
-import { useUser, useOrganization } from '@clerk/clerk-react';
+import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import type { AppUser } from '../types';
-import { AuthContext } from './AuthContextType';
-import { setApiClerkUserId, setApiOrganizationId, getBaseUrl } from '../services/api';
+import { setApiToken, setApiOrganizationId, getBaseUrl } from '../services/api';
const API_URL = getBaseUrl();
-interface AuthProviderProps {
- children: React.ReactNode;
+export interface AuthContextType {
+ 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;
}
-export const AuthProvider: React.FC = ({ children }) => {
- const { user, isLoaded } = useUser();
- const { organization, membership } = useOrganization();
+export const AuthContext = createContext(undefined);
+
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [appUser, setAppUser] = useState(null);
+ const [token, setToken] = useState(localStorage.getItem('jwt_token'));
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
+ // Initial load: se tem token, setar no interceptor e buscar dados do usuário
useEffect(() => {
- setApiClerkUserId(user?.id || null);
- setApiOrganizationId(organization?.id || null);
- }, [user?.id, organization?.id]);
-
- 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 {
+ if (token) {
+ setApiToken(token);
+ refetchUser();
+ } else {
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 () => {
- if (!user) return;
-
+ if (!token) return;
+ setIsLoading(true);
try {
- const response = await fetch(`${API_URL}/users/me`, {
+ const response = await fetch(`${API_URL}/auth/me`, {
headers: {
- 'x-clerk-user-id': user.id,
- ...(organization?.id && { 'x-organization-id': organization.id }),
+ 'Authorization': `Bearer ${token}`
},
});
if (response.ok) {
const userData = await response.json();
- const effectiveRole = userData.organizationRole || userData.role || 'guest';
- setAppUser({
- ...userData,
- id: userData._id || userData.id,
- role: effectiveRole,
- });
+ setAppUser(userData);
+ if (userData.organizationId) {
+ setApiOrganizationId(userData.organizationId);
+ }
+ } else {
+ // Token inválido ou expirado
+ logout();
}
} catch (err) {
console.error('Error refetching user:', err);
+ setError('Falha na comunicação de autenticação.');
+ } finally {
+ setIsLoading(false);
}
- }, [user, organization?.id]);
-
- // Re-sync when organization changes
- useEffect(() => {
- if (isLoaded && user) {
- syncUser();
- }
- }, [isLoaded, user, organization?.id, syncUser]);
+ }, [token, logout]);
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]);
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 (
= ({ children }) => {
);
};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+};
diff --git a/src/client/main.tsx b/src/client/main.tsx
index bfdebef..e491130 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")
-}
+import { createRoot } from 'react-dom/client';
+import './index.css';
+import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
-
+
+);
diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx
index b1a480d..4ff78a1 100644
--- a/src/client/pages/Login.tsx
+++ b/src/client/pages/Login.tsx
@@ -1,7 +1,45 @@
-import { SignIn } from "@clerk/clerk-react";
+import React, { useState } from "react";
import { Hammer } from "lucide-react";
+import { useAuth } from "../context/useAuth";
+import { getBaseUrl } from "../services/api";
+
+const API_URL = getBaseUrl();
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 (
{/* Background decorative elements */}
@@ -18,13 +56,53 @@ export const Login = () => {
Gestão de Pintura Industrial
- {/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */}
-
-
+ {/* Custom Login Form */}
+
+
Entrar na sua conta
+
+ {errorMsg && (
+
+ {errorMsg}
+
+ )}
+
+
diff --git a/src/client/services/api.ts b/src/client/services/api.ts
index 7b61baa..aabba75 100644
--- a/src/client/services/api.ts
+++ b/src/client/services/api.ts
@@ -17,14 +17,13 @@ const api = axios.create({
},
});
-// Store the current user's clerk ID and Organization ID/Name
-let currentClerkUserId: string | null = null;
+let currentToken: string | null = null;
let currentOrgId: string | null = null;
let currentOrgName: string | null = null;
-// Function to set the clerk user ID (called from AuthContext)
-export const setApiClerkUserId = (clerkId: string | null) => {
- currentClerkUserId = clerkId;
+// Function to set the JWT token
+export const setApiToken = (token: string | null) => {
+ currentToken = token;
};
// Function to set the organization ID and Name (called from Layout/Context)
@@ -45,11 +44,10 @@ export const setApiOrganizationId = setApiOrgId;
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;
+ if (currentToken) {
+ config.headers['Authorization'] = `Bearer ${currentToken}`;
}
if (currentOrgId) {
config.headers['x-organization-id'] = currentOrgId;
diff --git a/src/server/app.ts b/src/server/app.ts
index 7a2016c..f970589 100644
--- a/src/server/app.ts
+++ b/src/server/app.ts
@@ -11,6 +11,7 @@ import yieldStudyRoutes from './routes/yieldStudyRoutes.js';
import userRoutes from './routes/userRoutes.js';
import systemSettingsRoutes from './routes/systemSettingsRoutes.js';
import geometryTypeRoutes from './routes/geometryTypeRoutes.js';
+import authRoutes from './routes/authRoutes.js';
import stockRoutes from './routes/stockRoutes.js';
import notificationRoutes from './routes/notificationRoutes.js';
@@ -24,17 +25,19 @@ 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', 'x-organization-id']
}));
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'}`);
+ console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
+import { authMiddleware } from './middleware/auth.js';
+app.use(authMiddleware);
app.use(extractUser);
// Static Uploads
@@ -49,6 +52,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/authController.ts b/src/server/controllers/authController.ts
new file mode 100644
index 0000000..48a0f7a
--- /dev/null
+++ b/src/server/controllers/authController.ts
@@ -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 => {
+ 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 => {
+ 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' });
+ }
+};
diff --git a/src/server/middleware/auth.ts b/src/server/middleware/auth.ts
new file mode 100644
index 0000000..3a11227
--- /dev/null
+++ b/src/server/middleware/auth.ts
@@ -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' });
+ }
+};
diff --git a/src/server/models/User.ts b/src/server/models/User.ts
index f3d5663..eb3360e 100644
--- a/src/server/models/User.ts
+++ b/src/server/models/User.ts
@@ -8,6 +8,7 @@ export interface IUser extends Document {
name: string;
role: UserRole;
isBanned: boolean;
+ passwordHash?: string;
organizationId?: string;
createdAt: Date;
updatedAt: Date;
@@ -21,6 +22,10 @@ const UserSchema: Schema = new Schema({
unique: true,
index: true
},
+ passwordHash: {
+ type: String,
+ required: false
+ },
organizationId: {
type: String,
index: true
diff --git a/src/server/routes/authRoutes.ts b/src/server/routes/authRoutes.ts
new file mode 100644
index 0000000..ef11d14
--- /dev/null
+++ b/src/server/routes/authRoutes.ts
@@ -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;