[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,150 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user