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>
151 lines
5.4 KiB
PHP
151 lines
5.4 KiB
PHP
<?php
|
|
/**
|
|
* NIS2 Agile - Training Controller
|
|
*
|
|
* Gestione formazione cybersecurity (Art. 20 NIS2).
|
|
*/
|
|
|
|
require_once __DIR__ . '/BaseController.php';
|
|
|
|
class TrainingController extends BaseController
|
|
{
|
|
public function listCourses(): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$courses = Database::fetchAll(
|
|
'SELECT * FROM training_courses
|
|
WHERE (organization_id = ? OR organization_id IS NULL) AND is_active = 1
|
|
ORDER BY is_mandatory DESC, title',
|
|
[$this->getCurrentOrgId()]
|
|
);
|
|
|
|
$this->jsonSuccess($courses);
|
|
}
|
|
|
|
public function createCourse(): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
$this->validateRequired(['title']);
|
|
|
|
$courseId = Database::insert('training_courses', [
|
|
'organization_id' => $this->getCurrentOrgId(),
|
|
'title' => trim($this->getParam('title')),
|
|
'description' => $this->getParam('description'),
|
|
'target_role' => $this->getParam('target_role', 'all'),
|
|
'nis2_article' => $this->getParam('nis2_article'),
|
|
'is_mandatory' => $this->getParam('is_mandatory', 0),
|
|
'duration_minutes' => $this->getParam('duration_minutes'),
|
|
'content' => $this->getParam('content') ? json_encode($this->getParam('content')) : null,
|
|
'quiz' => $this->getParam('quiz') ? json_encode($this->getParam('quiz')) : null,
|
|
'passing_score' => $this->getParam('passing_score', 70),
|
|
]);
|
|
|
|
$this->logAudit('course_created', 'training_course', $courseId);
|
|
$this->jsonSuccess(['id' => $courseId], 'Corso creato', 201);
|
|
}
|
|
|
|
public function myAssignments(): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
$assignments = Database::fetchAll(
|
|
'SELECT ta.*, tc.title, tc.description, tc.duration_minutes, tc.is_mandatory
|
|
FROM training_assignments ta
|
|
JOIN training_courses tc ON tc.id = ta.course_id
|
|
WHERE ta.user_id = ?
|
|
ORDER BY ta.status, ta.due_date',
|
|
[$this->getCurrentUserId()]
|
|
);
|
|
|
|
$this->jsonSuccess($assignments);
|
|
}
|
|
|
|
public function assignCourse(): void
|
|
{
|
|
$this->requireOrgRole(['org_admin', 'compliance_manager']);
|
|
$this->validateRequired(['course_id', 'user_ids']);
|
|
|
|
$courseId = (int) $this->getParam('course_id');
|
|
$userIds = $this->getParam('user_ids');
|
|
$dueDate = $this->getParam('due_date');
|
|
|
|
$assigned = 0;
|
|
foreach ($userIds as $userId) {
|
|
$existing = Database::fetchOne(
|
|
'SELECT id FROM training_assignments WHERE course_id = ? AND user_id = ?',
|
|
[$courseId, $userId]
|
|
);
|
|
if ($existing) continue;
|
|
|
|
Database::insert('training_assignments', [
|
|
'course_id' => $courseId,
|
|
'user_id' => (int) $userId,
|
|
'organization_id' => $this->getCurrentOrgId(),
|
|
'due_date' => $dueDate,
|
|
]);
|
|
$assigned++;
|
|
}
|
|
|
|
$this->logAudit('training_assigned', 'training_course', $courseId, ['assigned_count' => $assigned]);
|
|
$this->jsonSuccess(['assigned' => $assigned], "{$assigned} assegnazioni create");
|
|
}
|
|
|
|
public function updateAssignment(int $id): void
|
|
{
|
|
$this->requireAuth();
|
|
|
|
$updates = [];
|
|
foreach (['status', 'quiz_score'] as $field) {
|
|
if ($this->hasParam($field)) {
|
|
$updates[$field] = $this->getParam($field);
|
|
}
|
|
}
|
|
|
|
if (isset($updates['status']) && $updates['status'] === 'in_progress' && !isset($updates['started_at'])) {
|
|
$updates['started_at'] = date('Y-m-d H:i:s');
|
|
}
|
|
|
|
if (isset($updates['status']) && $updates['status'] === 'completed') {
|
|
$updates['completed_at'] = date('Y-m-d H:i:s');
|
|
}
|
|
|
|
if (!empty($updates)) {
|
|
Database::update('training_assignments', $updates, 'id = ? AND user_id = ?', [$id, $this->getCurrentUserId()]);
|
|
}
|
|
|
|
$this->jsonSuccess($updates, 'Assegnazione aggiornata');
|
|
}
|
|
|
|
public function complianceStatus(): void
|
|
{
|
|
$this->requireOrgAccess();
|
|
|
|
$members = Database::fetchAll(
|
|
'SELECT u.id, u.full_name, u.email, uo.role
|
|
FROM user_organizations uo
|
|
JOIN users u ON u.id = uo.user_id
|
|
WHERE uo.organization_id = ? AND u.is_active = 1',
|
|
[$this->getCurrentOrgId()]
|
|
);
|
|
|
|
foreach ($members as &$member) {
|
|
$member['assignments'] = Database::fetchAll(
|
|
'SELECT ta.status, ta.completed_at, ta.quiz_score, tc.title, tc.is_mandatory
|
|
FROM training_assignments ta
|
|
JOIN training_courses tc ON tc.id = ta.course_id
|
|
WHERE ta.user_id = ? AND ta.organization_id = ?',
|
|
[$member['id'], $this->getCurrentOrgId()]
|
|
);
|
|
|
|
$mandatory = array_filter($member['assignments'], fn($a) => $a['is_mandatory']);
|
|
$completedMandatory = array_filter($mandatory, fn($a) => $a['status'] === 'completed');
|
|
$member['mandatory_compliance'] = count($mandatory) > 0
|
|
? round(count($completedMandatory) / count($mandatory) * 100)
|
|
: 100;
|
|
}
|
|
|
|
$this->jsonSuccess($members);
|
|
}
|
|
}
|