- {u.name.charAt(0).toUpperCase()}
+ {(u.name || u.email || '?').charAt(0).toUpperCase()}
- {u.name}
+ {u.name || 'Sem nome'}
{isCurrentUser && (
(Você)
)}
@@ -432,7 +276,7 @@ export const AdminDashboard: React.FC = () => {
onChange={(e) => handleRoleChange(u.id, e.target.value as UserRole)}
disabled={isCurrentUser || isActionDisabled || u.isBanned}
aria-label={`Alterar role de ${u.name}`}
- className={`px-3 py-1.5 rounded-lg border text-sm font-semibold transition-all ${roleInfo.color} disabled:opacity-50 disabled:cursor-not-allowed bg-transparent`}
+ className={clsx("px-3 py-1.5 rounded-lg border text-sm font-semibold transition-all bg-transparent disabled:opacity-50 disabled:cursor-not-allowed", roleInfo.color)}
>
@@ -457,10 +301,11 @@ export const AdminDashboard: React.FC = () => {
>
- ) : activeTab === 'organization' ? (
-
- {/* Organization Settings Panel */}
-
-
-
-
-
-
-
- Identidade Visual
- Gerencie o logo da sua organização
-
-
-
-
- {organization?.imageUrl ? (
-
-
- 
-
-
-
-
-
- ) : (
-
-
- Sem Logo
-
- )}
-
-
-
-
- {logoLoading && (
-
-
- Enviando logo...
-
- )}
-
-
-
-
-
-
-
-
-
-
- Requisitos & Dicas
- Regras para um visual impecável
-
-
-
-
-
-
-
- Formatos Suportados
-
-
- Aceitamos arquivos nos formatos PNG, JPG ou SVG. O formato SVG é recomendado para máxima nitidez em qualquer tamanho.
-
-
-
-
-
-
- Dimensões Recomendadas
-
-
- Recomendamos uma imagem quadrada de no mínimo 512x512 pixels. Logos horizontais podem não aparecer corretamente em todas as áreas.
-
-
-
-
-
-
- Limite de Tamanho
-
-
- O arquivo não deve ultrapassar 500 KB. Arquivos maiores serão rejeitados automaticamente para garantir rapidez no carregamento.
-
-
-
-
-
-
) : activeTab === 'settings' ? (
) : activeTab === 'backup' ? (
) : (
- // Lazily load or direct render StockDashboard (Need to import it)
- Gestão de Estoque
- Acesse a nova página dedicada ao controle de estoque.
-
- Ir para Estoque
-
+ Em breve
+ Novas configurações serão adicionadas aqui.
)}
);
};
+
+export default AdminDashboard;
diff --git a/src/client/pages/DeveloperDashboard.tsx b/src/client/pages/DeveloperDashboard.tsx
index 0987d15..1ad1053 100644
--- a/src/client/pages/DeveloperDashboard.tsx
+++ b/src/client/pages/DeveloperDashboard.tsx
@@ -419,7 +419,7 @@ export const DeveloperDashboard: React.FC = () => {
{admins.map(admin => (
-
+
{admin.name.charAt(0).toUpperCase()}
@@ -441,7 +441,7 @@ export const DeveloperDashboard: React.FC = () => {
{commonUsers.map(user => (
-
+
{
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState (null);
+ const navigate = useNavigate();
+ const { refetchUser } = useAuth();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const response = await api.post('/auth/login', { email, password });
+ const { token } = response.data;
+
+ localStorage.setItem('gpi_token', token);
+ await refetchUser();
+ navigate('/');
+ } catch (err: any) {
+ setError(err.response?.data?.error || 'Erro ao realizar login. Verifique suas credenciais.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
-export const Login = () => {
return (
-
- {/* Background decorative elements */}
-
-
-
-
- {/* Logo Area */}
-
-
- G
+
+
+
+
+ GPI
+ Gestão de Pintura Industrial
+
+
+
+
+
+
+ Não tem uma conta? Contate o administrador
+
- GPI
- Gestão de Pintura Industrial
- {/* Clerk SignIn Component - Customizado via Tema Global no main.tsx */}
-
-
+
+
+ © 2026 GPI - Sistema de Gestão Industrial
+
-
-
-
- © 2026 GPI - Eficiência Industrial
-
-
+
);
};
+
+export default Login;
diff --git a/src/client/pages/Register.tsx b/src/client/pages/Register.tsx
new file mode 100644
index 0000000..799515f
--- /dev/null
+++ b/src/client/pages/Register.tsx
@@ -0,0 +1,118 @@
+import React, { useState } from 'react';
+import { useNavigate, Link } from 'react-router-dom';
+import api from '../services/api';
+import { useAuth } from '../context/AuthContext';
+import { motion } from 'framer-motion';
+
+const Register: React.FC = () => {
+ const [email, setEmail] = useState('');
+ const [name, setName] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState (null);
+ const navigate = useNavigate();
+ const { refetchUser } = useAuth();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const response = await api.post('/auth/register', { email, password, name });
+ const { token } = response.data;
+
+ localStorage.setItem('gpi_token', token);
+ await refetchUser();
+ navigate('/');
+ } catch (err: any) {
+ setError(err.response?.data?.error || 'Erro ao realizar cadastro.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+ GPI
+ Novo Cadastro de Usuário
+
+
+
+
+
+
+ Já tem uma conta? Voltar para o login
+
+
+
+
+
+ );
+};
+
+export default Register;
diff --git a/src/client/services/api.ts b/src/client/services/api.ts
index 7b61baa..7ab0ce5 100644
--- a/src/client/services/api.ts
+++ b/src/client/services/api.ts
@@ -41,23 +41,17 @@ export const setApiOrgId = (orgId: string | null) => {
// Alias for consistency
export const setApiOrganizationId = setApiOrgId;
-// Request interceptor to add clerk user ID and Org ID headers
+// Request interceptor to add JWT token
api.interceptors.request.use(
(config) => {
- console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, {
- clerkId: currentClerkUserId,
- orgId: currentOrgId
- });
- if (currentClerkUserId) {
- config.headers['x-clerk-user-id'] = currentClerkUserId;
+ const token = localStorage.getItem('gpi_token');
+ if (token) {
+ config.headers['Authorization'] = `Bearer ${token}`;
}
+
if (currentOrgId) {
config.headers['x-organization-id'] = currentOrgId;
}
- if (currentOrgName) {
- // Encode to handle special characters
- config.headers['x-organization-name'] = encodeURIComponent(currentOrgName);
- }
return config;
},
(error) => {
@@ -65,12 +59,18 @@ api.interceptors.request.use(
}
);
-// Response interceptor to handle 403 errors (guest access denied)
+// Response interceptor to handle 401 (Unauthorized) and 403 errors
api.interceptors.response.use(
(response) => response,
(error) => {
+ if (error.response?.status === 401) {
+ // Token expired or invalid
+ localStorage.removeItem('gpi_token');
+ if (!window.location.pathname.includes('/login')) {
+ window.location.href = '/login';
+ }
+ }
if (error.response?.status === 403) {
- // Check if it's a guest permission error
const errorMessage = error.response?.data?.error || '';
if (errorMessage.includes('Convidados') || errorMessage.includes('guest') || errorMessage.includes('permissão')) {
triggerGuestWarning();
diff --git a/src/client/services/systemSettingsService.ts b/src/client/services/systemSettingsService.ts
index c7e18b0..60353ee 100644
--- a/src/client/services/systemSettingsService.ts
+++ b/src/client/services/systemSettingsService.ts
@@ -51,7 +51,7 @@ export const systemSettingsService = {
export interface GlobalUser {
_id: string;
- clerkId: string;
+ id: string;
name: string;
email: string;
role: string;
@@ -66,10 +66,10 @@ export interface GlobalOrganization {
isBanned: boolean;
name?: string; // Added
members: {
+ id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
- clerkUserId: string;
isBanned: boolean;
}[];
}
diff --git a/src/server/app.ts b/src/server/app.ts
index 7a2016c..5501481 100644
--- a/src/server/app.ts
+++ b/src/server/app.ts
@@ -16,29 +16,24 @@ import stockRoutes from './routes/stockRoutes.js';
import notificationRoutes from './routes/notificationRoutes.js';
import instrumentRoutes from './routes/instrumentRoutes.js';
import messageRoutes from './routes/messageRoutes.js';
+import authRoutes from './routes/authRoutes.js';
import backupRoutes from './routes/backupRoutes.js';
import path from 'path';
+import fs from 'fs';
+import { authenticateJWT } from './middleware/authMiddleware.js';
const app = express();
app.use(cors({
origin: '*', // Be more specific in production
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
- allowedHeaders: ['Content-Type', 'Authorization', 'x-clerk-user-id', 'x-organization-id']
+ allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json());
-import { extractUser } from './middleware/roleMiddleware.js';
-
-// LOG DE DEPURAÇÃO PARA CONEXÃO
-app.use((req, res, next) => {
- console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ClerkID: ${req.headers['x-clerk-user-id'] || 'None'}`);
- next();
-});
-
-app.use(extractUser);
+// JWT Authentication Middleware
+app.use(authenticateJWT);
// Static Uploads
-import fs from 'fs';
const uploadsPath = path.join(process.cwd(), 'uploads');
// Ensure uploads directory exists
@@ -49,6 +44,7 @@ if (!fs.existsSync(uploadsPath)) {
app.use('/uploads', express.static(uploadsPath));
// Routes
+app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
app.use('/api/projects', projectRoutes);
app.use('/api/parts', partRoutes);
diff --git a/src/server/controllers/applicationRecordController.ts b/src/server/controllers/applicationRecordController.ts
index 5898005..2a56012 100644
--- a/src/server/controllers/applicationRecordController.ts
+++ b/src/server/controllers/applicationRecordController.ts
@@ -1,11 +1,11 @@
-import { Request, Response } from 'express';
+import { Response } from 'express';
import * as appRecordService from '../services/applicationRecordService.js';
-import '../middleware/roleMiddleware.js'; // Ensure type augmentation
+import { AuthRequest } from '../middleware/authMiddleware.js';
-export const createApplicationRecord = async (req: Request, res: Response) => {
+export const createApplicationRecord = async (req: AuthRequest, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
- const createdBy = req.appUser?.clerkId;
+ const createdBy = req.appUser?._id?.toString();
const record = await appRecordService.createApplicationRecord({ ...req.body, organizationId, createdBy });
res.status(201).json(record);
} catch (error: unknown) {
@@ -14,7 +14,7 @@ export const createApplicationRecord = async (req: Request, res: Response) => {
}
};
-export const getApplicationRecordsByProject = async (req: Request, res: Response) => {
+export const getApplicationRecordsByProject = async (req: AuthRequest, res: Response) => {
try {
const { projectId } = req.params;
const organizationId = req.appUser?.organizationId;
@@ -26,10 +26,10 @@ export const getApplicationRecordsByProject = async (req: Request, res: Response
}
};
-export const updateApplicationRecord = async (req: Request, res: Response) => {
+export const updateApplicationRecord = async (req: AuthRequest, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const userRole = req.appUser?.organizationRole || req.appUser?.role;
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
@@ -49,10 +49,10 @@ export const updateApplicationRecord = async (req: Request, res: Response) => {
}
};
-export const deleteApplicationRecord = async (req: Request, res: Response) => {
+export const deleteApplicationRecord = async (req: AuthRequest, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const userRole = req.appUser?.organizationRole || req.appUser?.role;
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
diff --git a/src/server/controllers/authController.ts b/src/server/controllers/authController.ts
new file mode 100644
index 0000000..31898b6
--- /dev/null
+++ b/src/server/controllers/authController.ts
@@ -0,0 +1,105 @@
+import { Request, Response } from 'express';
+import jwt from 'jsonwebtoken';
+import bcrypt from 'bcryptjs';
+import User from '../models/User.js';
+
+const JWT_SECRET = process.env.JWT_SECRET || 'secret';
+const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
+
+export const login = async (req: Request, res: Response) => {
+ try {
+ const { email, password } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({ error: 'E-mail e senha são obrigatórios' });
+ }
+
+ const user = await User.findOne({ email }).select('+password');
+
+ if (!user || !user.password) {
+ return res.status(401).json({ error: 'Credenciais inválidas' });
+ }
+
+ const isMatch = await bcrypt.compare(password, user.password);
+
+ if (!isMatch) {
+ return res.status(401).json({ error: 'Credenciais inválidas' });
+ }
+
+ if (user.isBanned) {
+ return res.status(403).json({ error: 'Sua conta está bloqueada' });
+ }
+
+ const token = jwt.sign(
+ { id: user._id, email: user.email, role: user.role },
+ JWT_SECRET,
+ { expiresIn: JWT_EXPIRES_IN }
+ );
+
+ // Remove password from user object
+ const userObj = user.toObject();
+ delete userObj.password;
+
+ res.json({
+ token,
+ user: userObj
+ });
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({ error: 'Erro interno no servidor' });
+ }
+};
+
+export const register = async (req: Request, res: Response) => {
+ try {
+ const { email, password, name, organizationId } = req.body;
+
+ if (!email || !password || !name) {
+ return res.status(400).json({ error: 'Campos obrigatórios ausentes' });
+ }
+
+ const existingUser = await User.findOne({ email });
+ if (existingUser) {
+ return res.status(400).json({ error: 'E-mail já cadastrado' });
+ }
+
+ const hashedPassword = await bcrypt.hash(password, 12);
+
+ const user = await User.create({
+ email,
+ password: hashedPassword,
+ name,
+ organizationId,
+ role: 'user' // Default role
+ });
+
+ const token = jwt.sign(
+ { id: user._id, email: user.email, role: user.role },
+ JWT_SECRET,
+ { expiresIn: JWT_EXPIRES_IN }
+ );
+
+ const userObj = user.toObject();
+ delete userObj.password;
+
+ res.status(201).json({
+ token,
+ user: userObj
+ });
+ } catch (error) {
+ console.error('Registration error:', error);
+ res.status(500).json({ error: 'Erro ao criar usuário' });
+ }
+};
+
+export const getMe = async (req: any, res: Response) => {
+ try {
+ const user = await User.findById(req.user.id);
+ if (!user) {
+ return res.status(404).json({ error: 'Usuário não encontrado' });
+ }
+ res.json(user);
+ } catch (error) {
+ res.status(500).json({ error: 'Erro ao obter dados do usuário' });
+ }
+};
diff --git a/src/server/controllers/inspectionController.ts b/src/server/controllers/inspectionController.ts
index 7683d6e..7bee7d1 100644
--- a/src/server/controllers/inspectionController.ts
+++ b/src/server/controllers/inspectionController.ts
@@ -6,7 +6,7 @@ import '../middleware/roleMiddleware.js'; // Ensure type augmentation
export const createInspection = async (req: Request, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
- const createdBy = req.appUser?.clerkId;
+ const createdBy = req.appUser?._id?.toString();
const inspection = await inspectionService.createInspection({
...req.body,
organizationId,
@@ -46,7 +46,7 @@ export const getInspectionsByProject = async (req: Request, res: Response) => {
export const updateInspection = async (req: Request, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const userRole = req.appUser?.organizationRole || req.appUser?.role;
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
@@ -69,7 +69,7 @@ export const updateInspection = async (req: Request, res: Response) => {
export const deleteInspection = async (req: Request, res: Response) => {
try {
const organizationId = req.appUser?.organizationId;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const userRole = req.appUser?.organizationRole || req.appUser?.role;
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
@@ -99,7 +99,7 @@ export const getAllInspections = async (req: Request, res: Response) => {
}
};
-export const uploadPhoto = async (req: Request, res: Response) => {
+export const uploadPhoto = async (req: Request & { file?: any }, res: Response) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
diff --git a/src/server/controllers/messageController.ts b/src/server/controllers/messageController.ts
index cec2795..794ecbc 100644
--- a/src/server/controllers/messageController.ts
+++ b/src/server/controllers/messageController.ts
@@ -1,12 +1,13 @@
-import { Request, Response } from 'express';
+import { Response } from 'express';
import Message from '../models/Message.js';
import OrganizationMember from '../models/OrganizationMember.js';
+import { AuthRequest } from '../middleware/authMiddleware.js';
// Send a message
-export const sendMessage = async (req: Request, res: Response) => {
+export const sendMessage = async (req: AuthRequest, res: Response) => {
try {
const { toUserId, message } = req.body;
- const fromUserId = req.appUser?.clerkId;
+ const fromUserId = req.appUser?._id?.toString();
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
@@ -58,9 +59,9 @@ export const sendMessage = async (req: Request, res: Response) => {
};
// Get unread messages for current user
-export const getUnreadMessages = async (req: Request, res: Response) => {
+export const getUnreadMessages = async (req: AuthRequest, res: Response) => {
try {
- const toUserId = req.appUser?.clerkId;
+ const toUserId = req.appUser?._id?.toString();
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
@@ -82,7 +83,7 @@ export const getUnreadMessages = async (req: Request, res: Response) => {
// Populate sender info
const messagesWithSender = await Promise.all(
messages.map(async (msg) => {
- const sender = await OrganizationMember.findOne({ clerkUserId: msg.fromUserId });
+ const sender = await OrganizationMember.findOne({ userId: msg.fromUserId });
return {
...msg.toObject(),
fromUser: sender ? { name: sender.name, email: sender.email } : null,
@@ -98,10 +99,10 @@ export const getUnreadMessages = async (req: Request, res: Response) => {
};
// Mark message as read
-export const markMessageAsRead = async (req: Request, res: Response) => {
+export const markMessageAsRead = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
@@ -134,9 +135,9 @@ export const markMessageAsRead = async (req: Request, res: Response) => {
};
// Get my pending (unread) sent messages
-export const getMyPendingMessages = async (req: Request, res: Response) => {
+export const getMyPendingMessages = async (req: AuthRequest, res: Response) => {
try {
- const fromUserId = req.appUser?.clerkId;
+ const fromUserId = req.appUser?._id?.toString();
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
@@ -156,7 +157,7 @@ export const getMyPendingMessages = async (req: Request, res: Response) => {
// Populate recipient info
const messagesWithRecipient = await Promise.all(
messages.map(async (msg) => {
- const recipient = await OrganizationMember.findOne({ clerkUserId: msg.toUserId });
+ const recipient = await OrganizationMember.findOne({ userId: msg.toUserId });
return {
...msg.toObject(),
toUser: recipient ? { name: recipient.name, email: recipient.email } : null,
@@ -172,10 +173,10 @@ export const getMyPendingMessages = async (req: Request, res: Response) => {
};
// Delete a message (only if unread and sender is the current user)
-export const deleteMessage = async (req: Request, res: Response) => {
+export const deleteMessage = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const organizationId = req.headers['x-organization-id'] as string;
if (!organizationId) {
@@ -206,10 +207,10 @@ export const deleteMessage = async (req: Request, res: Response) => {
};
// Recipient deletes/archives a message
-export const archiveMessage = async (req: Request, res: Response) => {
+export const archiveMessage = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const organizationId = req.headers['x-organization-id'] as string;
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
@@ -225,10 +226,10 @@ export const archiveMessage = async (req: Request, res: Response) => {
}
};
-export const recipientDeleteMessage = async (req: Request, res: Response) => {
+export const recipientDeleteMessage = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
- const userId = req.appUser?.clerkId;
+ const userId = req.appUser?._id?.toString();
const organizationId = req.headers['x-organization-id'] as string;
const message = await Message.findOne({ _id: id, toUserId: userId, organizationId });
diff --git a/src/server/controllers/notificationController.ts b/src/server/controllers/notificationController.ts
index 4c066df..6060af1 100644
--- a/src/server/controllers/notificationController.ts
+++ b/src/server/controllers/notificationController.ts
@@ -1,14 +1,12 @@
import { Request, Response } from 'express';
import { notificationService } from '../services/notificationService.js';
+import { AuthRequest } from '../middleware/authMiddleware.js';
export const notificationController = {
- getUserNotifications: async (req: Request, res: Response) => {
+ getUserNotifications: async (req: AuthRequest, res: Response) => {
try {
const organizationId = req.headers['x-organization-id'] as string;
- const userId = req.headers['x-user-id'] as string; // Assumindo que temos o ID do usuário (clerkId ou email)
-
- // Se não tiver userId no header (ainda não implementado auth full), tentar pegar do query ou usar um fallback
- // Nota: Idealmente o middleware de auth popula req.user. Vamos assumir que passamos x-user-id no frontend por enquanto.
+ const userId = req.appUser?._id?.toString() || '';
if (!organizationId) {
return res.status(400).json({ error: 'Organization ID is required' });
@@ -26,7 +24,7 @@ export const notificationController = {
}
},
- markAsRead: async (req: Request, res: Response) => {
+ markAsRead: async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const notification = await notificationService.markAsRead(id as string);
@@ -37,10 +35,10 @@ export const notificationController = {
}
},
- markAllAsRead: async (req: Request, res: Response) => {
+ markAllAsRead: async (req: AuthRequest, res: Response) => {
try {
const organizationId = req.headers['x-organization-id'] as string;
- const userId = req.headers['x-user-id'] as string;
+ const userId = req.appUser?._id?.toString() || '';
if (!organizationId) {
return res.status(400).json({ error: 'Organization ID is required' });
@@ -54,10 +52,10 @@ export const notificationController = {
}
},
- clearAll: async (req: Request, res: Response) => {
+ clearAll: async (req: AuthRequest, res: Response) => {
try {
const organizationId = req.headers['x-organization-id'] as string;
- const userId = req.headers['x-user-id'] as string;
+ const userId = req.appUser?._id?.toString() || '';
if (!organizationId) {
return res.status(400).json({ error: 'Organization ID is required' });
@@ -71,10 +69,10 @@ export const notificationController = {
}
},
- archive: async (req: Request, res: Response) => {
+ archive: async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
- const userId = req.headers['x-user-id'] as string;
+ const userId = req.appUser?._id?.toString() || '';
const notification = await notificationService.archive(id as string, userId);
res.json(notification);
} catch (error) {
@@ -83,10 +81,10 @@ export const notificationController = {
}
},
- delete: async (req: Request, res: Response) => {
+ delete: async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
- const userId = req.headers['x-user-id'] as string;
+ const userId = req.appUser?._id?.toString() || '';
await notificationService.softDelete(id as string, userId);
res.json({ success: true });
} catch (error) {
diff --git a/src/server/controllers/stockController.ts b/src/server/controllers/stockController.ts
index 7109802..f835681 100644
--- a/src/server/controllers/stockController.ts
+++ b/src/server/controllers/stockController.ts
@@ -67,7 +67,7 @@ export const createStockItem = async (req: AuthRequest, res: Response) => {
const newItem = new StockItem({
organizationId,
- createdBy: req.appUser?.clerkId,
+ createdBy: req.appUser?._id,
dataSheetId,
rrNumber,
batchNumber,
@@ -86,7 +86,7 @@ export const createStockItem = async (req: AuthRequest, res: Response) => {
// Create Initial Movement (ENTRY)
await StockMovement.create({
organizationId,
- createdBy: req.appUser?.clerkId,
+ createdBy: req.appUser?._id,
stockItemId: savedItem._id,
movementNumber: 1,
type: 'ENTRY',
@@ -195,7 +195,7 @@ export const adjustStock = async (req: AuthRequest, res: Response) => {
// Register Movement
await StockMovement.create({
organizationId,
- createdBy: req.appUser?.clerkId,
+ createdBy: req.appUser?._id,
stockItemId: item._id,
movementNumber,
type: 'ADJUSTMENT',
@@ -241,7 +241,7 @@ export const consumeStock = async (req: AuthRequest, res: Response) => {
// Register Movement (Negative quantity for consumption)
await StockMovement.create({
organizationId,
- createdBy: req.appUser?.clerkId,
+ createdBy: req.appUser?._id,
stockItemId: item._id,
movementNumber,
type: 'CONSUMPTION',
@@ -348,7 +348,7 @@ export const updateStockMovement = async (req: AuthRequest, res: Response) => {
const { id } = req.params; // Movement ID
const organizationId = req.appUser?.organizationId;
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
- const userId = req.appUser?.clerkId || 'system';
+ const userId = req.appUser?._id || 'system';
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
if (!isAdmin) {
@@ -431,7 +431,7 @@ export const deleteStockMovement = async (req: AuthRequest, res: Response) => {
const { id } = req.params;
const organizationId = req.appUser?.organizationId;
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
- const userId = req.appUser?.clerkId || 'system';
+ const userId = req.appUser?._id || 'system';
const isAdmin = req.appUser?.role === 'admin' || req.appUser?.organizationRole === 'admin';
if (!isAdmin) {
diff --git a/src/server/controllers/systemSettingsController.ts b/src/server/controllers/systemSettingsController.ts
index f92efbf..9dec403 100644
--- a/src/server/controllers/systemSettingsController.ts
+++ b/src/server/controllers/systemSettingsController.ts
@@ -1,4 +1,4 @@
-import { Request, Response } from 'express';
+import { Response } from 'express';
import SystemSettings from '../models/SystemSettings.js';
import User from '../models/User.js';
import OrganizationMember from '../models/OrganizationMember.js';
@@ -6,8 +6,9 @@ import Organization from '../models/Organization.js';
import path from 'path';
import fs from 'fs';
import os from 'os';
+import { AuthRequest } from '../middleware/authMiddleware.js';
-export const getSettings = async (req: Request, res: Response) => {
+export const getSettings = async (req: AuthRequest, res: Response) => {
try {
let settings = await SystemSettings.findOne({ settingsId: 'global' });
@@ -27,7 +28,7 @@ export const getSettings = async (req: Request, res: Response) => {
}
};
-export const updateSettings = async (req: Request, res: Response) => {
+export const updateSettings = async (req: AuthRequest, res: Response) => {
try {
const { appName, appSubtitle, appLogoUrl } = req.body;
@@ -51,7 +52,7 @@ export const updateSettings = async (req: Request, res: Response) => {
};
-export const serveLogo = async (req: Request, res: Response) => {
+export const serveLogo = async (req: AuthRequest, res: Response) => {
try {
const { filename } = req.params as { filename: string };
@@ -74,7 +75,7 @@ export const serveLogo = async (req: Request, res: Response) => {
}
};
-export const uploadLogo = async (req: Request, res: Response) => {
+export const uploadLogo = async (req: AuthRequest & { file?: any }, res: Response) => {
try {
if (!req.file) {
return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
@@ -92,7 +93,7 @@ export const uploadLogo = async (req: Request, res: Response) => {
};
// Global Admin Functions
-export const getGlobalUsers = async (req: Request, res: Response) => {
+export const getGlobalUsers = async (req: AuthRequest, res: Response) => {
try {
const users = await User.find({}).sort({ createdAt: -1 });
res.json(users);
@@ -102,7 +103,7 @@ export const getGlobalUsers = async (req: Request, res: Response) => {
}
};
-export const getGlobalOrganizations = async (req: Request, res: Response) => {
+export const getGlobalOrganizations = async (req: AuthRequest, res: Response) => {
try {
// Aggregate members to group by org and get full member lists
const organizations = await OrganizationMember.aggregate([
@@ -111,10 +112,10 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => {
_id: '$organizationId',
members: {
$push: {
+ id: '$userId',
name: '$name',
email: '$email',
role: '$role',
- clerkUserId: '$clerkUserId',
isBanned: '$isBanned'
}
},
@@ -125,7 +126,7 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => {
$lookup: {
from: 'organizations', // Ensure this matches the collection name of Organization model
localField: '_id',
- foreignField: 'clerkId',
+ foreignField: 'organizationId', // We should rename clerkId in Organization model too
as: 'orgDetails'
}
},
@@ -155,7 +156,7 @@ export const getGlobalOrganizations = async (req: Request, res: Response) => {
}
};
-export const toggleOrganizationBan = async (req: Request, res: Response) => {
+export const toggleOrganizationBan = async (req: AuthRequest, res: Response) => {
try {
const { organizationId, isBanned } = req.body;
@@ -165,7 +166,7 @@ export const toggleOrganizationBan = async (req: Request, res: Response) => {
// Upsert the Organization record
const org = await Organization.findOneAndUpdate(
- { clerkId: organizationId },
+ { organizationId: organizationId },
{ isBanned: isBanned },
{ new: true, upsert: true }
);
diff --git a/src/server/controllers/userController.ts b/src/server/controllers/userController.ts
index b62cb7e..cb3e204 100644
--- a/src/server/controllers/userController.ts
+++ b/src/server/controllers/userController.ts
@@ -1,101 +1,9 @@
import { Request, Response } from 'express';
import User, { IUser } from '../models/User.js';
import OrganizationMember, { OrgRole } from '../models/OrganizationMember.js';
+import { AuthRequest } from '../middleware/authMiddleware.js';
-// Define locally to avoid import cycle risks
-interface IAppUser extends IUser {
- organizationId?: string;
- organizationRole?: OrgRole;
- organizationBanned?: boolean;
-}
-
-interface AuthRequest extends Request {
- appUser?: IAppUser;
-}
-
-/**
- * Sync user from Clerk to MongoDB
- * Creates user if doesn't exist, updates if exists
- * Also creates/updates OrganizationMember for the current organization
- */
-export const syncUser = async (req: Request, res: Response) => {
- console.log('--- syncUser called ---', req.body);
- try {
- const { clerkId, email, name, organizationId, clerkRole } = req.body;
-
- if (!clerkId || !email || !name) {
- return res.status(400).json({ error: 'clerkId, email e name são obrigatórios.' });
- }
-
- // 1. Upsert the global User record
- let user = await User.findOne({ clerkId });
-
- if (user) {
- user.email = email;
- user.name = name;
- await user.save();
- } else {
- user = await User.create({
- clerkId,
- email,
- name,
- role: 'guest', // Default global role
- isBanned: false
- });
- }
-
- if (organizationId) {
-
- // Map Clerk role to our app role
- let appRole: OrgRole = 'guest';
- if (clerkRole === 'org:admin') {
- appRole = 'admin';
- } else if (clerkRole === 'org:member') {
- appRole = 'user';
- }
-
- // Use findOneAndUpdate with upsert to handle race conditions atomically
- // This avoids the need for try/catch on create and handles existing members too
- const member = await OrganizationMember.findOneAndUpdate(
- { clerkUserId: clerkId, organizationId },
- {
- $set: {
- name,
- email,
- // Only update role if it's the first time (creation)
- // Or we can optionally update it if needed.
- // For now, let's NOT overwrite role on update to preserve local changes,
- // UNLESS we want to force sync with Clerk.
- // Let's use $setOnInsert for fields we only want to set on creation.
- },
- $setOnInsert: {
- role: appRole,
- isBanned: false
- }
- },
- { upsert: true, new: true, setDefaultsOnInsert: true }
- );
-
- // Return combined info
- return res.json({
- ...user.toObject(),
- organizationRole: member.role,
- organizationBanned: member.isBanned
- });
- }
-
- res.json(user);
- } catch (error) {
- console.error('Error syncing user:', error);
- // Retornar 200 mesmo com erro para não travar o frontend se for algo não crítico,
- // mas aqui é crítico. Vamos logar melhor.
- res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + (error instanceof Error ? error.message : String(error)) });
- }
-};
-
-/**
- * Get current user data with organization context
- */
+// Get current user data with organization context
export const getCurrentUser = async (req: AuthRequest, res: Response) => {
try {
if (!req.appUser) {
@@ -106,7 +14,7 @@ export const getCurrentUser = async (req: AuthRequest, res: Response) => {
if (organizationId) {
const member = await OrganizationMember.findOne({
- clerkUserId: req.appUser.clerkId,
+ userId: req.appUser._id.toString(),
organizationId
});
@@ -127,21 +35,16 @@ export const getCurrentUser = async (req: AuthRequest, res: Response) => {
}
};
-/**
- * Get all users for the current organization (admin only)
- */
+// Get all users for the current organization (admin only)
export const getAllUsers = async (req: Request, res: Response) => {
try {
const organizationId = req.headers['x-organization-id'] as string;
- console.log('getAllUsers called with organizationId:', organizationId);
-
if (!organizationId) {
return res.status(400).json({ error: 'Organização não selecionada.' });
}
const members = await OrganizationMember.find({ organizationId }).sort({ createdAt: -1 });
- console.log(`Found ${members.length} members for org ${organizationId}:`, members.map(m => ({ name: m.name, email: m.email, clerkId: m.clerkUserId })));
res.json(members);
} catch (error) {
console.error('Error getting users:', error);
@@ -149,9 +52,7 @@ export const getAllUsers = async (req: Request, res: Response) => {
}
};
-/**
- * Update user role within organization (admin only)
- */
+// Update user role within organization (admin only)
export const updateUserRole = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
@@ -184,14 +85,12 @@ export const updateUserRole = async (req: AuthRequest, res: Response) => {
res.json(member);
} catch (error) {
- console.error('Error toggling ban:', error);
- res.status(500).json({ error: 'Erro ao alterar status de banimento.' });
+ console.error('Error updating role:', error);
+ res.status(500).json({ error: 'Erro ao atualizar role.' });
}
};
-/**
- * Ban or unban user within organization (admin only)
- */
+// Ban or unban user within organization (admin only)
export const toggleBanUser = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
@@ -208,12 +107,12 @@ export const toggleBanUser = async (req: AuthRequest, res: Response) => {
}
// Prevent banning yourself
- if (req.appUser && member.clerkUserId === req.appUser.clerkId) {
+ if (req.appUser && member.userId === req.appUser._id.toString()) {
return res.status(400).json({ error: 'Você não pode banir a si mesmo.' });
}
// Prevent banning another admin
- if (member.role === 'admin') {
+ if (member.role === 'admin' && isBanned) {
return res.status(400).json({ error: 'Não é possível banir um administrador.' });
}
@@ -227,38 +126,22 @@ export const toggleBanUser = async (req: AuthRequest, res: Response) => {
}
};
-/**
- * Update current user's lastSeenAt timestamp
- */
+// Update current user's lastSeenAt timestamp
export const heartbeat = async (req: AuthRequest, res: Response) => {
try {
if (!req.appUser) {
return res.status(401).json({ error: 'Não autenticado.' });
}
- // Update User model
await User.findByIdAndUpdate(req.appUser._id, { lastSeenAt: new Date() });
-
- // Also update Organization Member for tighter query
- // But for now User model is enough if we join correctly, or just use User model for presence.
- // Actually, since we want to show users per organization, we should filter by Org.
- // Our 'User.ts' has organizationId, but it might be just the 'default' one.
- // Let's rely on OrganizationMember for the list, but we need to update lastSeenAt there too?
- // Strategy: Update User (global), and when querying active users, join or filter.
- // Better: Update OrganizationMember too if we want org-specific presence?
- // Simpler: Just update User. When fetching active users, we fetch OrganizationMembers and populate User details, filtering by User.lastSeenAt.
-
res.status(200).send();
} catch (error) {
- // Silent fail for heartbeat
console.error('Heartbeat error:', error);
res.status(500).send();
}
};
-/**
- * Get active users in the same organization (seen in last 2 mins)
- */
+// Get active users in the same organization (seen in last 2 mins)
export const getActiveUsers = async (req: AuthRequest, res: Response) => {
try {
const organizationId = req.headers['x-organization-id'] as string;
@@ -268,20 +151,15 @@ export const getActiveUsers = async (req: AuthRequest, res: Response) => {
return res.status(400).json([]);
}
- // Find members of this org
const members = await OrganizationMember.find({ organizationId });
+ const userIds = members.map(m => m.userId);
- // Get their Clerk IDs
- const clerkIds = members.map(m => m.clerkUserId);
-
- // Find Users who were seen recently (2 minutes)
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
const activeUsers = await User.find({
- clerkId: { $in: clerkIds },
- lastSeenAt: { $gte: twoMinutesAgo },
- _id: { $ne: currentUserId } // Optional: exclude self
- }).select('name email lastSeenAt clerkId'); // Only needed fields
+ _id: { $in: userIds, $ne: currentUserId },
+ lastSeenAt: { $gte: twoMinutesAgo }
+ }).select('name email lastSeenAt');
res.json(activeUsers);
} catch (error) {
@@ -300,20 +178,20 @@ export const deleteUser = async (req: Request, res: Response) => {
return res.status(400).json({ error: 'Organização não selecionada.' });
}
- console.log(`Deleting member ${id} from organization ${organizationId}`);
-
- // Delete from OrganizationMember collection
- const result = await OrganizationMember.findByIdAndDelete(id);
+ const result = await OrganizationMember.findOneAndDelete({ _id: id, organizationId });
if (!result) {
return res.status(404).json({ error: 'Membro não encontrado.' });
}
- console.log(`Member ${result.name} deleted successfully`);
-
res.json({ message: 'Membro removido com sucesso.', deletedMember: result });
} catch (error) {
console.error('Error deleting user:', error);
res.status(500).json({ error: 'Erro ao remover membro.' });
}
};
+
+// Placeholder for sync (not needed with custom auth but keeping to avoid breaks if called)
+export const syncUser = async (req: Request, res: Response) => {
+ res.json({ message: 'Sync no longer required with JWT auth.' });
+};
diff --git a/src/server/middleware/authMiddleware.ts b/src/server/middleware/authMiddleware.ts
new file mode 100644
index 0000000..331d1aa
--- /dev/null
+++ b/src/server/middleware/authMiddleware.ts
@@ -0,0 +1,38 @@
+import { Request, Response, NextFunction } from 'express';
+import jwt from 'jsonwebtoken';
+import User from '../models/User.js';
+
+const JWT_SECRET = process.env.JWT_SECRET || 'secret';
+
+export interface AuthRequest extends Request {
+ user?: any;
+ appUser?: any; // For backward compatibility
+}
+
+export const authenticateJWT = async (req: AuthRequest, res: Response, next: NextFunction) => {
+ try {
+ const authHeader = req.headers.authorization;
+
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ // Keep guest access if allowed by specific routes
+ return next();
+ }
+
+ const token = authHeader.split(' ')[1];
+ const decoded: any = jwt.verify(token, JWT_SECRET);
+
+ const user = await User.findById(decoded.id);
+
+ if (!user || user.isBanned) {
+ return res.status(401).json({ error: 'Sessão inválida ou usuário bloqueado' });
+ }
+
+ req.user = user;
+ req.appUser = user; // Map to appUser for existing controllers
+
+ next();
+ } catch (error) {
+ console.error('JWT validation error:', error);
+ return res.status(401).json({ error: 'Token inválido' });
+ }
+};
diff --git a/src/server/middleware/roleMiddleware.ts b/src/server/middleware/roleMiddleware.ts
index 957b9ae..501557d 100644
--- a/src/server/middleware/roleMiddleware.ts
+++ b/src/server/middleware/roleMiddleware.ts
@@ -17,93 +17,6 @@ declare module 'express-serve-static-core' {
}
}
-/**
- * Middleware to extract and verify user from Clerk ID header
- * Also loads organization-specific role if organization context is provided
- */
-export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
- try {
- const clerkId = req.headers['x-clerk-user-id'] as string;
- const organizationId = req.headers['x-organization-id'] as string;
-
- if (!clerkId) {
- return next(); // No user, continue without
- }
-
- const user = await User.findOne({ clerkId });
-
- if (user) {
- if (user.isBanned) {
- return res.status(403).json({ error: 'Conta bloqueada. Entre em contato com o administrador.' });
- }
-
- // Create extended user object
- const appUser: IAppUser = user.toObject() as IAppUser;
- appUser.organizationId = organizationId;
-
- // If organization context, get org-specific role
- if (organizationId) {
- // Check if Organization is globally banned (subscription specific, etc.)
- const orgStatus = await Organization.findOne({ clerkId: organizationId });
- const orgName = req.headers['x-organization-name'] ? decodeURIComponent(req.headers['x-organization-name'] as string) : undefined;
-
- if (orgStatus) {
- // Update name if different and present
- if (orgName && orgStatus.name !== orgName) {
- try {
- await Organization.updateOne(
- { clerkId: organizationId },
- { name: orgName }
- );
- } catch (err) {
- console.warn('Failed to update organization name', err);
- }
- }
-
- if (orgStatus.isBanned) {
- return res.status(403).json({
- error: 'Acesso bloqueado: Esta organização está suspensa. Entre em contato com o suporte.'
- });
- }
- } else {
- // Create new org with name if present
- try {
- await Organization.create({
- clerkId: organizationId,
- name: orgName
- });
- } catch (_e) {
- console.warn('Organization auto-create race condition', _e);
- }
- }
-
- const member = await OrganizationMember.findOne({ clerkUserId: clerkId, organizationId });
- if (member) {
- if (member.isBanned) {
- return res.status(403).json({ error: 'Acesso bloqueado nesta organização.' });
- }
- appUser.organizationRole = member.role;
- appUser.role = member.role; // Override global role with org role
- } else {
- // User exists but is not a member of this org yet
- appUser.organizationRole = 'guest';
- appUser.role = 'guest';
- }
- }
-
- req.appUser = appUser;
- // console.log(`✅ Request authenticated as: ${appUser.name} (${appUser.role})`);
- } else {
- console.warn(`⚠️ User with Clerk ID ${clerkId} not found in MongoDB. Sync required.`);
- }
-
- next();
- } catch (error) {
- console.error('Error extracting user:', error);
- next();
- }
-};
-
/**
* Middleware to require specific roles for a route
* @param allowedRoles Array of roles that can access the route
@@ -119,6 +32,7 @@ export const requireRole = (allowedRoles: OrgRole[]) => {
return next();
}
+ // Fallback to global role if organizationRole is not set
const effectiveRole = req.appUser.organizationRole || req.appUser.role;
if (!allowedRoles.includes(effectiveRole as OrgRole)) {
diff --git a/src/server/models/Message.ts b/src/server/models/Message.ts
index 0dd8324..3362df2 100644
--- a/src/server/models/Message.ts
+++ b/src/server/models/Message.ts
@@ -2,8 +2,8 @@ import mongoose, { Schema, Document } from 'mongoose';
export interface IMessage extends Document {
organizationId: string;
- fromUserId: string; // clerkId do remetente
- toUserId: string; // clerkId do destinatário
+ fromUserId: string; // ID do remetente
+ toUserId: string; // ID do destinatário
message: string;
isRead: boolean;
readAt?: Date;
diff --git a/src/server/models/Organization.ts b/src/server/models/Organization.ts
index 6e83f02..c06ebfa 100644
--- a/src/server/models/Organization.ts
+++ b/src/server/models/Organization.ts
@@ -1,7 +1,7 @@
import mongoose, { Schema, Document } from 'mongoose';
export interface IOrganization extends Document {
- clerkId: string;
+ organizationId: string;
name?: string;
isBanned: boolean;
createdAt: Date;
@@ -9,7 +9,7 @@ export interface IOrganization extends Document {
}
const OrganizationSchema: Schema = new Schema({
- clerkId: { type: String, required: true, unique: true, index: true },
+ organizationId: { type: String, required: true, unique: true, index: true },
name: { type: String },
isBanned: { type: Boolean, default: false },
}, { timestamps: true });
diff --git a/src/server/models/OrganizationMember.ts b/src/server/models/OrganizationMember.ts
index 04565d6..7742d03 100644
--- a/src/server/models/OrganizationMember.ts
+++ b/src/server/models/OrganizationMember.ts
@@ -3,7 +3,7 @@ import mongoose, { Schema, Document } from 'mongoose';
export type OrgRole = 'guest' | 'user' | 'admin';
export interface IOrganizationMember extends Document {
- clerkUserId: string;
+ userId: string;
organizationId: string;
role: OrgRole;
isBanned: boolean;
@@ -15,7 +15,7 @@ export interface IOrganizationMember extends Document {
}
const OrganizationMemberSchema: Schema = new Schema({
- clerkUserId: {
+ userId: {
type: String,
required: true,
index: true
@@ -47,6 +47,6 @@ const OrganizationMemberSchema: Schema = new Schema({
});
// Compound index for unique user per organization
-OrganizationMemberSchema.index({ clerkUserId: 1, organizationId: 1 }, { unique: true });
+OrganizationMemberSchema.index({ userId: 1, organizationId: 1 }, { unique: true });
export default mongoose.models.OrganizationMember || mongoose.model('OrganizationMember', OrganizationMemberSchema);
diff --git a/src/server/models/User.ts b/src/server/models/User.ts
index f3d5663..fd088db 100644
--- a/src/server/models/User.ts
+++ b/src/server/models/User.ts
@@ -3,8 +3,9 @@ import mongoose, { Schema, Document } from 'mongoose';
export type UserRole = 'guest' | 'user' | 'admin';
export interface IUser extends Document {
- clerkId: string;
+ clerkId?: string;
email: string;
+ password?: string;
name: string;
role: UserRole;
isBanned: boolean;
@@ -17,17 +18,23 @@ export interface IUser extends Document {
const UserSchema: Schema = new Schema({
clerkId: {
type: String,
- required: true,
unique: true,
+ sparse: true,
index: true
},
+ password: {
+ type: String,
+ select: false // Password shouldn't be returned by default
+ },
organizationId: {
type: String,
index: true
},
email: {
type: String,
- required: true
+ required: true,
+ unique: true,
+ index: true
},
name: {
type: String,
diff --git a/src/server/routes/authRoutes.ts b/src/server/routes/authRoutes.ts
new file mode 100644
index 0000000..6183402
--- /dev/null
+++ b/src/server/routes/authRoutes.ts
@@ -0,0 +1,11 @@
+import { Router } from 'express';
+import * as authController from '../controllers/authController.js';
+import { authenticateJWT } from '../middleware/authMiddleware.js';
+
+const router = Router();
+
+router.post('/login', authController.login);
+router.post('/register', authController.register);
+router.get('/me', authenticateJWT, authController.getMe);
+
+export default router;
diff --git a/src/server/routes/userRoutes.ts b/src/server/routes/userRoutes.ts
index 5486b0e..543f789 100644
--- a/src/server/routes/userRoutes.ts
+++ b/src/server/routes/userRoutes.ts
@@ -1,23 +1,23 @@
import express from 'express';
import { syncUser, getCurrentUser, getAllUsers, updateUserRole, toggleBanUser, heartbeat, getActiveUsers, deleteUser } from '../controllers/userController.js';
-import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
+import { requireAdmin, requireUser } from '../middleware/roleMiddleware.js';
const router = express.Router();
-// Sync user from Clerk (public - called on login)
+// Sync user (placeholder)
router.post('/sync', syncUser);
-// Get current user (requires extractUser middleware)
-router.get('/me', extractUser, getCurrentUser);
+// Get current user
+router.get('/me', requireUser, getCurrentUser);
// Heartbeat & Presence
-router.post('/heartbeat', extractUser, heartbeat);
-router.get('/active', extractUser, getActiveUsers);
+router.post('/heartbeat', requireUser, heartbeat);
+router.get('/active', requireUser, getActiveUsers);
// Admin-only routes
-router.get('/', extractUser, requireAdmin, getAllUsers);
-router.patch('/:id/role', extractUser, requireAdmin, updateUserRole);
-router.patch('/:id/ban', extractUser, requireAdmin, toggleBanUser);
-router.delete('/:id', extractUser, requireAdmin, deleteUser);
+router.get('/', requireUser, requireAdmin, getAllUsers);
+router.patch('/:id/role', requireUser, requireAdmin, updateUserRole);
+router.patch('/:id/ban', requireUser, requireAdmin, toggleBanUser);
+router.delete('/:id', requireUser, requireAdmin, deleteUser);
export default router;
diff --git a/src/server/scripts/migrate-admin.ts b/src/server/scripts/migrate-admin.ts
new file mode 100644
index 0000000..682dd2c
--- /dev/null
+++ b/src/server/scripts/migrate-admin.ts
@@ -0,0 +1,53 @@
+import mongoose from 'mongoose';
+import bcrypt from 'bcryptjs';
+import dotenv from 'dotenv';
+import path from 'path';
+
+dotenv.config({ path: path.join(process.cwd(), '.env') });
+
+const MONGODB_URI = process.env.MONGODB_URI || 'mongodb+srv://admtracksteel:29OHAHpKTI8XcCNt@cluster0.a4xiilu.mongodb.net/ts_gpi?retryWrites=true&w=majority&appName=Cluster0';
+
+const UserSchema = new mongoose.Schema({
+ email: { type: String, required: true, unique: true },
+ password: { type: String, required: true },
+ name: { type: String, required: true },
+ role: { type: String, enum: ['guest', 'user', 'admin'], default: 'guest' },
+ isBanned: { type: Boolean, default: false }
+}, { timestamps: true });
+
+async function migrateAdmin() {
+ try {
+ await mongoose.connect(MONGODB_URI);
+ console.log('✅ Conectado ao MongoDB');
+
+ const User = mongoose.models.User || mongoose.model('User', UserSchema);
+
+ const adminEmail = 'admtracksteel@gmail.com';
+ const defaultPassword = 'admin_gpi_2026'; // SENHA TEMPORÁRIA
+ const hashedPassword = await bcrypt.hash(defaultPassword, 12);
+
+ const admin = await User.findOneAndUpdate(
+ { email: adminEmail },
+ {
+ $set: {
+ password: hashedPassword,
+ role: 'admin',
+ name: 'Administrador Global',
+ isBanned: false
+ }
+ },
+ { upsert: true, new: true }
+ );
+
+ console.log(`✅ Administrador ${adminEmail} migrado com sucesso!`);
+ console.log(`🔑 Senha temporária definida: ${defaultPassword}`);
+ console.log(`⚠️ Por favor, altere sua senha após o primeiro login.`);
+
+ await mongoose.disconnect();
+ } catch (error) {
+ console.error('❌ Erro na migração:', error);
+ process.exit(1);
+ }
+}
+
+migrateAdmin();
|