Files
rx.scoreodonto.com/dental-server/routes/client-auth.js
T
VPS 4 Deploy Agent b6da56617a feat(rx): tenant_id (conta/dono) acima de clinica_id — multi-tenant 2 níveis
Hierarquia tenant_id (usuarios.id do dono) / clinica_id / pc / paciente, espelhando
o scoreodonto (conta multi-clínica planejada: um dono gerencia várias clínicas).

- Colunas tenant_id em clinics_devices e images (+índices); backfill device + 2776 imgs.
- Cadastro de device, socket (machine_token), JWT de client e uploads gravam tenant_id.
- Listagens (/images, /patients, /by-patient, /unique-clinics) filtram por tenant_id e/ou clinica_id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:26:06 +02:00

114 lines
3.7 KiB
JavaScript

const express = require('express');
const router = express.Router();
const db = require('../database');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { JWT_SECRET } = require('../auth');
// ==============================================================
// 1. LOGIN DO CLIENTE (APP DESKTOP)
// ==============================================================
router.post('/login', async (req, res) => {
const { email, password, machine_token } = req.body;
if (!email || !password || !machine_token) {
return res.status(400).json({
success: false,
message: 'Email, Senha e Token da Máquina são obrigatórios'
});
}
try {
// 1. Buscar o dispositivo pelo Token
const device = await db.get(`SELECT * FROM clinics_devices WHERE machine_token = $1`, [machine_token]);
if (!device) {
return res.status(401).json({ success: false, message: 'Token da máquina inválido ou inexistente' });
}
// 2. Verificar se o acesso está ativo
if (!device.is_active) {
return res.status(403).json({ success: false, message: 'O acesso desta máquina foi desativado pelo administrador' });
}
// 3. Verificar o Email
if (device.email !== email) {
return res.status(401).json({ success: false, message: 'Email incorreto para esta máquina' });
}
// 4. Verificar a Senha
const validPassword = await bcrypt.compare(password, device.password_hash);
if (!validPassword) {
return res.status(401).json({ success: false, message: 'Senha incorreta' });
}
// Atualizar último IP conectado
const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
await db.run(`UPDATE clinics_devices SET last_ip = $1 WHERE id = $2`, [clientIp, device.id]);
// 5. Gerar Token JWT de Sessão
// O Token vale por 2 horas, obrigando o cliente a re-logar ou pedir refresh
const sessionToken = jwt.sign(
{
clinicId: device.id,
tenantId: device.tenant_id, // conta/dono (scoreodonto)
clinicaId: device.clinica_id, // clínica (scoreodonto)
clinicName: device.clinic_name,
pcName: device.pc_name,
role: 'client_device'
},
JWT_SECRET,
{ expiresIn: '2h' }
);
res.json({
success: true,
message: 'Login aprovado',
token: sessionToken,
clinic: {
name: device.clinic_name,
pc: device.pc_name
}
});
} catch (error) {
console.error('Erro no login do cliente:', error);
res.status(500).json({ success: false, message: 'Erro interno de servidor' });
}
});
// ==============================================================
// 2. MIDDLEWARE DE VALIDAÇÃO (Para usar nas rotas de Upload)
// ==============================================================
const requireClientAuth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ success: false, message: 'Token JWT não fornecido' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
if (decoded.role !== 'client_device') {
return res.status(403).json({ success: false, message: 'Acesso negado: Perfil inválido' });
}
// Anexar info ao request para uso posterior
req.clientDevice = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ success: false, message: 'Sessão expirada. Faça login novamente.', expired: true });
}
return res.status(401).json({ success: false, message: 'Token JWT inválido' });
}
};
module.exports = {
router,
requireClientAuth,
JWT_SECRET
};