- LicenseService.php: strtoupper(trim()) before sending to Arbiter - mvc.js: toUpperCase().trim() on activate, validate, deactivate, webhook - verifymvc.js: toUpperCase().trim() on /verify-mvc Discord command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
197 lines
7.4 KiB
PHP
197 lines
7.4 KiB
PHP
<?php
|
|
|
|
/**
|
|
* MVC License Service — handles activation, validation, and grace period.
|
|
*
|
|
* Talks to Arbiter API at /api/mvc/* endpoints.
|
|
* Stores license state in Blueprint dbGet/dbSet.
|
|
*
|
|
* Keys stored:
|
|
* modpackchecker.order_id — BuiltByBit order ID
|
|
* modpackchecker.license_status — active|grace|expired|inactive
|
|
* modpackchecker.tier — standard|professional
|
|
* modpackchecker.grace_expires — ISO timestamp (set on first validation failure)
|
|
* modpackchecker.last_validated — ISO timestamp of last successful validation
|
|
* modpackchecker.latest_version — latest version from Arbiter
|
|
*/
|
|
|
|
namespace Pterodactyl\Services;
|
|
|
|
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class LicenseService
|
|
{
|
|
private string $arbiterBase = 'https://discord-bot.firefrostgaming.com/api/mvc';
|
|
private int $graceDays = 7;
|
|
|
|
public function __construct(
|
|
private BlueprintExtensionLibrary $blueprint
|
|
) {}
|
|
|
|
/**
|
|
* Activate a license by order ID.
|
|
* Called from admin UI "Save & Activate" button.
|
|
*/
|
|
public function activate(string $orderId): array
|
|
{
|
|
$orderId = strtoupper(trim($orderId));
|
|
$domain = config('app.url');
|
|
|
|
try {
|
|
$response = Http::timeout(15)->post("{$this->arbiterBase}/activate", [
|
|
'order_id' => $orderId,
|
|
'domain' => $domain,
|
|
'ip' => request()->server('SERVER_ADDR', ''),
|
|
]);
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->successful() && ($data['status'] ?? '') === 'active') {
|
|
$this->blueprint->dbSet('modpackchecker', 'order_id', $orderId);
|
|
$this->blueprint->dbSet('modpackchecker', 'license_status', 'active');
|
|
$this->blueprint->dbSet('modpackchecker', 'tier', $data['tier'] ?? 'standard');
|
|
$this->blueprint->dbSet('modpackchecker', 'last_validated', now()->toISOString());
|
|
$this->blueprint->dbSet('modpackchecker', 'grace_expires', '');
|
|
|
|
return ['success' => true, 'tier' => $data['tier'] ?? 'standard'];
|
|
}
|
|
|
|
return ['success' => false, 'error' => $data['error'] ?? 'Activation failed'];
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error('[MVC License] Activation error: ' . $e->getMessage());
|
|
return ['success' => false, 'error' => 'Could not reach license server. Try again later.'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate (phone-home). Called daily by mvc:validate Artisan command.
|
|
* On failure, enters grace period. After 7 days, marks expired.
|
|
*/
|
|
public function validate(): array
|
|
{
|
|
$orderId = $this->blueprint->dbGet('modpackchecker', 'order_id');
|
|
if (empty($orderId)) {
|
|
return ['status' => 'inactive', 'message' => 'No license configured'];
|
|
}
|
|
|
|
$domain = config('app.url');
|
|
|
|
try {
|
|
$response = Http::timeout(15)->post("{$this->arbiterBase}/validate", [
|
|
'order_id' => $orderId,
|
|
'domain' => $domain,
|
|
'version' => config('app.version', '1.0.0'),
|
|
'php_version' => PHP_VERSION,
|
|
]);
|
|
|
|
$data = $response->json();
|
|
|
|
if ($response->successful() && ($data['status'] ?? '') === 'active') {
|
|
$this->blueprint->dbSet('modpackchecker', 'license_status', 'active');
|
|
$this->blueprint->dbSet('modpackchecker', 'tier', $data['tier'] ?? 'standard');
|
|
$this->blueprint->dbSet('modpackchecker', 'last_validated', now()->toISOString());
|
|
$this->blueprint->dbSet('modpackchecker', 'grace_expires', '');
|
|
|
|
if (!empty($data['latest_version'])) {
|
|
$this->blueprint->dbSet('modpackchecker', 'latest_version', $data['latest_version']);
|
|
}
|
|
|
|
return ['status' => 'active', 'tier' => $data['tier'] ?? 'standard'];
|
|
}
|
|
|
|
return $this->enterGracePeriod($data['error'] ?? 'Validation failed');
|
|
|
|
} catch (\Exception $e) {
|
|
Log::warning('[MVC License] Validation failed: ' . $e->getMessage());
|
|
return $this->enterGracePeriod('Could not reach license server');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enter or continue grace period. After 7 days, expire.
|
|
*/
|
|
private function enterGracePeriod(string $reason): array
|
|
{
|
|
$graceExpires = $this->blueprint->dbGet('modpackchecker', 'grace_expires');
|
|
|
|
if (empty($graceExpires)) {
|
|
// First failure — start grace period
|
|
$expires = now()->addDays($this->graceDays)->toISOString();
|
|
$this->blueprint->dbSet('modpackchecker', 'license_status', 'grace');
|
|
$this->blueprint->dbSet('modpackchecker', 'grace_expires', $expires);
|
|
|
|
Log::warning("[MVC License] Grace period started. Expires: {$expires}. Reason: {$reason}");
|
|
return ['status' => 'grace', 'expires' => $expires, 'reason' => $reason];
|
|
}
|
|
|
|
// Check if grace period expired
|
|
if (now()->greaterThan($graceExpires)) {
|
|
$this->blueprint->dbSet('modpackchecker', 'license_status', 'expired');
|
|
Log::error('[MVC License] Grace period expired. License marked as expired.');
|
|
return ['status' => 'expired', 'reason' => $reason];
|
|
}
|
|
|
|
return ['status' => 'grace', 'expires' => $graceExpires, 'reason' => $reason];
|
|
}
|
|
|
|
/**
|
|
* Deactivate license on this panel.
|
|
*/
|
|
public function deactivate(): array
|
|
{
|
|
$orderId = $this->blueprint->dbGet('modpackchecker', 'order_id');
|
|
if (empty($orderId)) {
|
|
return ['success' => true];
|
|
}
|
|
|
|
try {
|
|
Http::timeout(15)->post("{$this->arbiterBase}/deactivate", [
|
|
'order_id' => $orderId,
|
|
'domain' => config('app.url'),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
// Best-effort — clear local state regardless
|
|
}
|
|
|
|
$this->blueprint->dbSet('modpackchecker', 'order_id', '');
|
|
$this->blueprint->dbSet('modpackchecker', 'license_status', 'inactive');
|
|
$this->blueprint->dbSet('modpackchecker', 'tier', 'standard');
|
|
$this->blueprint->dbSet('modpackchecker', 'grace_expires', '');
|
|
|
|
return ['success' => true];
|
|
}
|
|
|
|
/**
|
|
* Check if a pro feature is allowed based on current tier + license status.
|
|
*/
|
|
public function isProFeatureAllowed(): bool
|
|
{
|
|
$status = $this->blueprint->dbGet('modpackchecker', 'license_status');
|
|
$tier = $this->blueprint->dbGet('modpackchecker', 'tier');
|
|
|
|
if ($status === 'expired') {
|
|
return false;
|
|
}
|
|
|
|
return $tier === 'professional';
|
|
}
|
|
|
|
/**
|
|
* Get full license state for the admin UI.
|
|
*/
|
|
public function getState(): array
|
|
{
|
|
return [
|
|
'order_id' => $this->blueprint->dbGet('modpackchecker', 'order_id') ?: '',
|
|
'status' => $this->blueprint->dbGet('modpackchecker', 'license_status') ?: 'inactive',
|
|
'tier' => $this->blueprint->dbGet('modpackchecker', 'tier') ?: 'standard',
|
|
'grace_expires' => $this->blueprint->dbGet('modpackchecker', 'grace_expires') ?: '',
|
|
'last_validated' => $this->blueprint->dbGet('modpackchecker', 'last_validated') ?: '',
|
|
'latest_version' => $this->blueprint->dbGet('modpackchecker', 'latest_version') ?: '',
|
|
];
|
|
}
|
|
}
|