nis2-agile/application/controllers/TrainingController.php
Cristiano Benassati ae78a2f7f4 [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>
2026-02-17 17:50:18 +01:00

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);
}
}