diff --git a/dental-server/public/app.js b/dental-server/public/app.js
index 487c1ae..e36d4bb 100644
--- a/dental-server/public/app.js
+++ b/dental-server/public/app.js
@@ -451,7 +451,7 @@ async function loadPatients(silent = false, loadMore = false) {
// Template de um cartão de paciente (reutilizado no render completo e no append)
function patientCardHTML(p, index) {
const fullName = p.patient_name || 'Sem nome';
- const thumb = p.thumb_filename ? `/uploads/${p.thumb_filename}` : '';
+ const thumb = p.thumb_url || (p.thumb_filename ? `/uploads/${p.thumb_filename}` : '');
const count = p.image_count || 0;
const date = formatDate(p.last_date);
const doctor = p.doctor || '—';
@@ -610,7 +610,7 @@ async function loadPatientImages(patientName) {
function renderPatientImages() {
imagesGrid.innerHTML = patientImages.map(image => {
- const thumbUrl = image.thumb_filename ? `/uploads/${image.thumb_filename}` : `/uploads/${image.filename}`;
+ const thumbUrl = image.thumb_url || (image.thumb_filename ? `/uploads/${image.thumb_filename}` : (image.file_url || `/uploads/${image.filename}`));
return `

diff --git a/dental-server/routes/images.js b/dental-server/routes/images.js
index fbe5028..ec426b0 100644
--- a/dental-server/routes/images.js
+++ b/dental-server/routes/images.js
@@ -159,6 +159,21 @@ router.get('/patients', async (req, res) => {
params
);
+ // Converter thumb_filename para URL completa (Wasabi ou local)
+ for (const row of rows) {
+ if (row.thumb_filename) {
+ try {
+ const thumbUrl = await storage.getImageUrl(row.thumb_filename, row.client_name, row.patient_name);
+ row.thumb_url = thumbUrl || `/uploads/${row.thumb_filename}`;
+ } catch (e) {
+ console.error('Erro ao gerar URL de thumbnail:', e);
+ row.thumb_url = `/uploads/${row.thumb_filename}`;
+ }
+ } else {
+ row.thumb_url = '';
+ }
+ }
+
res.json(rows);
} catch (error) {
console.error('Erro ao listar pacientes:', error);
@@ -186,6 +201,24 @@ router.get('/by-patient', async (req, res) => {
[patientName, showDisabled ? 0 : 1]
);
+ // Converter filenames para URLs completas (Wasabi ou local)
+ for (const image of images) {
+ try {
+ if (image.filename) {
+ const fileUrl = await storage.getImageUrl(image.filename, image.client_name, image.patient_name);
+ image.file_url = fileUrl || `/uploads/${image.filename}`;
+ }
+ if (image.thumb_filename) {
+ const thumbUrl = await storage.getImageUrl(image.thumb_filename, image.client_name, image.patient_name);
+ image.thumb_url = thumbUrl || `/uploads/${image.thumb_filename}`;
+ }
+ } catch (e) {
+ console.error('Erro ao gerar URL para imagem:', e);
+ image.file_url = image.file_url || `/uploads/${image.filename}`;
+ image.thumb_url = image.thumb_url || `/uploads/${image.thumb_filename}`;
+ }
+ }
+
res.json(images);
} catch (error) {
console.error('Erro ao buscar imagens do paciente:', error);
@@ -462,11 +495,22 @@ router.delete('/:id', async (req, res) => {
// ================================================================
router.get('/:id/file-url', async (req, res) => {
try {
- const image = await db.get('SELECT filename FROM images WHERE id = $1', [req.params.id]);
+ const image = await db.get('SELECT filename, client_name, patient_name FROM images WHERE id = $1', [req.params.id]);
if (!image) return res.status(404).json({ error: 'Imagem não encontrada' });
- const url = image.filename.startsWith('http')
- ? image.filename
- : `/uploads/${image.filename}`;
+
+ let url;
+ if (image.filename.startsWith('http')) {
+ url = image.filename;
+ } else {
+ // Se o arquivo está no Wasabi, reconstroem a URL
+ try {
+ const wasabiUrl = await storage.getImageUrl(image.filename, image.client_name, image.patient_name);
+ url = wasabiUrl || `/uploads/${image.filename}`;
+ } catch (e) {
+ console.error('Erro ao gerar URL Wasabi:', e);
+ url = `/uploads/${image.filename}`;
+ }
+ }
res.json({ url });
} catch (e) {
res.status(500).json({ error: e.message });
diff --git a/dental-server/storage.js b/dental-server/storage.js
index ed063b1..f40af73 100644
--- a/dental-server/storage.js
+++ b/dental-server/storage.js
@@ -105,6 +105,32 @@ function sanitizePathSegment(segment) {
return segment.trim().replace(/[\/\\?#%*:"<>|]/g, '_');
}
+/**
+ * Retorna a URL assinada de uma imagem no Wasabi (ou null se não estiver lá ou Wasabi desativado)
+ */
+async function getImageUrl(filename, clientName, patientName, isProcessed = false, expirySeconds = 3600) {
+ if (!s3 || !currentBucket || !clientName || !patientName) {
+ return null;
+ }
+
+ try {
+ const cleanClient = sanitizePathSegment(clientName);
+ const cleanPatient = sanitizePathSegment(patientName);
+ const key = `${cleanClient}/${cleanPatient}/${filename}`;
+
+ // Gerar URL assinada com expiração (padrão: 1 hora)
+ const url = await getSignedUrl(
+ s3,
+ new GetObjectCommand({ Bucket: currentBucket, Key: key }),
+ { expiresIn: expirySeconds }
+ );
+ return url;
+ } catch (error) {
+ console.error(`⚠️ [Wasabi] Erro ao gerar URL assinada para ${filename}:`, error.message);
+ return null;
+ }
+}
+
/**
* Salva a imagem. Primeiro no disco local e depois, se o Wasabi estiver ativo, faz o upload.
* Remove a versão local temporária se o upload pro Wasabi der certo.
@@ -335,6 +361,7 @@ module.exports = {
loadConfigFromDb,
saveImage,
getImageBuffer,
+ getImageUrl,
deleteImage,
getDownloadPresignedUrl,
getUploadPresignedUrl,
diff --git a/dental-server/test-bcrypt.js b/dental-server/test-bcrypt.js
new file mode 100644
index 0000000..5dd1f5a
--- /dev/null
+++ b/dental-server/test-bcrypt.js
@@ -0,0 +1,13 @@
+const bcrypt = require('bcrypt');
+const hash = '$2b$10$hE94QZePo4G3a0L.ybjHUOtTLmGFcIpicXbstb6rvsV78Yehzez9S';
+const pass = 'admin1234';
+const pass2 = 'Scoreodonto';
+const pass3 = 'scoreodonto';
+
+async function test() {
+ console.log("admin1234:", await bcrypt.compare('admin1234', hash));
+ console.log("Scoreodonto:", await bcrypt.compare('Scoreodonto', hash));
+ console.log("scoreodonto:", await bcrypt.compare('scoreodonto', hash));
+ process.exit(0);
+}
+test();
diff --git a/dental-server/test-db.js b/dental-server/test-db.js
new file mode 100644
index 0000000..927c90a
--- /dev/null
+++ b/dental-server/test-db.js
@@ -0,0 +1,26 @@
+const { Pool } = require('pg');
+
+const pool = new Pool({
+ host: process.env.DB_HOST || '10.99.0.3',
+ port: process.env.DB_PORT || 5432,
+ user: process.env.DB_USER || 'clube67',
+ database: process.env.DB_NAME || 'dental_images',
+ password: process.env.DB_PASSWORD || ''
+});
+
+async function run() {
+ try {
+ const res = await pool.query(`
+ SELECT column_name, data_type
+ FROM information_schema.columns
+ WHERE table_name = 'images';
+ `);
+ console.table(res.rows);
+ } catch(e) {
+ console.error(e);
+ } finally {
+ pool.end();
+ }
+}
+
+run();
diff --git a/dental-server/test-db2.js b/dental-server/test-db2.js
new file mode 100644
index 0000000..48a2cb4
--- /dev/null
+++ b/dental-server/test-db2.js
@@ -0,0 +1,18 @@
+const { Pool } = require('pg');
+const pool = new Pool({
+ host: '10.99.0.3',
+ port: 5432,
+ user: 'clube67',
+ password: 'clube67_db_pass_9903',
+ database: 'dental_images'
+});
+
+async function test() {
+ const token = 'bab18d5a-09af-4759-b742-4e1eeb651694';
+ const email = 'ruibto@gmail.com';
+ console.log("Looking up token:", token);
+ const res = await pool.query(`SELECT * FROM clinics_devices WHERE machine_token = $1`, [token]);
+ console.log("Device found:", res.rows[0] || 'NOT FOUND');
+ process.exit(0);
+}
+test();
diff --git a/dental-server/test-wasabi-url.js b/dental-server/test-wasabi-url.js
new file mode 100644
index 0000000..f3b33f8
--- /dev/null
+++ b/dental-server/test-wasabi-url.js
@@ -0,0 +1,19 @@
+const db = require('./database');
+const storage = require('./storage');
+
+async function test() {
+ await db.initDatabase();
+ await storage.loadConfigFromDb();
+ const url = await storage.getDownloadPresignedUrl('1779978903531_1779978896458-6xwema4i0.png');
+ console.log('Presigned URL:', url);
+
+ if (url) {
+ const fetch = require('node-fetch');
+ const res = await fetch(url);
+ console.log('Status:', res.status);
+ const text = await res.text();
+ console.log('Response body:', text.substring(0, 200));
+ }
+}
+
+test().catch(console.error).finally(() => process.exit(0));
diff --git a/dental-server/updates/ScoreOdonto Dental Client Setup 1.2.5.exe b/dental-server/updates/ScoreOdonto Dental Client Setup 1.2.5.exe
new file mode 100644
index 0000000..72441cc
Binary files /dev/null and b/dental-server/updates/ScoreOdonto Dental Client Setup 1.2.5.exe differ
diff --git a/dental-server/updates/ScoreOdonto Dental Client Setup 1.2.6.exe b/dental-server/updates/ScoreOdonto Dental Client Setup 1.2.6.exe
new file mode 100644
index 0000000..2310ecf
Binary files /dev/null and b/dental-server/updates/ScoreOdonto Dental Client Setup 1.2.6.exe differ
diff --git a/dental-server/updates/latest.yml b/dental-server/updates/latest.yml
new file mode 100644
index 0000000..f9ab2c0
--- /dev/null
+++ b/dental-server/updates/latest.yml
@@ -0,0 +1,9 @@
+version: 1.2.6
+files:
+ - url: ScoreOdonto Dental Client Setup 1.2.6.exe
+ sha512: zEYuIRBUTlpGa1QtdUy7NOIKPJ6g5BtfvMIJRnfV6kCstcSfc0YhOaT+I5YlNu/96peTloPnpkESxMQxg0qhBA==
+ size: 102459655
+ isAdminRightsRequired: true
+path: ScoreOdonto Dental Client Setup 1.2.6.exe
+sha512: zEYuIRBUTlpGa1QtdUy7NOIKPJ6g5BtfvMIJRnfV6kCstcSfc0YhOaT+I5YlNu/96peTloPnpkESxMQxg0qhBA==
+releaseDate: '2026-05-28T03:45:37.308Z'