nis2-agile/public/js/api.js
DevEnv nis2-agile 91043d7391 [FEAT] Gap Analysis ACN: frontend (acn-gap.html + sidebar + api.js)
- public/acn-gap.html: catalogo requisiti consultabile, wizard per funzione FW
  con accordion misure/requisiti, opzioni attuato/parziale/non attuato/N.A.,
  autosave batch debounce, risultati con punteggio per funzione + piano d'azione
  gap + analisi AI. Badge livello soggetto (importante/essenziale). IT/EN inline.
- api.js: metodi acn* (catalog/list/create/requirements/respond/complete/report/
  aiAnalyze) che spacchettano l'envelope e lanciano errore su success=false.
- common.js: voce sidebar "Gap Analysis ACN".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:10:50 +02:00

385 lines
23 KiB
JavaScript

/**
* NIS2 Agile - API Client
*
* Client JavaScript per comunicare con il backend REST API.
*/
class NIS2API {
constructor(baseUrl = '/api') {
this.baseUrl = baseUrl;
this.token = localStorage.getItem('nis2_access_token');
this.refreshToken = localStorage.getItem('nis2_refresh_token');
this.orgId = localStorage.getItem('nis2_org_id');
}
// ═══════════════════════════════════════════════════════════════════
// HTTP Methods
// ═══════════════════════════════════════════════════════════════════
async request(method, endpoint, data = null, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
if (this.orgId) {
headers['X-Organization-Id'] = this.orgId;
}
const config = { method, headers };
if (data && (method === 'POST' || method === 'PUT')) {
config.body = JSON.stringify(data);
}
try {
const response = await fetch(url, config);
const json = await response.json();
// Token expired - try refresh
if (response.status === 401 && this.refreshToken && !options._isRetry) {
const refreshed = await this.doRefreshToken();
if (refreshed) {
return this.request(method, endpoint, data, { ...options, _isRetry: true });
}
}
if (!json.success && !options.silent) {
console.error(`[API] ${method} ${endpoint}:`, json.message);
}
return json;
} catch (error) {
console.error(`[API] Network error: ${method} ${endpoint}`, error);
const msg = typeof I18n !== 'undefined' ? I18n.t('msg.error_connection') : 'Errore di connessione al server';
return { success: false, message: msg };
}
}
get(endpoint) { return this.request('GET', endpoint); }
post(endpoint, data) { return this.request('POST', endpoint, data); }
put(endpoint, data) { return this.request('PUT', endpoint, data); }
del(endpoint) { return this.request('DELETE', endpoint); }
// ═══════════════════════════════════════════════════════════════════
// Auth
// ═══════════════════════════════════════════════════════════════════
async login(email, password) {
const result = await this.post('/auth/login', { email, password });
if (result.success) {
this.setTokens(result.data.access_token, result.data.refresh_token);
this.setUserRole(result.data.user.role);
if (result.data.organizations && result.data.organizations.length > 0) {
const primary = result.data.organizations.find(o => o.is_primary) || result.data.organizations[0];
this.setOrganization(primary.organization_id);
}
}
return result;
}
async register(email, password, fullName, roleOrType = 'azienda') {
// Supporta sia i nuovi role NIS2 diretti (compliance_manager, org_admin, etc.)
// che il vecchio user_type (azienda, consultant) per retrocompatibilità
const result = await this.post('/auth/register', {
email, password, full_name: fullName,
role: roleOrType,
user_type: roleOrType, // backward compat
});
if (result.success) {
this.setTokens(result.data.access_token, result.data.refresh_token);
localStorage.setItem('nis2_user_role', result.data.user.role);
}
return result;
}
async doRefreshToken() {
try {
const result = await this.post('/auth/refresh', { refresh_token: this.refreshToken });
if (result.success) {
this.setTokens(result.data.access_token, result.data.refresh_token);
return true;
}
} catch (e) { /* ignore */ }
this.logout();
return false;
}
logout() {
this.post('/auth/logout', {}).catch(() => {});
this.clearTokens();
window.location.href = '/login.html';
}
getMe() { return this.get('/auth/me'); }
setTokens(access, refresh) {
this.token = access;
this.refreshToken = refresh;
localStorage.setItem('nis2_access_token', access);
localStorage.setItem('nis2_refresh_token', refresh);
}
clearTokens() {
this.token = null;
this.refreshToken = null;
this.orgId = null;
localStorage.removeItem('nis2_access_token');
localStorage.removeItem('nis2_refresh_token');
localStorage.removeItem('nis2_org_id');
localStorage.removeItem('nis2_user_role');
}
// Salva ruolo utente al login
setUserRole(role) {
localStorage.setItem('nis2_user_role', role);
}
getUserRole() {
return localStorage.getItem('nis2_user_role');
}
isConsultant() {
return this.getUserRole() === 'consultant';
}
setOrganization(orgId) {
this.orgId = orgId;
localStorage.setItem('nis2_org_id', orgId);
}
isAuthenticated() {
return !!this.token;
}
// ═══════════════════════════════════════════════════════════════════
// Organizations
// ═══════════════════════════════════════════════════════════════════
createOrganization(data) { return this.post('/organizations/create', data); }
getCurrentOrg() { return this.get('/organizations/current'); }
listOrganizations() { return this.get('/organizations/list'); }
updateOrganization(id, data) { return this.put(`/organizations/${id}`, data); }
classifyEntity(data) { return this.post('/organizations/classify', data); }
// ═══════════════════════════════════════════════════════════════════
// Assessments
// ═══════════════════════════════════════════════════════════════════
listAssessments() { return this.get('/assessments/list'); }
createAssessment(data) { return this.post('/assessments/create', data); }
getAssessment(id) { return this.get(`/assessments/${id}`); }
getAssessmentQuestions(id) { return this.get(`/assessments/${id}/questions`); }
saveAssessmentResponse(id, data) { return this.post(`/assessments/${id}/respond`, data); }
completeAssessment(id) { return this.post(`/assessments/${id}/complete`, {}); }
getAssessmentReport(id) { return this.get(`/assessments/${id}/report`); }
aiAnalyzeAssessment(id) { return this.post(`/assessments/${id}/ai-analyze`, {}); }
// ═══════════════════════════════════════════════════════════════════
// Gap Analysis ACN (Determinazione 164179/2025 - misure/requisiti)
// I metodi acn* ritornano direttamente il payload `data` e lanciano
// un errore {message, error_code} se success=false (per il try/catch UI).
// ═══════════════════════════════════════════════════════════════════
async _acn(promise) {
const r = await promise;
if (!r || !r.success) {
const err = new Error((r && r.message) || 'Errore');
err.error_code = r && (r.error_code || (r.data && r.data.error_code));
err.data = r && r.data;
throw err;
}
return r.data;
}
acnCatalog() { return this._acn(this.get('/acn-gap/catalog')); }
acnList() { return this._acn(this.get('/acn-gap/list')); }
acnCreate(data) { return this._acn(this.post('/acn-gap/create', data || {})); }
acnGet(id) { return this._acn(this.get(`/acn-gap/${id}`)); }
acnRequirements(id) { return this._acn(this.get(`/acn-gap/${id}/requirements`)); }
acnRespond(id, data) { return this._acn(this.post(`/acn-gap/${id}/respond`, data)); }
acnComplete(id) { return this._acn(this.post(`/acn-gap/${id}/complete`, {})); }
acnReport(id) { return this._acn(this.get(`/acn-gap/${id}/report`)); }
acnAiAnalyze(id) { return this._acn(this.post(`/acn-gap/${id}/aiAnalyze`, {})); }
// ═══════════════════════════════════════════════════════════════════
// Dashboard
// ═══════════════════════════════════════════════════════════════════
getDashboardOverview() { return this.get('/dashboard/overview'); }
getComplianceScore() { return this.get('/dashboard/compliance-score'); }
getUpcomingDeadlines() { return this.get('/dashboard/upcoming-deadlines'); }
getRecentActivity() { return this.get('/dashboard/recent-activity'); }
getRiskHeatmap() { return this.get('/dashboard/risk-heatmap'); }
// ═══════════════════════════════════════════════════════════════════
// Risks
// ═══════════════════════════════════════════════════════════════════
listRisks(params = {}) { return this.get('/risks/list?' + new URLSearchParams(params)); }
createRisk(data) { return this.post('/risks/create', data); }
getRisk(id) { return this.get(`/risks/${id}`); }
updateRisk(id, data) { return this.put(`/risks/${id}`, data); }
deleteRisk(id) { return this.del(`/risks/${id}`); }
getRiskMatrix() { return this.get('/risks/matrix'); }
aiSuggestRisks() { return this.post('/risks/ai-suggest', {}); }
// FAIR quantitativo + KRI (P2)
computeFair(id, data) { return this.post(`/risks/${id}/fair`, data); }
getFairRegister() { return this.get('/risks/fairRegister'); }
listKri() { return this.get('/risks/kri'); }
createKri(data) { return this.post('/risks/kri', data); }
updateKri(id, data) { return this.put(`/risks/kri/${id}`, data); }
// ═══════════════════════════════════════════════════════════════════
// Incidents
// ═══════════════════════════════════════════════════════════════════
listIncidents(params = {}) { return this.get('/incidents/list?' + new URLSearchParams(params)); }
createIncident(data) { return this.post('/incidents/create', data); }
getIncident(id) { return this.get(`/incidents/${id}`); }
updateIncident(id, data) { return this.put(`/incidents/${id}`, data); }
sendEarlyWarning(id) { return this.post(`/incidents/${id}/early-warning`, {}); }
sendNotification(id) { return this.post(`/incidents/${id}/notification`, {}); }
sendFinalReport(id) { return this.post(`/incidents/${id}/final-report`, {}); }
aiClassifyIncident(id) { return this.post(`/incidents/${id}/aiClassify`, {}); }
getIncidentMetrics(id) { return this.get(`/incidents/${id}/metrics`); }
getIncidentPir(id) { return this.get(`/incidents/${id}/pir`); }
saveIncidentPir(id, data) { return this.post(`/incidents/${id}/pir`, data); }
// ═══════════════════════════════════════════════════════════════════
// Policies
// ═══════════════════════════════════════════════════════════════════
listPolicies(params = {}) { return this.get('/policies/list?' + new URLSearchParams(params)); }
createPolicy(data) { return this.post('/policies/create', data); }
getPolicy(id) { return this.get(`/policies/${id}`); }
updatePolicy(id, data) { return this.put(`/policies/${id}`, data); }
approvePolicy(id) { return this.post(`/policies/${id}/approve`, {}); }
aiGeneratePolicy(category) { return this.post('/policies/ai-generate', { category }); }
getPolicyTemplates() { return this.get('/policies/templates'); }
// ═══════════════════════════════════════════════════════════════════
// Supply Chain
// ═══════════════════════════════════════════════════════════════════
listSuppliers() { return this.get('/supply-chain/list'); }
createSupplier(data) { return this.post('/supply-chain/create', data); }
getSupplier(id) { return this.get(`/supply-chain/${id}`); }
updateSupplier(id, data) { return this.put(`/supply-chain/${id}`, data); }
assessSupplier(id, data) { return this.post(`/supply-chain/${id}/assess`, data); }
// ═══════════════════════════════════════════════════════════════════
// Training
// ═══════════════════════════════════════════════════════════════════
listCourses() { return this.get('/training/courses'); }
getMyTraining() { return this.get('/training/assignments'); }
getTrainingCompliance() { return this.get('/training/compliance-status'); }
// ═══════════════════════════════════════════════════════════════════
// Assets
// ═══════════════════════════════════════════════════════════════════
listAssets(params = {}) { return this.get('/assets/list?' + new URLSearchParams(params)); }
createAsset(data) { return this.post('/assets/create', data); }
getAsset(id) { return this.get(`/assets/${id}`); }
updateAsset(id, data) { return this.put(`/assets/${id}`, data); }
deleteAsset(id) { return this.del(`/assets/${id}`); }
// NIS2 relevance scoring (GV.OC-04)
getScoringGrid() { return this.get('/assets/scoringGrid'); }
scoreAsset(id, criteria) { return this.post(`/assets/${id}/score`, { criteria }); }
listRelevantSystems() { return this.get('/assets/relevantSystems'); }
importAssets(data) { return this.post('/assets/import', data); } // P2 import CMDB/CSV
getControlsMonitoring() { return this.get('/audit/controlsMonitoring'); }
getAcnRequirements() { return this.get('/audit/acnRequirements'); } // requisiti ACN per org
updateAcnRequirement(id, status, note) { return this.put(`/audit/acnRequirements/${id}`, { status, evidence_note: note }); }
sendSupplierQuestionnaire(id, email) { return this.post(`/supply-chain/${id}/send-questionnaire`, email ? { email } : {}); } // self-assessment fornitore (link esterno)
getSupplierQuestionnaireStatus(id) { return this.get(`/supply-chain/${id}/questionnaire-status`); } // P1 continuous control monitoring (JWT)
// Modulo questionari configurabile (Fase 1): categorie, template, domande, import
getSupplierCategories() { return this.get('/supply-chain/categories'); }
createSupplierCategory(data) { return this.post('/supply-chain/categories', data); }
updateSupplierCategory(id, data) { return this.put(`/supply-chain/categories/${id}`, data); }
deleteSupplierCategory(id) { return this.del(`/supply-chain/categories/${id}`); }
getQuestionnaireTemplates() { return this.get('/supply-chain/templates'); }
getQuestionnaireTemplate(id) { return this.get(`/supply-chain/templates/${id}`); }
createQuestionnaireTemplate(data) { return this.post('/supply-chain/templates', data); }
updateQuestionnaireTemplate(id, data) { return this.put(`/supply-chain/templates/${id}`, data); }
addTemplateQuestion(templateId, data) { return this.post(`/supply-chain/${templateId}/questions`, data); }
updateTemplateQuestion(id, data) { return this.put(`/supply-chain/questions/${id}`, data); }
deleteTemplateQuestion(id) { return this.del(`/supply-chain/questions/${id}`); }
importSuppliers(suppliers) { return this.post('/supply-chain/import', { suppliers }); }
// Campagne questionario (Fase 2)
getQuestionnaireCampaigns() { return this.get('/supply-chain/campaigns'); }
createQuestionnaireCampaign(supplierId, data) { return this.post(`/supply-chain/${supplierId}/campaigns`, data); }
// Campagne questionario (Fase 2)
getQuestionnaireCampaigns() { return this.get('/supply-chain/campaigns'); }
createQuestionnaireCampaign(supplierId, data) { return this.post(`/supply-chain/${supplierId}/campaigns`, data); }
// ═══════════════════════════════════════════════════════════════════
// Audit
// ═══════════════════════════════════════════════════════════════════
listControls() { return this.get('/audit/controls'); }
updateControl(id, data) { return this.put(`/audit/controls/${id}`, data); }
generateComplianceReport() { return this.get('/audit/report'); }
getAuditLogs(params = {}) { return this.get('/audit/logs?' + new URLSearchParams(params)); }
getIsoMapping() { return this.get('/audit/iso27001-mapping'); }
getExecutiveReportUrl() { return this.baseUrl + '/audit/executive-report'; }
getExportUrl(type) { return this.baseUrl + '/audit/export?type=' + type; }
// ═══════════════════════════════════════════════════════════════════
// Onboarding
// ═══════════════════════════════════════════════════════════════════
async uploadVisura(file) {
const formData = new FormData();
formData.append('visura', file);
const headers = { 'Authorization': 'Bearer ' + this.token };
if (this.orgId) headers['X-Organization-Id'] = this.orgId;
try {
const response = await fetch(this.baseUrl + '/onboarding/upload-visura', {
method: 'POST',
headers,
body: formData,
});
return response.json();
} catch (error) {
const msg = typeof I18n !== 'undefined' ? I18n.t('msg.error_connection') : 'Errore di connessione al server';
return { success: false, message: msg };
}
}
fetchCompany(vatNumber) { return this.post('/onboarding/fetch-company', { vat_number: vatNumber }); }
completeOnboarding(data) { return this.post('/onboarding/complete', data); }
// ═══════════════════════════════════════════════════════════════════
// Non-Conformity & CAPA
// ═══════════════════════════════════════════════════════════════════
listNCRs(params = {}) { return this.get('/ncr/list?' + new URLSearchParams(params)); }
createNCR(data) { return this.post('/ncr/create', data); }
getNCR(id) { return this.get(`/ncr/${id}`); }
updateNCR(id, data) { return this.put(`/ncr/${id}`, data); }
addCapa(ncrId, data) { return this.post(`/ncr/${ncrId}/capa`, data); }
updateCapa(capaId, data) { return this.put(`/ncr/capa/${capaId}`, data); }
createNCRsFromAssessment(assessmentId) { return this.post('/ncr/from-assessment', { assessment_id: assessmentId }); }
getNCRStats() { return this.get('/ncr/stats'); }
// ═══════════════════════════════════════════════════════════════════
// Feedback & Segnalazioni
// ═══════════════════════════════════════════════════════════════════
submitFeedback(data) { return this.post('/feedback/submit', data); }
getMyFeedback() { return this.get('/feedback/mine'); }
listFeedback(params = {}) { return this.get('/feedback/list?' + new URLSearchParams(params)); }
getFeedback(id) { return this.get(`/feedback/${id}`); }
updateFeedback(id, data) { return this.put(`/feedback/${id}`, data); }
resolveFeedback(id, password) { return this.post(`/feedback/${id}/resolve`, { password }); }
}
// Singleton globale
const api = new NIS2API();