[DEMO] resetDataset cablato a DemoSeedService (reset reale dataset demo/sandbox)
- application/services/DemoSeedService.php: logica seed/clone golden #151 -> 996001/996002 (riusabile) - DemoController::resetDataset: re-seed reale via service (?scope=demo|sandbox|default both), non piu stub - scripts/seed-demo-dataset.php: thin CLI wrapper sul service Testato prod: POST /api/demo/reset-dataset (X-Internal-Key) scope=sandbox -> 200, 996002 ri-seedata. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e0204dbaa7
commit
6900d942a4
@@ -10,6 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/BaseController.php';
|
require_once __DIR__ . '/BaseController.php';
|
||||||
|
require_once __DIR__ . '/../services/DemoSeedService.php';
|
||||||
|
|
||||||
class DemoController extends BaseController
|
class DemoController extends BaseController
|
||||||
{
|
{
|
||||||
@@ -180,12 +181,21 @@ class DemoController extends BaseController
|
|||||||
$this->jsonError('Non autorizzato', 401, 'UNAUTHORIZED');
|
$this->jsonError('Non autorizzato', 401, 'UNAUTHORIZED');
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Fase 1 (increment dataset): DELETE + re-seed org demo 996001 da seed idempotente.
|
// Reset idempotente del dataset demo (clona la golden #151 in 996001/996002).
|
||||||
|
// ?scope=demo → solo 996001 (read-only) | ?scope=sandbox → solo 996002 | default: entrambe.
|
||||||
|
$scope = (string) $this->getParam('scope', '');
|
||||||
|
$targets = $scope === 'demo' ? [996001 => DemoSeedService::TARGETS[996001]]
|
||||||
|
: ($scope === 'sandbox' ? [996002 => DemoSeedService::TARGETS[996002]]
|
||||||
|
: DemoSeedService::TARGETS);
|
||||||
|
|
||||||
|
$result = DemoSeedService::seed($targets);
|
||||||
|
$failed = array_filter($result, fn($v) => str_starts_with($v, 'errore'));
|
||||||
|
|
||||||
$this->jsonSuccess([
|
$this->jsonSuccess([
|
||||||
'reset' => 'pending_implementation',
|
'reset' => empty($failed) ? 'done' : 'partial',
|
||||||
'company_ids' => [self::DEMO_ORG_ID],
|
'company_ids' => array_keys($result),
|
||||||
'note' => 'Logica di reset dataset demo in arrivo (seed isolato org 996001).',
|
'detail' => $result,
|
||||||
], 'Reset demo (stub)');
|
], empty($failed) ? 'Dataset demo reimpostato' : 'Reset demo parziale (vedi detail)', empty($failed) ? 200 : 207);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══ Helpers ═════════════════════════════════════════════════════════════
|
// ═══ Helpers ═════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* NIS2 Agile - DemoSeedService
|
||||||
|
* ----------------------------------------------------------------------------
|
||||||
|
* Seed/reset del dataset DEMO isolato (Avatar di prodotto / formazione).
|
||||||
|
* Clona la golden org DataCore (#151) negli org del range demo riservato:
|
||||||
|
* - 996001 → dataset DEMO (servito read-only via demo_jwt scope=demo:read-only)
|
||||||
|
* - 996002 → SANDBOX scrivibile isolata per i "prova tu" della formazione
|
||||||
|
*
|
||||||
|
* Idempotente: cancella e ricrea i dati del range demo. Usato da:
|
||||||
|
* - DemoController::resetDataset (reset tra sessioni demo / sandbox)
|
||||||
|
* - scripts/seed-demo-dataset.php (CLI bootstrap manuale)
|
||||||
|
*
|
||||||
|
* Sta sotto application/ (montata in nis2-app) così è raggiungibile sia da
|
||||||
|
* PHP-FPM (resetDataset) sia da CLI.
|
||||||
|
*/
|
||||||
|
class DemoSeedService
|
||||||
|
{
|
||||||
|
public const SRC_ORG = 151; // golden DataCore
|
||||||
|
public const TARGETS = [
|
||||||
|
996001 => 'DataCore Demo S.r.l.',
|
||||||
|
996002 => 'NIS2 Sandbox Formazione S.r.l.',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const USERCOLS = ['owner_user_id','responsible_user_id','assigned_to','reported_by','created_by','uploaded_by','relevance_assessed_by','assessed_by','approved_by','reviewed_by','closed_by','user_id'];
|
||||||
|
/** parent → child (FK figlio) da remappare al nuovo parent */
|
||||||
|
private const PC = [
|
||||||
|
['assessments','assessment_responses','assessment_id'],
|
||||||
|
['risks','risk_treatments','risk_id'],
|
||||||
|
['incidents','incident_timeline','incident_id'],
|
||||||
|
['training_courses','training_assignments','course_id'],
|
||||||
|
['policies','policy_versions','policy_id'],
|
||||||
|
];
|
||||||
|
private const SIMPLE = ['compliance_controls','suppliers','assets'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Esegue il seed/reset. $targets: mappa orgId=>nome (default: entrambe).
|
||||||
|
* @return array<int,string> esito per org ("ok uid=N" | "errore: ...")
|
||||||
|
*/
|
||||||
|
public static function seed(?array $targets = null): array
|
||||||
|
{
|
||||||
|
$targets = $targets ?: self::TARGETS;
|
||||||
|
$pdo = Database::getInstance();
|
||||||
|
$out = [];
|
||||||
|
$pdo->exec('SET FOREIGN_KEY_CHECKS=0');
|
||||||
|
foreach ($targets as $T => $NAME) {
|
||||||
|
$T = (int) $T;
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
try {
|
||||||
|
self::purge($pdo, $T);
|
||||||
|
$org = Database::fetchOne('SELECT * FROM organizations WHERE id = ?', [self::SRC_ORG]);
|
||||||
|
self::copyRow($pdo, 'organizations', $org, ['id' => $T, 'name' => $NAME], 0);
|
||||||
|
|
||||||
|
$uid = (int) Database::insert('users', [
|
||||||
|
'email' => "demo-$T@nis2-demo.local", 'full_name' => "Demo NIS2 $T", 'role' => 'org_admin',
|
||||||
|
'password_hash' => password_hash(bin2hex(random_bytes(12)), PASSWORD_DEFAULT),
|
||||||
|
'is_active' => 1, 'must_change_password' => 0, 'password_version' => 1,
|
||||||
|
]);
|
||||||
|
$pdo->prepare('INSERT INTO user_organizations (user_id,organization_id,role) VALUES (?,?,?)')->execute([$uid, $T, 'org_admin']);
|
||||||
|
|
||||||
|
foreach (self::PC as [$par, $chi, $fk]) {
|
||||||
|
if (!self::tableExists($pdo, $par)) continue;
|
||||||
|
foreach (Database::fetchAll("SELECT * FROM `$par` WHERE organization_id = ? ORDER BY id", [self::SRC_ORG]) as $r) {
|
||||||
|
$newPar = self::copyRow($pdo, $par, $r, ['organization_id' => $T], $uid);
|
||||||
|
if (self::tableExists($pdo, $chi)) {
|
||||||
|
foreach (Database::fetchAll("SELECT * FROM `$chi` WHERE `$fk` = ? ORDER BY id", [$r['id']]) as $cr) {
|
||||||
|
$ov = [$fk => $newPar];
|
||||||
|
if (in_array('organization_id', self::cols($pdo, $chi), true)) $ov['organization_id'] = $T;
|
||||||
|
self::copyRow($pdo, $chi, $cr, $ov, $uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (self::SIMPLE as $t) {
|
||||||
|
if (!self::tableExists($pdo, $t)) continue;
|
||||||
|
foreach (Database::fetchAll("SELECT * FROM `$t` WHERE organization_id = ? ORDER BY id", [self::SRC_ORG]) as $r) {
|
||||||
|
self::copyRow($pdo, $t, $r, ['organization_id' => $T], $uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pdo->commit();
|
||||||
|
$out[$T] = "ok (uid=$uid)";
|
||||||
|
} catch (Throwable $ex) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
$out[$T] = 'errore: ' . $ex->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$pdo->exec('SET FOREIGN_KEY_CHECKS=1');
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function purge(PDO $pdo, int $T): void
|
||||||
|
{
|
||||||
|
foreach (self::PC as [$par, $chi, $fk]) {
|
||||||
|
if (self::tableExists($pdo, $chi)) $pdo->exec("DELETE FROM `$chi` WHERE `$fk` IN (SELECT id FROM `$par` WHERE organization_id = $T)");
|
||||||
|
}
|
||||||
|
foreach (array_merge(array_column(self::PC, 0), self::SIMPLE) as $t) {
|
||||||
|
if (self::tableExists($pdo, $t)) $pdo->exec("DELETE FROM `$t` WHERE organization_id = $T");
|
||||||
|
}
|
||||||
|
$pdo->exec("DELETE FROM user_organizations WHERE organization_id = $T");
|
||||||
|
$pdo->exec("DELETE FROM users WHERE email = 'demo-$T@nis2-demo.local'");
|
||||||
|
$pdo->exec("DELETE FROM organizations WHERE id = $T");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function cols(PDO $pdo, string $t): array
|
||||||
|
{
|
||||||
|
static $cache = [];
|
||||||
|
if (!isset($cache[$t])) {
|
||||||
|
$cache[$t] = $pdo->query("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='$t' ORDER BY ORDINAL_POSITION")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
}
|
||||||
|
return $cache[$t];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function tableExists(PDO $pdo, string $t): bool
|
||||||
|
{
|
||||||
|
return (int) $pdo->query("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='$t'")->fetchColumn() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Copia una riga in $table applicando $overrides e rimappando le colonne utente a $uid. */
|
||||||
|
private static function copyRow(PDO $pdo, string $table, array $row, array $overrides, int $uid): int
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
foreach (self::cols($pdo, $table) as $c) {
|
||||||
|
if ($c === 'id') { if (array_key_exists('id', $overrides)) $data['id'] = $overrides['id']; continue; }
|
||||||
|
if (array_key_exists($c, $overrides)) { $data[$c] = $overrides[$c]; continue; }
|
||||||
|
$v = $row[$c] ?? null;
|
||||||
|
if (in_array($c, self::USERCOLS, true) && $v !== null) $v = $uid;
|
||||||
|
$data[$c] = $v;
|
||||||
|
}
|
||||||
|
$cl = implode(',', array_map(fn($x) => "`$x`", array_keys($data)));
|
||||||
|
$ph = implode(',', array_fill(0, count($data), '?'));
|
||||||
|
$pdo->prepare("INSERT INTO `$table` ($cl) VALUES ($ph)")->execute(array_values($data));
|
||||||
|
return (int) $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* NIS2 Agile - Seed dataset DEMO isolato (Avatar di prodotto / formazione)
|
* NIS2 Agile - Seed dataset DEMO isolato (CLI thin wrapper)
|
||||||
* ----------------------------------------------------------------------------
|
* ----------------------------------------------------------------------------
|
||||||
* Clona la golden org DataCore (#151) negli org del range demo riservato:
|
* Logica nel service: application/services/DemoSeedService.php (usato anche da
|
||||||
* - 996001 → dataset DEMO (servito read-only via demo_jwt scope=demo:read-only)
|
* DemoController::resetDataset). Questo è solo il bootstrap CLI.
|
||||||
* - 996002 → SANDBOX scrivibile isolata per i "prova tu" della formazione
|
|
||||||
*
|
*
|
||||||
* Idempotente: cancella e ricrea i dati del range demo ad ogni run. Richiamabile
|
* ESEGUIRE SU HETZNER. NB: nis2-app monta SOLO application/+public/ → per girare
|
||||||
* da DemoController::resetDataset (reset dataset demo) e da TrainingSandbox.
|
* via `docker exec` copiare sotto application/:
|
||||||
*
|
|
||||||
* ESEGUIRE SU HETZNER (richiede DB container + SSL runtime). NB: nis2-app monta
|
|
||||||
* SOLO application/+public/ → per girare via `docker exec` copiare sotto application/:
|
|
||||||
* cp scripts/seed-demo-dataset.php application/_seed.php
|
* cp scripts/seed-demo-dataset.php application/_seed.php
|
||||||
* docker exec -i nis2-app php /var/www/nis2-agile/application/_seed.php
|
* docker exec -i nis2-app php /var/www/nis2-agile/application/_seed.php
|
||||||
* rm application/_seed.php
|
* rm application/_seed.php
|
||||||
@@ -24,70 +20,16 @@ $e=@file_get_contents('/proc/1/environ');
|
|||||||
if ($e) foreach (explode("\0", $e) as $p) { if (strpos($p, '=') !== false) { putenv($p); [$k, $v] = explode('=', $p, 2); $_ENV[$k] = $v; $_SERVER[$k] = $v; } }
|
if ($e) foreach (explode("\0", $e) as $p) { if (strpos($p, '=') !== false) { putenv($p); [$k, $v] = explode('=', $p, 2); $_ENV[$k] = $v; $_SERVER[$k] = $v; } }
|
||||||
if (!defined('BASE_PATH')) define('BASE_PATH', dirname(__DIR__));
|
if (!defined('BASE_PATH')) define('BASE_PATH', dirname(__DIR__));
|
||||||
if (!defined('APP_PATH')) define('APP_PATH', BASE_PATH . '/application');
|
if (!defined('APP_PATH')) define('APP_PATH', BASE_PATH . '/application');
|
||||||
require_once APP_PATH.'/config/env.php'; require_once APP_PATH.'/config/config.php'; require_once APP_PATH.'/config/database.php';
|
require_once APP_PATH . '/config/env.php';
|
||||||
$pdo=Database::getInstance();
|
require_once APP_PATH . '/config/config.php';
|
||||||
|
require_once APP_PATH . '/config/database.php';
|
||||||
|
require_once APP_PATH . '/services/DemoSeedService.php';
|
||||||
|
|
||||||
$opts = getopt('', ['only::']);
|
$opts = getopt('', ['only::']);
|
||||||
$SRC=151;
|
$targets = isset($opts['only'])
|
||||||
$ALL=[996001=>'DataCore Demo S.r.l.', 996002=>'NIS2 Sandbox Formazione S.r.l.'];
|
? array_intersect_key(DemoSeedService::TARGETS, [(int) $opts['only'] => 1])
|
||||||
$TARGETS = isset($opts['only']) ? array_intersect_key($ALL,[(int)$opts['only']=>1]) : $ALL;
|
: DemoSeedService::TARGETS;
|
||||||
|
|
||||||
$USERCOLS=['owner_user_id','responsible_user_id','assigned_to','reported_by','created_by','uploaded_by','relevance_assessed_by','assessed_by','approved_by','reviewed_by','closed_by','user_id'];
|
$res = DemoSeedService::seed($targets);
|
||||||
function cols($pdo,$t){ return $pdo->query("SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='$t' ORDER BY ORDINAL_POSITION")->fetchAll(PDO::FETCH_COLUMN); }
|
foreach ($res as $org => $msg) echo "ORG $org: $msg\n";
|
||||||
function tableExists($pdo,$t){ return (int)$pdo->query("SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='$t'")->fetchColumn()>0; }
|
echo "Seed dataset demo completato (" . implode(',', array_keys($res)) . ").\n";
|
||||||
function copyRow($pdo,$table,$row,$overrides,$uid,$USERCOLS){
|
|
||||||
static $cc=[]; if(!isset($cc[$table])) $cc[$table]=cols($pdo,$table);
|
|
||||||
$data=[];
|
|
||||||
foreach($cc[$table] as $c){
|
|
||||||
if($c==='id'){ if(array_key_exists('id',$overrides)) $data['id']=$overrides['id']; continue; }
|
|
||||||
if(array_key_exists($c,$overrides)){ $data[$c]=$overrides[$c]; continue; }
|
|
||||||
$v=$row[$c]??null; if(in_array($c,$USERCOLS,true)&&$v!==null) $v=$uid; $data[$c]=$v;
|
|
||||||
}
|
|
||||||
$cl=implode(',',array_map(fn($x)=>"`$x`",array_keys($data))); $ph=implode(',',array_fill(0,count($data),'?'));
|
|
||||||
$pdo->prepare("INSERT INTO `$table` ($cl) VALUES ($ph)")->execute(array_values($data));
|
|
||||||
return (int)$pdo->lastInsertId();
|
|
||||||
}
|
|
||||||
|
|
||||||
$PC=[['assessments','assessment_responses','assessment_id'],['risks','risk_treatments','risk_id'],['incidents','incident_timeline','incident_id'],['training_courses','training_assignments','course_id'],['policies','policy_versions','policy_id']];
|
|
||||||
$SIMPLE=['compliance_controls','suppliers','assets'];
|
|
||||||
|
|
||||||
$pdo->exec("SET FOREIGN_KEY_CHECKS=0");
|
|
||||||
foreach($TARGETS as $T=>$NAME){
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
try {
|
|
||||||
foreach($PC as [$par,$chi,$fk]){ if(tableExists($pdo,$chi)) $pdo->exec("DELETE FROM `$chi` WHERE `$fk` IN (SELECT id FROM `$par` WHERE organization_id=$T)"); }
|
|
||||||
foreach(array_merge(array_column($PC,0),$SIMPLE) as $t){ if(tableExists($pdo,$t)) $pdo->exec("DELETE FROM `$t` WHERE organization_id=$T"); }
|
|
||||||
$pdo->exec("DELETE FROM user_organizations WHERE organization_id=$T");
|
|
||||||
$pdo->exec("DELETE FROM users WHERE email='demo-$T@nis2-demo.local'");
|
|
||||||
$pdo->exec("DELETE FROM organizations WHERE id=$T");
|
|
||||||
|
|
||||||
$org=Database::fetchOne("SELECT * FROM organizations WHERE id=?",[$SRC]);
|
|
||||||
copyRow($pdo,'organizations',$org,['id'=>$T,'name'=>$NAME],0,$USERCOLS);
|
|
||||||
// Utente demo: Database::insert (solo colonne fornite → i default DB si applicano;
|
|
||||||
// copyRow forzerebbe null sulle NOT NULL con default es. must_change_password).
|
|
||||||
$uid=(int)Database::insert('users',[
|
|
||||||
'email'=>"demo-$T@nis2-demo.local",'full_name'=>"Demo NIS2 $T",'role'=>'org_admin',
|
|
||||||
'password_hash'=>password_hash(bin2hex(random_bytes(12)),PASSWORD_DEFAULT),
|
|
||||||
'is_active'=>1,'must_change_password'=>0,'password_version'=>1,
|
|
||||||
]);
|
|
||||||
$pdo->prepare("INSERT INTO user_organizations (user_id,organization_id,role) VALUES (?,?,'org_admin')")->execute([$uid,$T]);
|
|
||||||
|
|
||||||
foreach($PC as [$par,$chi,$fk]){
|
|
||||||
if(!tableExists($pdo,$par)) continue;
|
|
||||||
foreach(Database::fetchAll("SELECT * FROM `$par` WHERE organization_id=? ORDER BY id",[$SRC]) as $r){
|
|
||||||
$newPar=copyRow($pdo,$par,$r,['organization_id'=>$T],$uid,$USERCOLS);
|
|
||||||
if(tableExists($pdo,$chi)) foreach(Database::fetchAll("SELECT * FROM `$chi` WHERE `$fk`=? ORDER BY id",[$r['id']]) as $cr){
|
|
||||||
$ov=[$fk=>$newPar]; if(in_array('organization_id',cols($pdo,$chi),true)) $ov['organization_id']=$T;
|
|
||||||
copyRow($pdo,$chi,$cr,$ov,$uid,$USERCOLS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach($SIMPLE as $t){ if(!tableExists($pdo,$t)) continue;
|
|
||||||
foreach(Database::fetchAll("SELECT * FROM `$t` WHERE organization_id=? ORDER BY id",[$SRC]) as $r) copyRow($pdo,$t,$r,['organization_id'=>$T],$uid,$USERCOLS);
|
|
||||||
}
|
|
||||||
$pdo->commit();
|
|
||||||
echo "ORG $T ($NAME): seed OK (uid demo=$uid)\n";
|
|
||||||
} catch(Throwable $ex){ $pdo->rollBack(); echo "ORG $T ERRORE ROLLBACK: ".$ex->getMessage()."\n"; }
|
|
||||||
}
|
|
||||||
$pdo->exec("SET FOREIGN_KEY_CHECKS=1");
|
|
||||||
echo "Seed dataset demo completato (".implode(',',array_keys($TARGETS)).").\n";
|
|
||||||
|
|||||||
Reference in New Issue
Block a user