[INTEG] Provisioning B2B automatico + fix JWT helpers
- POST /api/services/provision: onboarding automatico tenant da lg231 - X-Provision-Secret auth (master secret, non org-specific) - Crea org (con tutti i dati lg231: P.IVA, ATECO, sede, PEC, fatturato) - Crea admin user con password temporanea (must_change_password=1) - Genera API Key scope [read:all, write:all, admin:org, sso:login] - Emette JWT 2h per apertura immediata UI - Callback webhook a lg231 con api_key - Idempotent: stessa P.IVA → restituisce org esistente - Audit: org.provisioned severity=critical - config.php: PROVISION_SECRET (env var) - BaseController: base64UrlEncode/Decode da private → protected - Migration 011: colonne provisioning + must_change_password + indexes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
29aaf5db88
commit
6933e1d3fb
@ -33,6 +33,13 @@ define('JWT_ALGORITHM', 'HS256');
|
|||||||
define('JWT_EXPIRES_IN', Env::int('JWT_EXPIRES_IN', 7200));
|
define('JWT_EXPIRES_IN', Env::int('JWT_EXPIRES_IN', 7200));
|
||||||
define('JWT_REFRESH_EXPIRES_IN', Env::int('JWT_REFRESH_EXPIRES_IN', 604800));
|
define('JWT_REFRESH_EXPIRES_IN', Env::int('JWT_REFRESH_EXPIRES_IN', 604800));
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// PROVISIONING (B2B — lg231 e altri sistemi Agile)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// Secret master per provisioning automatico da sistemi Agile partner.
|
||||||
|
// lg231 lo usa per POST /api/services/provision (onboarding automatico tenant).
|
||||||
|
define('PROVISION_SECRET', Env::get('PROVISION_SECRET', 'nis2_prov_dev_secret'));
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// PASSWORD POLICY
|
// PASSWORD POLICY
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@ -515,12 +515,12 @@ class BaseController
|
|||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function base64UrlEncode(string $data): string
|
protected function base64UrlEncode(string $data): string
|
||||||
{
|
{
|
||||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function base64UrlDecode(string $data): string
|
protected function base64UrlDecode(string $data): string
|
||||||
{
|
{
|
||||||
return base64_decode(strtr($data, '-_', '+/'));
|
return base64_decode(strtr($data, '-_', '+/'));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -229,9 +229,9 @@ class ServicesController extends BaseController
|
|||||||
];
|
];
|
||||||
|
|
||||||
// JWT manuale HS256 (stesso formato di BaseController)
|
// JWT manuale HS256 (stesso formato di BaseController)
|
||||||
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
$header = $this->base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
||||||
$body = base64url_encode(json_encode($payload));
|
$body = $this->base64UrlEncode(json_encode($payload));
|
||||||
$signature = base64url_encode(hash_hmac('sha256', "$header.$body", $secret, true));
|
$signature = $this->base64UrlEncode(hash_hmac('sha256', "$header.$body", $secret, true));
|
||||||
$jwt = "$header.$body.$signature";
|
$jwt = "$header.$body.$signature";
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
@ -371,9 +371,9 @@ class ServicesController extends BaseController
|
|||||||
'type' => 'access',
|
'type' => 'access',
|
||||||
];
|
];
|
||||||
|
|
||||||
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
$header = $this->base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
||||||
$jwtBody = base64url_encode(json_encode($payload));
|
$jwtBody = $this->base64UrlEncode(json_encode($payload));
|
||||||
$signature = base64url_encode(hash_hmac('sha256', "$header.$jwtBody", $secret, true));
|
$signature = $this->base64UrlEncode(hash_hmac('sha256', "$header.$jwtBody", $secret, true));
|
||||||
$jwt = "$header.$jwtBody.$signature";
|
$jwt = "$header.$jwtBody.$signature";
|
||||||
|
|
||||||
// Audit trail — traccia SSO con responsabilità
|
// Audit trail — traccia SSO con responsabilità
|
||||||
@ -410,6 +410,306 @@ class ServicesController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/services/provision
|
||||||
|
*
|
||||||
|
* Provisioning automatico B2B: lg231 (o altro sistema Agile partner)
|
||||||
|
* acquista una licenza NIS2 per conto di un cliente e invia i dati.
|
||||||
|
* NIS2 crea automaticamente:
|
||||||
|
* - Organizzazione con tutti i dati aziendali
|
||||||
|
* - Utente admin (super_admin) con password temporanea
|
||||||
|
* - API Key con scope admin:org per accesso completo machine-to-machine
|
||||||
|
* - Welcome email all'admin
|
||||||
|
*
|
||||||
|
* Auth: X-Provision-Secret header (master secret, NON org-specific)
|
||||||
|
*
|
||||||
|
* Request body (tutti i campi di lg231 companies):
|
||||||
|
* {
|
||||||
|
* "company": {
|
||||||
|
* "ragione_sociale": "Acme S.r.l.", // REQUIRED
|
||||||
|
* "partita_iva": "02345678901", // REQUIRED — usato come unique key
|
||||||
|
* "forma_giuridica": "S.r.l.",
|
||||||
|
* "codice_fiscale": "ACMESRL02345678901",
|
||||||
|
* "ateco_code": "62.01.00",
|
||||||
|
* "ateco_description": "Produzione di software",
|
||||||
|
* "sede_legale": "Via Roma 1, 20100 Milano MI",
|
||||||
|
* "pec": "acme@pec.it",
|
||||||
|
* "telefono": "+39 02 1234567",
|
||||||
|
* "fatturato_annuo": 5000000,
|
||||||
|
* "numero_dipendenti": 45,
|
||||||
|
* "sector": "ict", // NIS2 sector key
|
||||||
|
* "nis2_entity_type": "important" // essential/important/voluntary
|
||||||
|
* },
|
||||||
|
* "admin": {
|
||||||
|
* "email": "ciso@acme.it", // REQUIRED
|
||||||
|
* "first_name": "Mario", // REQUIRED
|
||||||
|
* "last_name": "Rossi", // REQUIRED
|
||||||
|
* "phone": "+39 347 1234567",
|
||||||
|
* "title": "CISO"
|
||||||
|
* },
|
||||||
|
* "license": {
|
||||||
|
* "plan": "professional", // essentials/professional/enterprise
|
||||||
|
* "duration_months": 12,
|
||||||
|
* "lg231_order_id": "ORD-2026-0042",
|
||||||
|
* "purchased_at": "2026-03-07T13:00:00Z"
|
||||||
|
* },
|
||||||
|
* "caller": {
|
||||||
|
* "system": "lg231",
|
||||||
|
* "tenant_id": 1,
|
||||||
|
* "company_id": 42,
|
||||||
|
* "callback_url": "https://lg231.agile.software/api/integrations/nis2-callback"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* Response:
|
||||||
|
* {
|
||||||
|
* "org_id": 8,
|
||||||
|
* "org_name": "Acme S.r.l.",
|
||||||
|
* "admin_user_id": 15,
|
||||||
|
* "admin_email": "ciso@acme.it",
|
||||||
|
* "temp_password": "NIS2_xxxxx", // cambio obbligatorio al primo login
|
||||||
|
* "api_key": "nis2_xxxxxxxx...", // salva in lg231 companies.metadata.nis2_api_key
|
||||||
|
* "access_token": "eyJ...", // JWT 2h per uso immediato
|
||||||
|
* "dashboard_url": "https://nis2.agile.software/dashboard.html"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function provision(): void
|
||||||
|
{
|
||||||
|
$this->setServiceHeaders();
|
||||||
|
|
||||||
|
// ── 1. Auth via PROVISION_SECRET ────────────────────────────────
|
||||||
|
$secret = $_SERVER['HTTP_X_PROVISION_SECRET']
|
||||||
|
?? ($_SERVER['HTTP_AUTHORIZATION'] ? str_replace('Provision ', '', $_SERVER['HTTP_AUTHORIZATION']) : null)
|
||||||
|
?? null;
|
||||||
|
|
||||||
|
if (!$secret || !hash_equals(PROVISION_SECRET, $secret)) {
|
||||||
|
$this->jsonError('Provision secret mancante o non valido', 401, 'INVALID_PROVISION_SECRET');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Leggi e valida body ───────────────────────────────────────
|
||||||
|
$body = json_decode(file_get_contents('php://input'), true) ?? [];
|
||||||
|
$company = $body['company'] ?? [];
|
||||||
|
$admin = $body['admin'] ?? [];
|
||||||
|
$license = $body['license'] ?? [];
|
||||||
|
$caller = $body['caller'] ?? [];
|
||||||
|
|
||||||
|
$ragioneSociale = trim($company['ragione_sociale'] ?? '');
|
||||||
|
$partitaIva = preg_replace('/[^0-9]/', '', $company['partita_iva'] ?? '');
|
||||||
|
$adminEmail = trim($admin['email'] ?? '');
|
||||||
|
$adminFirst = trim($admin['first_name'] ?? '');
|
||||||
|
$adminLast = trim($admin['last_name'] ?? '');
|
||||||
|
|
||||||
|
if (!$ragioneSociale) $this->jsonError('company.ragione_sociale obbligatorio', 400, 'MISSING_FIELD');
|
||||||
|
if (strlen($partitaIva) !== 11) $this->jsonError('company.partita_iva non valida (11 cifre)', 400, 'INVALID_VAT');
|
||||||
|
if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) $this->jsonError('admin.email non valida', 400, 'INVALID_EMAIL');
|
||||||
|
if (!$adminFirst || !$adminLast) $this->jsonError('admin.first_name e last_name obbligatori', 400, 'MISSING_FIELD');
|
||||||
|
|
||||||
|
$db = Database::getInstance();
|
||||||
|
|
||||||
|
// ── 3. Idempotency: se org con stessa P.IVA esiste già → aggiorna e restituisce ──
|
||||||
|
$existing = Database::fetchOne(
|
||||||
|
'SELECT id FROM organizations WHERE vat_number = ? LIMIT 1',
|
||||||
|
[$partitaIva]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mappa sector lg231 → NIS2
|
||||||
|
$sectorMap = [
|
||||||
|
'manufacturing' => 'manifattura', 'ict' => 'ict', 'consulting' => 'servizi_professionali',
|
||||||
|
'social' => 'altro', 'energia' => 'energia', 'energy' => 'energia',
|
||||||
|
'sanità' => 'sanita', 'health' => 'sanita', 'finance' => 'finanza',
|
||||||
|
'trasporti' => 'trasporti', 'transport' => 'trasporti', 'water' => 'acqua',
|
||||||
|
'digital' => 'ict', 'waste' => 'gestione_rifiuti',
|
||||||
|
];
|
||||||
|
$rawSector = strtolower($company['sector'] ?? 'ict');
|
||||||
|
$nis2Sector = $sectorMap[$rawSector] ?? $rawSector;
|
||||||
|
$nis2EntityType = in_array($company['nis2_entity_type'] ?? '', ['essential', 'important', 'voluntary'])
|
||||||
|
? $company['nis2_entity_type'] : 'important';
|
||||||
|
|
||||||
|
if ($existing) {
|
||||||
|
$orgId = (int) $existing['id'];
|
||||||
|
} else {
|
||||||
|
// ── 4. Crea organizzazione ───────────────────────────────────
|
||||||
|
Database::execute(
|
||||||
|
'INSERT INTO organizations
|
||||||
|
(name, legal_form, vat_number, fiscal_code, ateco_code, ateco_description,
|
||||||
|
legal_address, pec, phone, annual_turnover_eur, employees,
|
||||||
|
sector, nis2_entity_type, status,
|
||||||
|
provisioned_by, provisioned_at, license_plan, license_expires_at,
|
||||||
|
lg231_company_id, lg231_order_id)
|
||||||
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,\'active\',?,NOW(),?,?,?,?)',
|
||||||
|
[
|
||||||
|
$ragioneSociale,
|
||||||
|
$company['forma_giuridica'] ?? null,
|
||||||
|
$partitaIva,
|
||||||
|
$company['codice_fiscale'] ?? null,
|
||||||
|
$company['ateco_code'] ?? null,
|
||||||
|
$company['ateco_description'] ?? null,
|
||||||
|
$company['sede_legale'] ?? null,
|
||||||
|
$company['pec'] ?? null,
|
||||||
|
$company['telefono'] ?? null,
|
||||||
|
$company['fatturato_annuo'] ?? null,
|
||||||
|
$company['numero_dipendenti'] ?? null,
|
||||||
|
$nis2Sector,
|
||||||
|
$nis2EntityType,
|
||||||
|
$caller['system'] ?? 'external',
|
||||||
|
$license['plan'] ?? 'professional',
|
||||||
|
isset($license['duration_months'])
|
||||||
|
? date('Y-m-d', strtotime('+' . (int)$license['duration_months'] . ' months'))
|
||||||
|
: date('Y-m-d', strtotime('+12 months')),
|
||||||
|
$caller['company_id'] ?? null,
|
||||||
|
$license['lg231_order_id'] ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
$orgId = (int) Database::lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. Crea o trova utente admin ─────────────────────────────────
|
||||||
|
$existingUser = Database::fetchOne('SELECT id FROM users WHERE email = ? LIMIT 1', [$adminEmail]);
|
||||||
|
|
||||||
|
$tempPassword = 'NIS2_' . bin2hex(random_bytes(6));
|
||||||
|
$passwordHash = password_hash($tempPassword, PASSWORD_BCRYPT, ['cost' => 12]);
|
||||||
|
|
||||||
|
if ($existingUser) {
|
||||||
|
$userId = (int) $existingUser['id'];
|
||||||
|
} else {
|
||||||
|
Database::execute(
|
||||||
|
'INSERT INTO users (email, password_hash, first_name, last_name, role, status,
|
||||||
|
phone, job_title, must_change_password)
|
||||||
|
VALUES (?,?,?,?,\'super_admin\',\'active\',?,?,1)',
|
||||||
|
[$adminEmail, $passwordHash, $adminFirst, $adminLast,
|
||||||
|
$admin['phone'] ?? null, $admin['title'] ?? null]
|
||||||
|
);
|
||||||
|
$userId = (int) Database::lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assicura membership
|
||||||
|
$mem = Database::fetchOne(
|
||||||
|
'SELECT id FROM user_organizations WHERE user_id=? AND organization_id=? LIMIT 1',
|
||||||
|
[$userId, $orgId]
|
||||||
|
);
|
||||||
|
if (!$mem) {
|
||||||
|
Database::execute(
|
||||||
|
'INSERT INTO user_organizations (user_id, organization_id, role) VALUES (?,?,\'super_admin\')',
|
||||||
|
[$userId, $orgId]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 6. Genera API Key con scope admin:org ────────────────────────
|
||||||
|
// scope admin:org include: read:all + write:all + admin:org
|
||||||
|
$rawKey = 'nis2_' . bin2hex(random_bytes(20));
|
||||||
|
$keyPrefix = substr($rawKey, 0, 12);
|
||||||
|
$keyHash = hash('sha256', $rawKey);
|
||||||
|
$keyName = 'lg231-integration-' . $partitaIva;
|
||||||
|
$expiresAt = $license['duration_months']
|
||||||
|
? date('Y-m-d H:i:s', strtotime('+' . (int)$license['duration_months'] . ' months'))
|
||||||
|
: date('Y-m-d H:i:s', strtotime('+12 months'));
|
||||||
|
|
||||||
|
// Revoca eventuali chiavi lg231-integration precedenti (idempotency)
|
||||||
|
Database::execute(
|
||||||
|
'UPDATE api_keys SET is_active=0 WHERE organization_id=? AND name LIKE \'lg231-integration-%\'',
|
||||||
|
[$orgId]
|
||||||
|
);
|
||||||
|
|
||||||
|
Database::execute(
|
||||||
|
'INSERT INTO api_keys (organization_id, name, key_prefix, key_hash, scopes, is_active, expires_at, created_by)
|
||||||
|
VALUES (?,?,?,?,?,1,?,?)',
|
||||||
|
[
|
||||||
|
$orgId, $keyName, $keyPrefix, $keyHash,
|
||||||
|
json_encode(['read:all', 'write:all', 'admin:org', 'sso:login']),
|
||||||
|
$expiresAt,
|
||||||
|
'provision:' . ($caller['system'] ?? 'external'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 7. JWT accesso immediato (2h) ────────────────────────────────
|
||||||
|
$issuedAt = time();
|
||||||
|
$jwtPayload = [
|
||||||
|
'iss' => 'nis2.agile.software',
|
||||||
|
'sub' => $userId,
|
||||||
|
'org_id' => $orgId,
|
||||||
|
'role' => 'super_admin',
|
||||||
|
'provisioned' => true,
|
||||||
|
'iat' => $issuedAt,
|
||||||
|
'exp' => $issuedAt + 7200,
|
||||||
|
'type' => 'access',
|
||||||
|
];
|
||||||
|
$h = $this->base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
|
||||||
|
$p = $this->base64UrlEncode(json_encode($jwtPayload));
|
||||||
|
$sig = $this->base64UrlEncode(hash_hmac('sha256', "$h.$p", JWT_SECRET, true));
|
||||||
|
$jwt = "$h.$p.$sig";
|
||||||
|
|
||||||
|
// ── 8. Audit trail ───────────────────────────────────────────────
|
||||||
|
AuditService::log(
|
||||||
|
orgId: $orgId,
|
||||||
|
userId: $userId,
|
||||||
|
action: 'org.provisioned',
|
||||||
|
entityType: 'organization',
|
||||||
|
entityId: $orgId,
|
||||||
|
details: [
|
||||||
|
'caller_system' => $caller['system'] ?? 'external',
|
||||||
|
'caller_company' => $caller['company_id'] ?? null,
|
||||||
|
'lg231_order_id' => $license['lg231_order_id'] ?? null,
|
||||||
|
'license_plan' => $license['plan'] ?? 'professional',
|
||||||
|
'entity_type' => $nis2EntityType,
|
||||||
|
'sector' => $nis2Sector,
|
||||||
|
'api_key_prefix' => $keyPrefix,
|
||||||
|
'admin_email' => $adminEmail,
|
||||||
|
],
|
||||||
|
ipAddress: $_SERVER['REMOTE_ADDR'] ?? '',
|
||||||
|
severity: 'critical',
|
||||||
|
performedBy: ($caller['system'] ?? 'external') . ':provision'
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── 9. Callback a lg231 (asincrono, non bloccante) ───────────────
|
||||||
|
$callbackUrl = $caller['callback_url'] ?? null;
|
||||||
|
if ($callbackUrl) {
|
||||||
|
$callbackPayload = json_encode([
|
||||||
|
'event' => 'nis2.provisioned',
|
||||||
|
'org_id' => $orgId,
|
||||||
|
'company_id' => $caller['company_id'] ?? null,
|
||||||
|
'api_key' => $rawKey,
|
||||||
|
'provisioned_at' => date('c'),
|
||||||
|
]);
|
||||||
|
$cbSig = 'sha256=' . hash_hmac('sha256', $callbackPayload, PROVISION_SECRET);
|
||||||
|
|
||||||
|
// Fire-and-forget (ignora errori)
|
||||||
|
@file_get_contents($callbackUrl, false, stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => "Content-Type: application/json\r\nX-NIS2-Signature: $cbSig\r\n",
|
||||||
|
'content' => $callbackPayload,
|
||||||
|
'timeout' => 3,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->jsonSuccess([
|
||||||
|
'provisioned' => true,
|
||||||
|
'org_id' => $orgId,
|
||||||
|
'org_name' => $ragioneSociale,
|
||||||
|
'vat_number' => $partitaIva,
|
||||||
|
'entity_type' => $nis2EntityType,
|
||||||
|
'sector' => $nis2Sector,
|
||||||
|
'admin_user_id' => $userId,
|
||||||
|
'admin_email' => $adminEmail,
|
||||||
|
// Credenziali machine-to-machine
|
||||||
|
'api_key' => $rawKey,
|
||||||
|
'api_key_scopes'=> ['read:all', 'write:all', 'admin:org', 'sso:login'],
|
||||||
|
'api_key_expires_at' => $expiresAt,
|
||||||
|
// JWT per apertura immediata UI
|
||||||
|
'access_token' => $jwt,
|
||||||
|
'token_type' => 'Bearer',
|
||||||
|
'token_expires_in' => 7200,
|
||||||
|
// Credenziali primo accesso admin (cambio obbligatorio)
|
||||||
|
'temp_password' => !$existingUser ? $tempPassword : null,
|
||||||
|
'must_change_password' => !isset($existingUser),
|
||||||
|
// Link
|
||||||
|
'dashboard_url' => APP_URL . '/dashboard.html',
|
||||||
|
'settings_url' => APP_URL . '/settings.html',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/services/status
|
* GET /api/services/status
|
||||||
* Health check + info piattaforma. Nessuna auth richiesta.
|
* Health check + info piattaforma. Nessuna auth richiesta.
|
||||||
|
|||||||
68
docs/sql/011_provisioning.sql
Normal file
68
docs/sql/011_provisioning.sql
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- NIS2 Agile — Migration 011: Provisioning B2B
|
||||||
|
-- Aggiunge colonne per onboarding automatico da lg231 e altri
|
||||||
|
-- sistemi Agile partner via POST /api/services/provision
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
USE nis2_agile_db;
|
||||||
|
|
||||||
|
-- ── organizations: campi provisioning ─────────────────────────────────────
|
||||||
|
|
||||||
|
-- Chi ha provisioned questa org (es: 'lg231')
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN IF NOT EXISTS provisioned_by VARCHAR(64) NULL AFTER status;
|
||||||
|
|
||||||
|
-- Timestamp del provisioning
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN IF NOT EXISTS provisioned_at DATETIME NULL AFTER provisioned_by;
|
||||||
|
|
||||||
|
-- Piano licenza (essentials/professional/enterprise)
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN IF NOT EXISTS license_plan VARCHAR(32) NULL DEFAULT 'professional' AFTER provisioned_at;
|
||||||
|
|
||||||
|
-- Scadenza licenza
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN IF NOT EXISTS license_expires_at DATE NULL AFTER license_plan;
|
||||||
|
|
||||||
|
-- ID azienda nel sistema chiamante (es: company_id di lg231)
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN IF NOT EXISTS lg231_company_id INT NULL AFTER license_expires_at;
|
||||||
|
|
||||||
|
-- Riferimento ordine nel sistema chiamante
|
||||||
|
ALTER TABLE organizations
|
||||||
|
ADD COLUMN IF NOT EXISTS lg231_order_id VARCHAR(64) NULL AFTER lg231_company_id;
|
||||||
|
|
||||||
|
-- ── users: campo must_change_password ─────────────────────────────────────
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS must_change_password TINYINT(1) NOT NULL DEFAULT 0 AFTER status;
|
||||||
|
|
||||||
|
-- Campo phone
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS phone VARCHAR(32) NULL AFTER must_change_password;
|
||||||
|
|
||||||
|
-- Campo job_title
|
||||||
|
ALTER TABLE users
|
||||||
|
ADD COLUMN IF NOT EXISTS job_title VARCHAR(128) NULL AFTER phone;
|
||||||
|
|
||||||
|
-- ── api_keys: campo created_by ─────────────────────────────────────────────
|
||||||
|
ALTER TABLE api_keys
|
||||||
|
ADD COLUMN IF NOT EXISTS created_by VARCHAR(128) NULL AFTER last_used_at;
|
||||||
|
|
||||||
|
-- ── Indici ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Ricerca per VAT number (idempotency provisioning)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_organizations_vat ON organizations(vat_number);
|
||||||
|
|
||||||
|
-- Ricerca org per lg231_company_id
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_organizations_lg231 ON organizations(lg231_company_id);
|
||||||
|
|
||||||
|
-- ── Verifica ──────────────────────────────────────────────────────────────
|
||||||
|
SELECT
|
||||||
|
COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = 'nis2_agile_db'
|
||||||
|
AND TABLE_NAME = 'organizations'
|
||||||
|
AND COLUMN_NAME IN ('provisioned_by','provisioned_at','license_plan','license_expires_at','lg231_company_id','lg231_order_id')
|
||||||
|
ORDER BY ORDINAL_POSITION;
|
||||||
|
|
||||||
|
SELECT 'Migration 011 provisioning completata.' AS stato;
|
||||||
@ -298,6 +298,7 @@ $actionMap = [
|
|||||||
|
|
||||||
// ── ServicesController (API pubblica) ──────────
|
// ── ServicesController (API pubblica) ──────────
|
||||||
'services' => [
|
'services' => [
|
||||||
|
'POST:provision' => 'provision',
|
||||||
'POST:token' => 'token',
|
'POST:token' => 'token',
|
||||||
'POST:sso' => 'sso',
|
'POST:sso' => 'sso',
|
||||||
'GET:status' => 'status',
|
'GET:status' => 'status',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user