'(aucun — voit uniquement « default »)']; foreach ($registry['channels'] as $c) { $name = (string)($c['name'] ?? ''); if ($name === '' || $name === 'default') continue; // default est l'option implicite (clé vide) $label = $c['label'] ?? $name; $out[$name] = "{$name} — {$label}"; } return $out; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { Auth::checkCsrf(); $action = $_POST['action'] ?? ''; try { if ($action === 'create') { $owner = trim($_POST['owner'] ?? ''); $until = trim($_POST['until'] ?? ''); $maxMachines = max(1, (int)($_POST['max_machines'] ?? 1)); $notes = trim($_POST['notes'] ?? '') ?: null; // Channel = '' ↦ NULL en DB (default manifest). Whitelist regex anti-injection. $channel = trim((string)($_POST['channel'] ?? '')); if ($channel === '') { $channel = null; } elseif (!preg_match('/^[a-z0-9_-]{1,64}$/', $channel)) { throw new Exception('Channel invalide : seules les lettres minuscules, chiffres, _ et - sont autorisés (max 64 caractères).'); } $canSeeBetas = isset($_POST['can_see_betas']) ? 1 : 0; if ($owner === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $until)) { throw new Exception('Nom du client et date d\'expiration sont requis (date au format YYYY-MM-DD).'); } for ($attempts = 0; $attempts < 5; $attempts++) { $newKey = generateLicenseKey(); try { $db->prepare('INSERT INTO licenses (license_key, owner_name, issued_at, download_entitlement_until, max_machines, channel, can_see_betas, notes) VALUES (?, ?, NOW(), ?, ?, ?, ?, ?)') ->execute([$newKey, $owner, $until . ' 23:59:59', $maxMachines, $channel, $canSeeBetas, $notes]); $message = "License émise avec succès. Note la clé maintenant — elle ne sera plus jamais affichée."; break; } catch (PDOException $e) { if (str_contains($e->getMessage(), 'Duplicate')) { continue; } throw $e; } } } elseif ($action === 'revoke') { $id = (int)($_POST['id'] ?? 0); $db->prepare('UPDATE licenses SET revoked_at = NOW() WHERE id = ?')->execute([$id]); $message = "License #{$id} révoquée."; } elseif ($action === 'unrevoke') { $id = (int)($_POST['id'] ?? 0); $db->prepare('UPDATE licenses SET revoked_at = NULL WHERE id = ?')->execute([$id]); $message = "License #{$id} réactivée."; } elseif ($action === 'extend') { $id = (int)($_POST['id'] ?? 0); $newUntil = trim($_POST['new_until'] ?? ''); if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $newUntil)) { throw new Exception('Date invalide.'); } $db->prepare('UPDATE licenses SET download_entitlement_until = ? WHERE id = ?') ->execute([$newUntil . ' 23:59:59', $id]); $message = "License #{$id} prolongée jusqu'au {$newUntil}."; } elseif ($action === 'set_channel') { $id = (int)($_POST['id'] ?? 0); $channel = trim((string)($_POST['channel'] ?? '')); if ($channel === '') { $channel = null; // default manifest } elseif (!preg_match('/^[a-z0-9_-]{1,64}$/', $channel)) { throw new Exception('Channel invalide.'); } $db->prepare('UPDATE licenses SET channel = ? WHERE id = ?')->execute([$channel, $id]); $label = $channel ?? 'default'; $message = "License #{$id} : channel passé à « {$label} ». Le client devra rouvrir le launcher pour prendre en compte le changement (revalidation license)."; } elseif ($action === 'toggle_betas') { $id = (int)($_POST['id'] ?? 0); $stmt = $db->prepare('UPDATE licenses SET can_see_betas = 1 - can_see_betas WHERE id = ?'); $stmt->execute([$id]); $row = $db->prepare('SELECT can_see_betas FROM licenses WHERE id = ?'); $row->execute([$id]); $now = (int)$row->fetchColumn(); $message = $now ? "License #{$id} : accès aux versions BÊTA activé." : "License #{$id} : accès aux versions BÊTA désactivé."; } elseif ($action === 'set_max_machines') { $id = (int)($_POST['id'] ?? 0); $newMax = (int)($_POST['max_machines'] ?? 0); if ($id <= 0 || $newMax < 1 || $newMax > 100) { throw new Exception('Nombre de machines invalide (1–100).'); } // Refus de descendre sous le nombre de machines déjà activées — // sinon état incohérent (machines_count > max_machines). L'admin doit // d'abord libérer des slots via "Libérer" individuel ou "Libérer toutes". $stmt = $db->prepare('SELECT COUNT(*) FROM license_machines WHERE license_id = ?'); $stmt->execute([$id]); $current = (int)$stmt->fetchColumn(); if ($newMax < $current) { throw new Exception("Impossible : {$current} machines sont déjà actives. Libère des slots d'abord ou choisis ≥ {$current}."); } $db->prepare('UPDATE licenses SET max_machines = ? WHERE id = ?')->execute([$newMax, $id]); $message = "License #{$id} : limite passée à {$newMax} machine(s)."; } elseif ($action === 'reset_machines') { $id = (int)($_POST['id'] ?? 0); $db->prepare('DELETE FROM license_machines WHERE license_id = ?')->execute([$id]); $message = "Toutes les machines libérées pour la license #{$id}."; } elseif ($action === 'remove_machine') { $licenseId = (int)($_POST['license_id'] ?? 0); $machineId = trim((string)($_POST['machine_id'] ?? '')); if ($licenseId <= 0 || $machineId === '') { throw new Exception('license_id et machine_id requis'); } $stmt = $db->prepare('DELETE FROM license_machines WHERE license_id = ? AND machine_id = ?'); $stmt->execute([$licenseId, $machineId]); $count = $stmt->rowCount(); $shortId = substr($machineId, 0, 12); $message = $count > 0 ? "Machine {$shortId}… libérée pour la license #{$licenseId}." : "Aucune machine correspondante trouvée pour license #{$licenseId}."; } } catch (Exception $e) { $message = $e->getMessage(); $messageType = 'error'; } } $availableChannels = listAvailableChannels(); $licenses = $db->query( 'SELECT l.*, (SELECT COUNT(*) FROM license_machines m WHERE m.license_id = l.id) AS machines_count FROM licenses l ORDER BY l.id DESC' )->fetchAll(); // Pré-fetch toutes les machines de toutes les licenses en une seule requête, // indexées par license_id pour l'affichage par-row sans N+1. $allMachines = $db->query( 'SELECT license_id, machine_id, machine_label, first_seen, last_seen FROM license_machines ORDER BY last_seen DESC' )->fetchAll(); $machinesByLicense = []; foreach ($allMachines as $m) { $machinesByLicense[(int)$m['license_id']][] = $m; } Layout::header('Licenses', 'licenses'); ?>
= htmlspecialchars($newKey) ?>
Elle est stockée hashée — la prochaine fois que tu rechargeras cette page elle ne sera plus visible.
| ID | Client | Émise | Expire | Machines | Channel / Bêta | Statut | Actions | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| #= $l['id'] ?> |
= htmlspecialchars($l['owner_name']) ?>
= htmlspecialchars($l['notes']) ?>
|
= date('d/m/Y', strtotime($l['issued_at'])) ?> | = date('d/m/Y', strtotime($l['download_entitlement_until'])) ?> | 0): ?> = $l['machines_count'] ?> / = $l['max_machines'] ?> ▾ 0 / = $l['max_machines'] ?> |
|
= $status ?> |
ProlongerSlots |
||||||||||
|
Machines actives sur la license #= $l['id'] ?> (= htmlspecialchars($l['owner_name']) ?>) :
|
|||||||||||||||||
| Aucune license émise. | |||||||||||||||||