[LICENSE] Gestione licenze marketing + campi commerciali estesi

- licenseExt.html: dashboard marketing per generare/gestire licenze
  Login JWT super_admin, stats strip (totali/usate/orgs/utenti),
  form genera con label/piano/durata/max-aziende/max-utenti/prezzo/reseller,
  lista paginata con filtri stato+canale, dettaglio modale, revoca/rigenera,
  export CSV e copia token/URL
- Migration 013: invites +max_users_per_org, +price_eur, +reseller_name
  organizations +license_max_users (da provisioning)
- InviteController::create() gestisce nuovi campi, validate() espone max_users_per_org
- ServicesController::provision() salva license_max_users nell'org

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-03-07 15:34:38 +01:00
co-authored by Claude Sonnet 4.6
parent 612befd66d
commit cb0988da27
4 changed files with 962 additions and 26 deletions
+32 -24
View File
@@ -69,6 +69,9 @@ class InviteController extends BaseController
$restrictVat = preg_replace('/[^0-9]/', '', $body['restrict_vat'] ?? '');
$restrictEmail = filter_var($body['restrict_email'] ?? '', FILTER_VALIDATE_EMAIL) ?: null;
$notes = trim($body['notes'] ?? '') ?: null;
$maxUsersPerOrg = isset($body['max_users_per_org']) ? max(1, min(9999, (int)$body['max_users_per_org'])) : null;
$priceEur = isset($body['price_eur']) ? round((float)$body['price_eur'], 2) : null;
$resellerName = substr(trim($body['reseller_name'] ?? ''), 0, 128) ?: null;
$issuedBy = $this->getCurrentUserId();
$expiresAt = date('Y-m-d H:i:s', strtotime("+{$expiresDays} days"));
@@ -81,37 +84,41 @@ class InviteController extends BaseController
Database::execute(
'INSERT INTO invites
(token_prefix, token_hash, plan, duration_months, label, notes,
max_uses, expires_at, channel, issued_to, issued_by,
max_uses, max_users_per_org, price_eur, reseller_name,
expires_at, channel, issued_to, issued_by,
restrict_vat, restrict_email)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)',
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
[
$prefix, $tokenHash, $plan, $durationMonths,
$label, $notes, $maxUses, $expiresAt,
$channel, $issuedTo, $issuedBy,
$label, $notes, $maxUses, $maxUsersPerOrg, $priceEur, $resellerName,
$expiresAt, $channel, $issuedTo, $issuedBy,
$restrictVat ?: null, $restrictEmail,
]
);
$inviteId = (int) Database::lastInsertId();
$created[] = [
'id' => $inviteId,
'token' => $rawToken, // SOLO qui viene restituito in chiaro
'token_prefix' => $prefix,
'plan' => $plan,
'duration_months' => $durationMonths,
'max_uses' => $maxUses,
'expires_at' => $expiresAt,
'channel' => $channel,
'issued_to' => $issuedTo,
'label' => $label,
'invite_url' => APP_URL . '/onboarding.html?invite=' . urlencode($rawToken),
'provision_hint' => 'POST /api/services/provision con invite_token: "' . $rawToken . '"',
'id' => $inviteId,
'token' => $rawToken, // SOLO qui viene restituito in chiaro
'token_prefix' => $prefix,
'plan' => $plan,
'duration_months' => $durationMonths,
'max_uses' => $maxUses,
'max_users_per_org' => $maxUsersPerOrg,
'price_eur' => $priceEur,
'expires_at' => $expiresAt,
'channel' => $channel,
'issued_to' => $issuedTo,
'label' => $label,
'invite_url' => APP_URL . '/onboarding.html?invite=' . urlencode($rawToken),
'provision_hint' => 'POST /api/services/provision con invite_token: "' . $rawToken . '"',
];
// Audit
$this->logAudit('invite.created', 'invite', $inviteId, [
'plan' => $plan, 'channel' => $channel,
'expires_at' => $expiresAt, 'max_uses' => $maxUses,
'max_users_per_org' => $maxUsersPerOrg,
]);
}
@@ -263,14 +270,15 @@ class InviteController extends BaseController
$inv = $result['invite'];
$this->jsonSuccess([
'valid' => true,
'plan' => $inv['plan'],
'duration_months' => (int) $inv['duration_months'],
'expires_at' => $inv['expires_at'],
'remaining_uses' => (int)$inv['max_uses'] - (int)$inv['used_count'],
'channel' => $inv['channel'],
'label' => $inv['label'],
'plan_features' => self::planFeatures($inv['plan']),
'valid' => true,
'plan' => $inv['plan'],
'duration_months' => (int) $inv['duration_months'],
'expires_at' => $inv['expires_at'],
'remaining_uses' => (int)$inv['max_uses'] - (int)$inv['used_count'],
'max_users_per_org' => $inv['max_users_per_org'] !== null ? (int)$inv['max_users_per_org'] : null,
'channel' => $inv['channel'],
'label' => $inv['label'],
'plan_features' => self::planFeatures($inv['plan']),
]);
}