fix: serve Wasabi image URLs instead of local /uploads/ paths — prevent broken image links
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
This commit is contained in:
@@ -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 `
|
||||
<div class="image-card ${!image.enabled ? 'disabled' : ''}" data-id="${image.id}" onclick="handleImageClick(${image.id})">
|
||||
<div class="image-preview"><img class="preview-img" src="${thumbUrl}" alt="" loading="lazy" decoding="async"></div>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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));
|
||||
Binary file not shown.
Binary file not shown.
@@ -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'
|
||||
Reference in New Issue
Block a user