const express = require('express'); const router = express.Router(); const db = require('../database'); const { verifyPassword, generateToken, hashPassword, authenticateToken } = require('../auth'); // ================================================================ // Rate limiter simples em memória — 5 tentativas por IP em 15 min // ================================================================ const RATE_WINDOW_MS = 15 * 60 * 1000; const RATE_MAX = 5; const _loginAttempts = new Map(); function checkLoginRateLimit(ip) { const now = Date.now(); const attempts = (_loginAttempts.get(ip) || []).filter(t => now - t < RATE_WINDOW_MS); if (attempts.length >= RATE_MAX) return false; attempts.push(now); _loginAttempts.set(ip, attempts); return true; } function clearLoginRateLimit(ip) { _loginAttempts.delete(ip); } // ================================================================ // POST /api/auth/login - Login // ================================================================ router.post('/login', async (req, res) => { try { const ip = req.ip || req.connection.remoteAddress || 'unknown'; if (!checkLoginRateLimit(ip)) { return res.status(429).json({ error: 'Muitas tentativas. Aguarde 15 minutos.' }); } const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: 'Usuário e senha são obrigatórios' }); } // Buscar usuário const user = await db.get( 'SELECT * FROM users WHERE username = $1 OR email = $2', [username, username] ); if (!user) { return res.status(401).json({ error: 'Usuário ou senha inválidos' }); } // Verificar senha const validPassword = await verifyPassword(password, user.password_hash); if (!validPassword) { return res.status(401).json({ error: 'Usuário ou senha inválidos' }); } clearLoginRateLimit(ip); // Gerar token const token = generateToken(user); // Resposta res.json({ success: true, token: token, user: { id: user.id, username: user.username, email: user.email, clinic_name: user.clinic_name, pc_name: user.pc_name, is_admin: user.is_admin } }); } catch (error) { console.error('Erro no login:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); // ================================================================ // POST /api/auth/verify-client-credentials - Verificar credenciais do client // ================================================================ router.post('/verify-client-credentials', async (req, res) => { try { const { email, password, token } = req.body; // Obter a API KEY do ambiente ou usar uma padrão (fallback local temporário) const serverApiKey = process.env.CLIENT_API_KEY || 'rf-dental-secure-key-2026'; if (token) { if (token === serverApiKey) { return res.json({ success: true, message: 'API Key válida' }); } // Verificar se é um machine_token (UUID tem hífen) if (email && password && token.includes('-')) { console.log(`[AUTH] Tentando validar machine_token: ${token} para email: ${email}`); const device = await db.get(`SELECT * FROM clinics_devices WHERE machine_token = $1`, [token]); if (device && device.email === email && device.is_active) { const validPassword = await verifyPassword(password, device.password_hash); console.log(`[AUTH] validPassword=${validPassword}`); if (validPassword) { return res.json({ success: true, message: 'Machine Token e Credenciais válidas', clinic: { name: device.clinic_name, pc: device.pc_name } }); } } else { console.log(`[AUTH] Falha: device=${!!device}, is_active=${device?.is_active}`); } return res.status(401).json({ error: 'Credenciais da clínica ou Token inválidos' }); } // Se não for a API Key padrão nem machine_token, tentamos verificar se é um JWT try { const jwt = require('jsonwebtoken'); const { JWT_SECRET } = require('../auth'); const decoded = jwt.verify(token, JWT_SECRET); if (decoded) { return res.json({ success: true, message: 'JWT válido' }); } } catch (e) { return res.status(401).json({ error: 'Token ou API Key inválidos' }); } } // Fallback se não mandou token mas mandou email/senha (Não é o padrão atual do client, mas por segurança) if (email && password) { const user = await db.get('SELECT * FROM users WHERE email = $1 OR username = $2', [email, email]); if (user) { const validPassword = await verifyPassword(password, user.password_hash); if (validPassword) { return res.json({ success: true, message: 'Credenciais válidas' }); } } } res.status(401).json({ error: 'Credenciais inválidas ou ausentes' }); } catch (error) { console.error('Erro na validação do client:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); // ================================================================ // GET /api/auth/verify - Verificar token // ================================================================ router.get('/verify', async (req, res) => { try { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Token não fornecido' }); } const jwt = require('jsonwebtoken'); const decoded = jwt.verify(token, require('../auth').JWT_SECRET); const user = await db.get('SELECT * FROM users WHERE id = $1', [decoded.id]); if (!user) { return res.status(404).json({ error: 'Usuário não encontrado' }); } res.json({ valid: true, user: { id: user.id, username: user.username, email: user.email, clinic_name: user.clinic_name, pc_name: user.pc_name, is_admin: user.is_admin } }); } catch (error) { res.status(401).json({ error: 'Token inválido' }); } }); // ================================================================ // GET /api/auth/me - Retorna dados do perfil do usuário logado // Usado pela tela "Minha Conta" do mobile // ================================================================ router.get('/me', authenticateToken, async (req, res) => { try { const user = await db.get( 'SELECT id, username, email, clinic_name, pc_name, is_admin, created_at FROM users WHERE id = $1', [req.user.id] ); if (!user) { return res.status(404).json({ error: 'Usuário não encontrado' }); } res.json({ success: true, user }); } catch (error) { console.error('Erro ao buscar perfil:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); // ================================================================ // POST /api/auth/logout - Logout // ================================================================ router.post('/logout', (req, res) => { res.json({ success: true, message: 'Logout realizado com sucesso' }); }); // ================================================================ // POST /api/auth/create-user - Criar usuário (admin only) // ================================================================ router.post('/create-user', authenticateToken, async (req, res) => { try { if (!req.user.is_admin) { return res.status(403).json({ error: 'Apenas administradores podem criar usuários' }); } const { username, email, password, clinic_name, pc_name, is_admin } = req.body; if (!username || !email || !password) { return res.status(400).json({ error: 'Username, email e senha são obrigatórios' }); } // Verificar se usuário já existe const existing = await db.get( 'SELECT * FROM users WHERE username = $1 OR email = $2', [username, email] ); if (existing) { return res.status(409).json({ error: 'Usuário ou email já cadastrado' }); } // Hash da senha const passwordHash = await hashPassword(password); // Criar usuário const result = await db.run( `INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin, created_at) VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP) RETURNING id`, [username, email, passwordHash, clinic_name || null, pc_name || null, !!is_admin] ); res.json({ success: true, message: 'Usuário criado com sucesso', userId: result.lastID }); } catch (error) { console.error('Erro ao criar usuário:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); // ================================================================ // GERENCIAMENTO DE USUÁRIOS/CLÍNICAS (ADMIN) // ================================================================ router.get('/users', authenticateToken, async (req, res) => { try { if (!req.user.is_admin) { return res.status(403).json({ error: 'Acesso negado' }); } const users = await db.all('SELECT id, username, email, clinic_name, pc_name, is_admin, created_at FROM users ORDER BY created_at DESC'); res.json({ success: true, users }); } catch (error) { res.status(500).json({ error: 'Erro ao buscar usuários' }); } }); router.delete('/users/:id', authenticateToken, async (req, res) => { try { if (!req.user.is_admin) { return res.status(403).json({ error: 'Acesso negado' }); } const userId = req.params.id; if (parseInt(userId) === req.user.id) { return res.status(400).json({ error: 'Não é possível excluir o próprio usuário admin' }); } await db.run('DELETE FROM users WHERE id = $1', [userId]); res.json({ success: true, message: 'Usuário excluído com sucesso' }); } catch (error) { res.status(500).json({ error: 'Erro ao excluir usuário' }); } }); router.post('/users/:id/token', authenticateToken, async (req, res) => { try { if (!req.user.is_admin) { return res.status(403).json({ error: 'Acesso negado' }); } const user = await db.get('SELECT * FROM users WHERE id = $1', [req.params.id]); if (!user) return res.status(404).json({ error: 'Usuário não encontrado' }); // Gera um token com duração longa para uso no desktop (ex: 3650 dias = ~10 anos) const token = generateToken(user, '3650d'); res.json({ success: true, token }); } catch (error) { res.status(500).json({ error: 'Erro ao gerar token' }); } }); // ================================================================ // PUT /api/auth/update-credentials - Alterar usuário/senha // ================================================================ router.put('/update-credentials', authenticateToken, async (req, res) => { try { const { currentPassword, newUsername, newEmail, newPassword } = req.body; const userId = req.user.id; if (!currentPassword) { return res.status(400).json({ error: 'A senha atual é obrigatória' }); } if (!newUsername && !newEmail && !newPassword) { return res.status(400).json({ error: 'Nenhum dado novo foi fornecido para atualização' }); } // Buscar usuário atual para verificar a senha const user = await db.get('SELECT * FROM users WHERE id = $1', [userId]); if (!user) { return res.status(404).json({ error: 'Usuário não encontrado' }); } // Verificar senha atual const validPassword = await verifyPassword(currentPassword, user.password_hash); if (!validPassword) { return res.status(401).json({ error: 'A senha atual está incorreta' }); } let finalUsername = user.username; let finalEmail = user.email; let finalPasswordHash = user.password_hash; // Verificar e aplicar novo username se fornecido if (newUsername && newUsername !== user.username) { const existing = await db.get('SELECT * FROM users WHERE username = $1 AND id != $2', [newUsername, userId]); if (existing) { return res.status(409).json({ error: 'Este nome de usuário já está em uso' }); } finalUsername = newUsername; } // Verificar e aplicar novo e-mail se fornecido if (newEmail && newEmail !== user.email) { const existing = await db.get('SELECT * FROM users WHERE email = $1 AND id != $2', [newEmail, userId]); if (existing) { return res.status(409).json({ error: 'Este e-mail já está em uso' }); } finalEmail = newEmail; } // Verificar e aplicar nova senha se fornecido if (newPassword) { if (newPassword.length < 4) { return res.status(400).json({ error: 'A nova senha deve ter pelo menos 4 caracteres' }); } finalPasswordHash = await hashPassword(newPassword); } // Atualizar no banco de dados await db.run( 'UPDATE users SET username = $1, email = $2, password_hash = $3 WHERE id = $4', [finalUsername, finalEmail, finalPasswordHash, userId] ); // Gerar novo token pois o username ou e-mail podem ter mudado const updatedUser = { id: userId, username: finalUsername, email: finalEmail, is_admin: user.is_admin }; const newToken = generateToken(updatedUser); res.json({ success: true, message: 'Credenciais atualizadas com sucesso', token: newToken, user: updatedUser }); } catch (error) { console.error('Erro ao atualizar credenciais:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); // ================================================================ // API TOKENS ESTÁTICOS // ================================================================ router.get('/tokens', authenticateToken, async (req, res) => { try { if (!req.user.is_admin) { return res.status(403).json({ error: 'Acesso negado' }); } const tokens = await db.all('SELECT id, name, token, created_at, last_used_at FROM api_tokens ORDER BY created_at DESC'); res.json({ success: true, tokens }); } catch (error) { console.error('Erro ao listar tokens:', error); res.status(500).json({ error: 'Erro ao buscar tokens' }); } }); router.post('/tokens', authenticateToken, async (req, res) => { try { if (!req.user.is_admin) { return res.status(403).json({ error: 'Acesso negado' }); } const { name } = req.body; if (!name || name.trim() === '') { return res.status(400).json({ error: 'O nome da integração é obrigatório' }); } const crypto = require('crypto'); const token = 'api_' + crypto.randomBytes(24).toString('hex'); await db.run( 'INSERT INTO api_tokens (name, token, created_at) VALUES ($1, $2, CURRENT_TIMESTAMP)', [name, token] ); res.json({ success: true, token, name }); } catch (error) { console.error('Erro ao gerar token:', error); res.status(500).json({ error: 'Erro ao gerar token' }); } }); router.delete('/tokens/:id', authenticateToken, async (req, res) => { try { if (!req.user.is_admin) { return res.status(403).json({ error: 'Acesso negado' }); } await db.run('DELETE FROM api_tokens WHERE id = $1', [req.params.id]); res.json({ success: true }); } catch (error) { console.error('Erro ao deletar token:', error); res.status(500).json({ error: 'Erro ao excluir token' }); } }); module.exports = router;