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} +
+ )} + +
+
+ + 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" + /> +
+ +
+
+ +
+ 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="••••••••" + /> +
+ + +
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;