03019898b0
Добавлена офлайн/EndUser-подпись «ознакомлен и принял в работу» для документов отчётов и файловые вложения к задачам через Nextcloud. Co-authored-by: Cursor <cursoragent@cursor.com>
176 lines
7.1 KiB
JavaScript
176 lines
7.1 KiB
JavaScript
/**
|
|
* КЕП-подпись отчётов: офлайн-загрузка + опциональный ІІТ EndUser в браузере.
|
|
*
|
|
* EndUser (euSign) — проприетарная библиотека ІІТ. Положите её в
|
|
* /static/vendor/eusign/ и подключите в layout, либо задайте window.KEP_ENDUSER_READY.
|
|
* Без неё работает сценарий: скачать PDF → подписать офлайн → загрузить.
|
|
*/
|
|
(function () {
|
|
function page() {
|
|
return document.getElementById('kep-sign-page');
|
|
}
|
|
|
|
function csrfToken() {
|
|
var el = document.querySelector('#kep-offline-form input[name=csrfmiddlewaretoken]');
|
|
return el ? el.value : '';
|
|
}
|
|
|
|
function setSha(hex) {
|
|
var input = document.getElementById('kep-content-sha');
|
|
var line = document.getElementById('kep-sha-line');
|
|
if (input) input.value = hex || '';
|
|
if (line) {
|
|
line.textContent = hex
|
|
? ('SHA-256: ' + hex)
|
|
: '';
|
|
}
|
|
}
|
|
|
|
function endUserAvailable() {
|
|
if (window.KEP_ENDUSER_READY) return true;
|
|
if (typeof window.EndUser !== 'undefined') return true;
|
|
if (typeof window.euSign !== 'undefined') return true;
|
|
if (typeof window.EUSignCP !== 'undefined') return true;
|
|
return false;
|
|
}
|
|
|
|
function updateEndUserUi() {
|
|
var status = document.getElementById('kep-enduser-status');
|
|
var btn = document.getElementById('kep-browser-sign');
|
|
if (!status || !btn) return;
|
|
if (endUserAvailable()) {
|
|
status.textContent = 'Библиотека EndUser найдена. Можно подписать ключом в браузере.';
|
|
btn.disabled = false;
|
|
} else {
|
|
status.textContent = 'Библиотека EndUser не обнаружена — используйте офлайн-подпись ниже.';
|
|
btn.disabled = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Хук для интеграции ІІТ: задайте window.kepSignWithEndUser = async function(pdfBytes) {
|
|
* return { pdf: signedBytes, meta: { subject, serial, issuer, format: 'pades' } };
|
|
* }
|
|
*/
|
|
async function signWithEndUser(pdfBytes) {
|
|
if (typeof window.kepSignWithEndUser === 'function') {
|
|
return window.kepSignWithEndUser(pdfBytes);
|
|
}
|
|
throw new Error(
|
|
'Подключите ІІТ EndUser и задайте window.kepSignWithEndUser(pdfBytes).'
|
|
);
|
|
}
|
|
|
|
async function fetchPreparePdf(url) {
|
|
var resp = await fetch(url, { credentials: 'same-origin' });
|
|
if (!resp.ok) {
|
|
var text = await resp.text();
|
|
throw new Error(text || ('HTTP ' + resp.status));
|
|
}
|
|
var sha = resp.headers.get('X-Content-SHA256') || '';
|
|
var buf = await resp.arrayBuffer();
|
|
return { bytes: new Uint8Array(buf), sha: sha };
|
|
}
|
|
|
|
async function uploadSigned(completeUrl, pdfBytes, meta, sha) {
|
|
var fd = new FormData();
|
|
fd.append('csrfmiddlewaretoken', csrfToken());
|
|
fd.append('content_sha256', sha || '');
|
|
fd.append('cert_meta', JSON.stringify(meta || {}));
|
|
if (meta && meta.subject) fd.append('cert_subject', meta.subject);
|
|
if (meta && meta.serial) fd.append('cert_serial', meta.serial);
|
|
if (meta && meta.issuer) fd.append('cert_issuer', meta.issuer);
|
|
var blob = new Blob([pdfBytes], { type: 'application/pdf' });
|
|
fd.append('file', blob, 'signed.pdf');
|
|
var resp = await fetch(completeUrl, {
|
|
method: 'POST',
|
|
body: fd,
|
|
credentials: 'same-origin',
|
|
headers: { 'HX-Request': 'true' },
|
|
redirect: 'follow',
|
|
});
|
|
// complete делает redirect на страницу подписи — перезагрузим фрагмент
|
|
if (resp.redirected) {
|
|
window.location.href = resp.url;
|
|
return;
|
|
}
|
|
var html = await resp.text();
|
|
var target = document.querySelector('#main-content');
|
|
if (target && typeof htmx !== 'undefined' && htmx.swap) {
|
|
htmx.swap(target, html, { swapStyle: 'innerHTML' });
|
|
} else {
|
|
window.location.reload();
|
|
}
|
|
}
|
|
|
|
document.addEventListener('click', function (e) {
|
|
var prep = e.target && e.target.closest ? e.target.closest('[data-kep-prepare]') : null;
|
|
if (prep) {
|
|
e.preventDefault();
|
|
var root = page();
|
|
if (!root) return;
|
|
var url = root.getAttribute('data-prepare-url');
|
|
fetchPreparePdf(url).then(function (r) {
|
|
setSha(r.sha);
|
|
var blob = new Blob([r.bytes], { type: 'application/pdf' });
|
|
var a = document.createElement('a');
|
|
a.href = URL.createObjectURL(blob);
|
|
a.download = 'report-for-sign.pdf';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000);
|
|
}).catch(function (err) {
|
|
var line = document.getElementById('kep-sha-line');
|
|
if (line) line.textContent = (err && err.message) ? err.message : 'Не удалось скачать PDF';
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (e.target && e.target.id === 'kep-browser-sign') {
|
|
e.preventDefault();
|
|
var root = page();
|
|
if (!root) return;
|
|
var prepareUrl = root.getAttribute('data-prepare-url');
|
|
var completeUrl = root.getAttribute('data-complete-url');
|
|
var btn = e.target;
|
|
btn.disabled = true;
|
|
var status = document.getElementById('kep-enduser-status');
|
|
if (status) status.textContent = 'Готовим PDF…';
|
|
fetchPreparePdf(prepareUrl)
|
|
.then(function (r) {
|
|
setSha(r.sha);
|
|
if (status) status.textContent = 'Ожидание подписи ключом…';
|
|
return signWithEndUser(r.bytes).then(function (out) {
|
|
return uploadSigned(
|
|
completeUrl,
|
|
out.pdf || out.bytes || out,
|
|
out.meta || {},
|
|
r.sha
|
|
);
|
|
});
|
|
})
|
|
.catch(function (err) {
|
|
if (status) {
|
|
status.textContent = (err && err.message)
|
|
? err.message
|
|
: 'Не удалось подписать в браузере. Используйте офлайн-загрузку.';
|
|
}
|
|
btn.disabled = !endUserAvailable();
|
|
});
|
|
}
|
|
});
|
|
|
|
function boot() {
|
|
if (!page()) return;
|
|
updateEndUserUi();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', boot);
|
|
} else {
|
|
boot();
|
|
}
|
|
document.addEventListener('htmx:afterSwap', boot);
|
|
})();
|