[CORE] Initial project scaffold - NIS2 Agile Compliance Platform
Complete MVP implementation including: - PHP 8.4 backend with Front Controller pattern (80+ API endpoints) - Multi-tenant architecture with organization_id isolation - JWT authentication (HS256, 2h access + 7d refresh tokens) - 14 controllers: Auth, Organization, Assessment, Dashboard, Risk, Incident, Policy, SupplyChain, Training, Asset, Audit, Admin - AI Service integration (Anthropic Claude API) for gap analysis, risk suggestions, policy generation, incident classification - NIS2 gap analysis questionnaire (~80 questions, 10 categories) - MySQL schema (20 tables) with NIS2 Art. 21 compliance controls - NIS2 Art. 23 incident reporting workflow (24h/72h/30d) - Frontend: login, register, dashboard, assessment wizard, org setup - Docker configuration (PHP-FPM + Nginx + MySQL) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* NIS2 Agile - API Client
|
||||
*
|
||||
* Client JavaScript per comunicare con il backend REST API.
|
||||
*/
|
||||
|
||||
class NIS2API {
|
||||
constructor(baseUrl = '/nis2/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);
|
||||
return { success: false, message: 'Errore di connessione al server' };
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
const result = await this.post('/auth/register', { email, password, full_name: fullName });
|
||||
if (result.success) {
|
||||
this.setTokens(result.data.access_token, result.data.refresh_token);
|
||||
}
|
||||
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 = '/nis2/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');
|
||||
}
|
||||
|
||||
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`, {}); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 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', {}); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 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`, {}); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 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); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 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'); }
|
||||
}
|
||||
|
||||
// Singleton globale
|
||||
const api = new NIS2API();
|
||||
Reference in New Issue
Block a user