turnero: integración QZ Tray para impresión silenciosa en kiosko

- Genera par de llaves RSA (cert en config/qz/)
- api/qz_sign.php firma los print jobs con SHA512
- assets/js/qz-print.js conecta QZ Tray y envía trabajo HTML
- ver_formulario_enviado.php carga QZ cuando ?autoprint=1
- Fallback a window.print() si QZ no está instalado

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-07-21 14:52:19 -05:00
co-authored by Claude Sonnet 4.6
parent 2cfb77b1a6
commit 5dd6ee5040
6 changed files with 133 additions and 1 deletions
+18
View File
@@ -0,0 +1,18 @@
<?php
/**
* POST /api/qz_sign.php
* Firma el request de QZ Tray con la llave privada RSA del servidor.
* QZ Tray verifica la firma usando el digital-certificate.txt instalado en el cliente.
*/
header('Content-Type: text/plain');
header('Access-Control-Allow-Origin: *');
$request = file_get_contents('php://input');
if (!$request) { http_response_code(400); exit('missing request'); }
$keyPath = __DIR__ . '/../config/qz/private-key.pem';
$key = openssl_pkey_get_private('file://' . $keyPath);
if (!$key) { http_response_code(500); exit('key error'); }
openssl_sign($request, $signature, $key, 'SHA512');
echo base64_encode($signature);
+48
View File
@@ -0,0 +1,48 @@
/**
* qz-print.js — Impresión silenciosa vía QZ Tray
* Requiere qz-tray.js cargado antes de este archivo.
* Uso: qzPrint({ url, printer })
*/
window.qzPrint = async function({ printer } = {}) {
try {
// 1. Conectar
if (!qz.websocket.isActive()) {
await qz.websocket.connect();
}
// 2. Firma con el servidor
qz.security.setSignatureAlgorithm('SHA512');
qz.security.setSignaturePromise(function(toSign) {
return function(resolve, reject) {
fetch('/api/qz_sign.php', { method: 'POST', body: toSign })
.then(r => r.text()).then(resolve).catch(reject);
};
});
// 3. Certificado público
qz.security.setCertificatePromise(function(resolve) {
fetch('/config/qz/digital-certificate.txt')
.then(r => r.text()).then(resolve);
});
// 4. Seleccionar impresora
var p = printer || await qz.printers.getDefault();
console.log('[QZ] Impresora:', p);
// 5. Imprimir la página actual como HTML
var cfg = qz.configs.create(p);
var data = [{
type : 'pixel',
format: 'html',
flavor: 'plain',
data : document.documentElement.outerHTML,
}];
await qz.print(cfg, data);
console.log('[QZ] Trabajo enviado correctamente');
} catch(e) {
console.warn('[QZ] Error:', e.message || e);
// Fallback al diálogo del navegador si QZ no está disponible
window.print();
}
};
+5
View File
@@ -0,0 +1,5 @@
# Bloquear acceso web a la llave privada
<Files "private-key.pem">
Order deny,allow
Deny from all
</Files>
+24
View File
@@ -0,0 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID+TCCAuGgAwIBAgIULkHXGPyy0w0ieUoN/YOZOO5MPvkwDQYJKoZIhvcNAQEL
BQAwgYsxCzAJBgNVBAYTAkNPMRswGQYDVQQIDBJOb3J0ZSBkZSBTYW50YW5kZXIx
DzANBgNVBAcMBkN1Y3V0YTEjMCEGA1UECgwaTGFib3JhdG9yaW8gWGltZW5hIENh
aWNlZG8xKTAnBgNVBAMMIGVycC5sYWJvcmF0b3Jpb3hpbWVuYWNhaWNlZG8uY29t
MB4XDTI2MDcyMTE5NTEzNloXDTM2MDcxODE5NTEzNlowgYsxCzAJBgNVBAYTAkNP
MRswGQYDVQQIDBJOb3J0ZSBkZSBTYW50YW5kZXIxDzANBgNVBAcMBkN1Y3V0YTEj
MCEGA1UECgwaTGFib3JhdG9yaW8gWGltZW5hIENhaWNlZG8xKTAnBgNVBAMMIGVy
cC5sYWJvcmF0b3Jpb3hpbWVuYWNhaWNlZG8uY29tMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAjbzIjSUEj6qjwHvg+qNAEk0rAjfezbpyGPGkbH1AFnmI
8rz2TZKAN5PWJSvjYS91YEtnfFttEDa0wQpvHOIPhn2l8gAsYNFoDS+i1ilXHBEA
OtAB7uVL2TvrZBTQtcab4upI1ehUjdqA9ae51KBEfqOD3k7dc5X8IeskBDMLfI5G
cuJejGpx6S8aFur/PfZfu3oApHW02b4GXqSguUHwBJZtGauFQWRrN/6VG3YVKXON
g8bDEyFgPCZ2Slp4pHoLK5BIjBkF1t3PUcSq5rRZCxjYzoOcDIfoVV5f4Y+QSspM
u3tI+cMAnqwrKma/TBmSSikTMuK9nb+qjVvlQ/kijwIDAQABo1MwUTAdBgNVHQ4E
FgQUVnCTxhnONKLUYtxlT4SsIPH5rEgwHwYDVR0jBBgwFoAUVnCTxhnONKLUYtxl
T4SsIPH5rEgwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEL/L
PP8b2UQavVLh3r0D3Is0V7JrVTRsT/iB2FwJLhudHAojVBinh7+tvZboEFkJ5trR
bNG3XxXDprw76awc/BsR19YXWrjZfraROrVNYEVTW1+Mit2HYRuTiEnVxOX1EENR
dAwZrwtFmpy3I2sZNblxQm0cZbDq6g4x/LBTwUXmXQK259Nrv5SEjs8wixeDMDNC
wuA9vAamsw+38nqHCouNtE+yPwM7srinFkfilMnIB2d874eqaVBvEKWmUtBCsWaO
f5OviyodKH9BtF+ACpgU2E/at03VratR5REIjje52qsWOMY1Wnn8FIYsCWJIPQYu
m56yKz4rQLPX/kyeCQ==
-----END CERTIFICATE-----
+28
View File
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCNvMiNJQSPqqPA
e+D6o0ASTSsCN97NunIY8aRsfUAWeYjyvPZNkoA3k9YlK+NhL3VgS2d8W20QNrTB
Cm8c4g+GfaXyACxg0WgNL6LWKVccEQA60AHu5UvZO+tkFNC1xpvi6kjV6FSN2oD1
p7nUoER+o4PeTt1zlfwh6yQEMwt8jkZy4l6ManHpLxoW6v899l+7egCkdbTZvgZe
pKC5QfAElm0Zq4VBZGs3/pUbdhUpc42DxsMTIWA8JnZKWnikegsrkEiMGQXW3c9R
xKrmtFkLGNjOg5wMh+hVXl/hj5BKyky7e0j5wwCerCsqZr9MGZJKKRMy4r2dv6qN
W+VD+SKPAgMBAAECggEAPqedtfsPrZx+f6ejN9hzicOQCA5/kMzjBBDJoOWrL2Qx
PDB45qikwiy5ZLwmav8qMVOT3v6hUyIDvDPrE0cBGvAvK6+U7oWTLAULRAWJStBf
HCB4Qk0dPt3Ee/zRmBFANspfQSPPQNe+2xj2Rj5EmQCaWerd7Or3xlymEq8n3Dp2
E9pFn4wiEDdjXG69/fqu/LoFpz31E2Mp0q+V4ZdWvMXA4U6akH2/oGWCnjDdhm/O
Wy1shQL5oVxdw/yhHhaC/3GZv/MKVbaW7JeBtnt3S5Szktr6A1O2obSJS+KOw9d+
Y2rqIMAWXv+kHKzAB2LtfczpfkEDkqQ7jCbu+sjX4QKBgQC+3uwfwM9mlxoCt93E
cerMyfrbssTjZjZSsxnHsGz+qmDr8TIyOrwcZnnYs6MNkuTJW6MvgN8hM8oO2arh
E3s0oWrbILD42rzVQUeLr7/N/JjUwK7yyEnfZX/hnQdilRsT+rxpXBIA0HfUheih
6KkddNoOyvs+wu1eOralO117LwKBgQC+Ge4ch1hicRchwX/Bu+h3cdkLVVgdI08+
O7vnSCuhj8Ead7z3C5XjEqJOs75xY+LF4UxPsrqTuCxpWIOupCSnRe+RGMcJdzbR
0i/leyL9fWhvHzP13R4T6sqAHqIXKgCfR7Hx99TW3Whc4N2GoqAnIrjqow+H+ic8
8Hrr+Ul2oQKBgQC1AyRbWLdYS6RXP5gJXR+X51UIVZlzLtQFyeSBBEfZnCselzdL
e3g6VtTnNjVEAjMG4uj3e/gfvMW7H6J2ocsONqbn+TDcUFUUyTvYtWvpJcyqt7Ey
fc/RFKkahZkjXNS5NejI4pAQRaPe4L+mDMeVL+Q8czOiaapC2tusB4i38QKBgCYh
G1Jbj03HcyVRI2ffYcQ7cJZGWvMVNvq7jnfYUPAJ3miJpbxDdZ/jB+0TPlqN91lL
VDwUFDo20ambmGX6BGQMsf1/Y8SxRayWJQc5SI5hjgXj0084N6U1DcLe4hIVWaSZ
A8cNt4IVTK58Z9JuYgMXgtGFPUM/2Ijvjygvix2hAoGBALSgjUzho5Jn/vAQza/M
k4q24RZ0kBJ1Go+/b3iSOieCCVKgow9224r/frcrFvXWzbNjhKs/HAV1bB9uCrxD
/V/1byH0XLYmYIKvhTqpTtBLCYXrv+2m+ox8pHrgPze4KuJy3VdN/V/Dj3CpY0VL
G5Uf9TfOiMk4EiZw/pBR1YT9
-----END PRIVATE KEY-----
+10 -1
View File
@@ -2762,7 +2762,16 @@ const topaz = (() => {
<?php endif; ?>
</script>
<?php if ($autoprint): ?>
<script>window.addEventListener('load', function() { setTimeout(function() { window.print(); }, 600); });</script>
<script src="https://cdn.jsdelivr.net/npm/qz-tray@2.2.4/qz-tray.js"></script>
<script src="/assets/js/qz-print.js"></script>
<script>
window.addEventListener('load', function() {
setTimeout(function() {
if (typeof qz !== 'undefined') { qzPrint(); }
else { window.print(); }
}, 800);
});
</script>
<?php endif; ?>
</body>
</html>