[FEAT] #384 p.5 — allegati file su Policy (sezione Allegati nel dettaglio) + allowlist server-side upload evidenze

- policies.html: sezione 'Allegati' nel dettaglio policy (Bootstrap Italia card),
  upload (entity_type=policy) via /api/audit/evidence/upload, lista con download/autore/data
- AuditController::uploadEvidence: allowlist estensioni server-side (anti stored-XSS same-origin)
- version.json -> 1.23.5

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-06-18 19:00:01 +02:00
co-authored by Claude Opus 4.8
parent 6f3ca4b4bd
commit 8a8045904b
3 changed files with 109 additions and 1 deletions
+100
View File
@@ -627,7 +627,29 @@
</div>
</div>
</div>
<div class="card" style="margin-top:20px;">
<div class="card-header">
<h3>Allegati</h3>
</div>
<div class="card-body">
<div id="policy-attachments-${policy.id}" aria-live="polite">
<p style="color:var(--gray-500);font-size:0.85rem;margin:0;">Caricamento allegati...</p>
</div>
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:14px;padding-top:14px;border-top:1px solid var(--gray-200);">
<input type="file" id="policy-att-file-${policy.id}" class="form-input" style="flex:1;min-width:220px;"
accept=".pdf,.png,.jpg,.jpeg,.gif,.webp,.txt,.csv,.xlsx,.xls,.docx,.doc,.pptx,.ppt,.odt,.ods,.zip"
aria-label="Seleziona un file da allegare alla policy" aria-describedby="policy-att-hint-${policy.id}">
<button class="btn btn-primary" onclick="caricaAllegato(${policy.id})">
<svg viewBox="0 0 20 20" fill="currentColor" width="16" height="16"><path d="M10 3a1 1 0 01.7.29l3 3a1 1 0 11-1.4 1.42L11 6.4V13a1 1 0 11-2 0V6.4L7.7 7.71a1 1 0 01-1.4-1.42l3-3A1 1 0 0110 3zM4 15a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1z"/></svg>
Carica allegato
</button>
</div>
<p id="policy-att-hint-${policy.id}" style="font-size:0.78rem;color:var(--gray-500);margin:8px 0 0;">Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip. Dimensione massima 10 MB.</p>
</div>
</div>
`;
loadPolicyAttachments(policy.id);
}
function backToList() {
@@ -637,6 +659,84 @@
currentView = 'list';
}
// ── Allegati Policy (ticket #384 punto 5) ───────────────
const ATT_ALLOWED_EXT = ['pdf','png','jpg','jpeg','gif','webp','txt','csv','xlsx','xls','docx','doc','pptx','ppt','odt','ods','zip'];
function fmtFileSize(bytes) {
if (bytes === null || bytes === undefined || bytes === '') return '';
bytes = Number(bytes);
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
async function loadPolicyAttachments(policyId) {
const box = document.getElementById('policy-attachments-' + policyId);
if (!box) return;
try {
const headers = { 'Authorization': 'Bearer ' + api.token };
if (api.orgId) headers['X-Organization-Id'] = api.orgId;
const resp = await fetch(api.baseUrl + '/audit/evidence/list?entity_type=policy&entity_id=' + policyId, { headers });
const result = await resp.json();
const files = (result && result.success && Array.isArray(result.data)) ? result.data : [];
if (!files.length) {
box.innerHTML = '<p style="color:var(--gray-500);font-size:0.85rem;margin:0;">Nessun allegato. Carica un documento di supporto alla policy.</p>';
return;
}
box.innerHTML = files.map(f => `
<div style="display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--gray-100);">
<svg viewBox="0 0 20 20" fill="var(--primary, #0066CC)" width="18" height="18" style="flex-shrink:0;"><path d="M8 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0017.414 6L14 2.586A2 2 0 0012.586 2H8z"/></svg>
<a href="/uploads/${escapeHtml(f.file_path)}" target="_blank" rel="noopener" style="flex:1;word-break:break-all;">${escapeHtml(f.file_name)}</a>
<span style="color:var(--gray-500);font-size:0.78rem;white-space:nowrap;">${fmtFileSize(f.file_size)}</span>
<span style="color:var(--gray-500);font-size:0.78rem;white-space:nowrap;">${formatDate(f.created_at)}${f.uploaded_by_name ? ' &middot; ' + escapeHtml(f.uploaded_by_name) : ''}</span>
</div>
`).join('');
} catch (e) {
box.innerHTML = '<p style="color:var(--danger, #b91c1c);font-size:0.85rem;margin:0;">Errore nel caricamento degli allegati.</p>';
}
}
async function caricaAllegato(policyId) {
const input = document.getElementById('policy-att-file-' + policyId);
if (!input || !input.files || !input.files.length) {
showNotification('Seleziona un file da allegare.', 'warning');
return;
}
const file = input.files[0];
const ext = (file.name.split('.').pop() || '').toLowerCase();
if (!ATT_ALLOWED_EXT.includes(ext)) {
showNotification('Tipo di file non consentito. Formati ammessi: PDF, immagini, documenti Office, txt, csv, zip.', 'error');
return;
}
if (file.size > 10 * 1024 * 1024) {
showNotification('File troppo grande (max 10 MB).', 'error');
return;
}
const btn = input.parentElement.querySelector('.btn-primary');
setButtonLoading(btn, true);
try {
const fd = new FormData();
fd.append('file', file);
fd.append('entity_type', 'policy');
fd.append('entity_id', policyId);
const headers = { 'Authorization': 'Bearer ' + api.token };
if (api.orgId) headers['X-Organization-Id'] = api.orgId;
const resp = await fetch(api.baseUrl + '/audit/evidence/upload', { method: 'POST', headers, body: fd });
const result = await resp.json();
setButtonLoading(btn, false);
if (result && result.success) {
showNotification('Allegato caricato con successo.', 'success');
input.value = '';
loadPolicyAttachments(policyId);
} else {
showNotification((result && result.message) || 'Errore nel caricamento dell\'allegato.', 'error');
}
} catch (e) {
setButtonLoading(btn, false);
showNotification('Errore di connessione durante il caricamento.', 'error');
}
}
// ── Create / Edit Modal ─────────────────────────────────
function openCreateModal() {
showPolicyFormModal(null);
+1 -1
View File
@@ -1 +1 @@
{"version": "1.23.4", "build": "2026-06-18-v1.23.4", "date": "2026-06-18", "changelog": "Ticket #384 punto 2.1 (ricodifica): nella pagina 'Misure e Requisiti' i codici delle policy/procedure di default ora si presentano con il prefisso 'Policy.' al posto di 'Proc.' (es. Proc.01 -> Policy.01), in coerenza con l'unificazione lessicale Policy/Procedura del punto 1. Cambia solo l'etichetta mostrata a video: il codice canonico nel catalogo resta invariato come chiave di collegamento requisito<->policy. Restano in lavorazione a fasi: unificazione archivi M:N requisito<->policy (punto 2.2), link attivi bidirezionali tra i due menu (punto 3), allegati nel modale policy (punto 5), matrice RACI assegnabile per policy con R e A obbligatorie in approvazione (punto 6)."}
{"version": "1.23.5", "build": "2026-06-18-v1.23.5", "date": "2026-06-18", "changelog": "Ticket #384 punto 5 (allegati Policy): nel dettaglio di ogni Policy ora c'e' la sezione 'Allegati' per caricare documenti di supporto (PDF, immagini, Office, txt, csv, zip; max 10 MB), elencarli con autore/data e scaricarli. Hardening sicurezza: aggiunta allowlist server-side delle estensioni sull'endpoint di upload evidenze (rifiuto html/svg/js, anti stored-XSS same-origin). Restano in lavorazione a fasi: unificazione archivi M:N requisito<->policy (punto 2.2), link attivi bidirezionali tra i due menu (punto 3), matrice RACI assegnabile per policy con R e A obbligatorie in approvazione (punto 6)."}