Phase 11D: Blueprint license activation, phone-home, and tier gating
- LicenseService.php: activate/validate/deactivate + 7-day grace period - ValidateLicense.php: mvc:validate Artisan command (daily cron) - Updated controller.php: license activation/deactivation in update handler - Updated view.blade.php: order ID input, status indicator (green/yellow/red), grace/expired banners, dynamic pro-tier field gating, update-available card Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7a2a1d8dbd
commit
8872f67727
35
docs/code-bridge/archive/RES-2026-04-12-phase11bc-deploy.md
Normal file
35
docs/code-bridge/archive/RES-2026-04-12-phase11bc-deploy.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Architectural Response
|
||||||
|
|
||||||
|
**Re:** Phase 11B/C Deployment Complete
|
||||||
|
**Date:** 2026-04-12
|
||||||
|
**From:** Chronicler #84 — The Meridian
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status: DEPLOYED ✅
|
||||||
|
|
||||||
|
### Discord Role Created
|
||||||
|
- Name: **ModpackChecker Customer**
|
||||||
|
- Color: Frost teal (#1A9ECF)
|
||||||
|
- ID: `1493061127423262870`
|
||||||
|
|
||||||
|
### Env Var Added
|
||||||
|
```
|
||||||
|
MVC_CUSTOMER_ROLE_ID=1493061127423262870
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Deployed
|
||||||
|
- `src/discord/verifymvc.js` ✅
|
||||||
|
- `src/discord/events.js` ✅
|
||||||
|
- `src/index.js` ✅
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- Health: `{"status":"online","bot":"The Arbiter#6636"}` ✅
|
||||||
|
- Logs: `✅ Successfully reloaded application (/) commands` ✅
|
||||||
|
- `/verify-mvc` is live in Discord
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Proceed to Phase 11D.
|
||||||
|
|
||||||
|
*— Chronicler #84, The Meridian*
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
# Code Status Update
|
# Code Status Update
|
||||||
**Last Updated:** 2026-04-12 20:45 CDT
|
**Last Updated:** 2026-04-12 21:15 CDT
|
||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
Phase 11B/C complete — /verify-mvc slash command written. Ready for Chronicler deployment + role creation.
|
Phase 11D complete — Blueprint extension licensing integration written. Ready for testing.
|
||||||
|
|
||||||
## Recently Completed
|
## Recently Completed
|
||||||
- Phase 11A: DEPLOYED ✅ — migration ran, API routes live, health check passed
|
- Phase 11A: DEPLOYED ✅ — DB + API routes live on Command Center
|
||||||
- Phase 11B/C: Created `src/discord/verifymvc.js` — /verify-mvc slash command
|
- Phase 11B/C: DEPLOYED ✅ — /verify-mvc live, customer role created (1493061127423262870)
|
||||||
- Phase 11B/C: Wired into events.js handler + index.js command registration
|
- Phase 11D: Created LicenseService.php — activate, validate, deactivate, grace period, tier check
|
||||||
- Command assigns MVC_CUSTOMER_ROLE_ID role on successful verification
|
- Phase 11D: Created ValidateLicense.php — `mvc:validate` Artisan command (daily phone-home)
|
||||||
|
- Phase 11D: Updated admin controller.php — license activation/deactivation in update handler
|
||||||
|
- Phase 11D: Updated admin view.blade.php — order ID input, status indicator, grace/expired banners, dynamic tier gating
|
||||||
|
|
||||||
## Next Steps Pending
|
## Next Steps Pending
|
||||||
- **DEPLOY: Chronicler restarts Arbiter + creates ModpackChecker Customer role on Discord**
|
- Phase 11D testing: build .blueprint, install, verify license flow end-to-end
|
||||||
- **Chronicler needs to:** set MVC_CUSTOMER_ROLE_ID in .env after creating role
|
|
||||||
- Phase 11D: Blueprint extension — license activation UI, phone-home cron, tier gating
|
|
||||||
- Phase 11E: GitBook knowledge base migration
|
- Phase 11E: GitBook knowledge base migration
|
||||||
- Phase 11F: BuiltByBit listing creation (Standard $14.99, Professional $24.99)
|
- Phase 11F: BuiltByBit listing creation (Standard $14.99, Professional $24.99)
|
||||||
- Phase 11G: Business hours & support boundaries
|
- Phase 11G: Business hours & support boundaries
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use Illuminate\View\Factory as ViewFactory;
|
|||||||
use Pterodactyl\Http\Controllers\Controller;
|
use Pterodactyl\Http\Controllers\Controller;
|
||||||
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
|
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
|
||||||
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
|
use Pterodactyl\Http\Requests\Admin\AdminFormRequest;
|
||||||
|
use Pterodactyl\Services\LicenseService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
|
||||||
class modpackcheckerExtensionController extends Controller
|
class modpackcheckerExtensionController extends Controller
|
||||||
@@ -14,54 +15,70 @@ class modpackcheckerExtensionController extends Controller
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private ViewFactory $view,
|
private ViewFactory $view,
|
||||||
private BlueprintExtensionLibrary $blueprint,
|
private BlueprintExtensionLibrary $blueprint,
|
||||||
|
private LicenseService $licenseService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function index(): View
|
public function index(): View
|
||||||
{
|
{
|
||||||
// Get current settings
|
|
||||||
$curseforge_api_key = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
|
$curseforge_api_key = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
|
||||||
$discord_webhook_url = $this->blueprint->dbGet('modpackchecker', 'discord_webhook_url');
|
$discord_webhook_url = $this->blueprint->dbGet('modpackchecker', 'discord_webhook_url');
|
||||||
$check_interval = $this->blueprint->dbGet('modpackchecker', 'check_interval');
|
$check_interval = $this->blueprint->dbGet('modpackchecker', 'check_interval');
|
||||||
$tier = $this->blueprint->dbGet('modpackchecker', 'tier');
|
|
||||||
|
|
||||||
// Set defaults if empty
|
|
||||||
if ($check_interval == '') {
|
if ($check_interval == '') {
|
||||||
$this->blueprint->dbSet('modpackchecker', 'check_interval', 'daily');
|
$this->blueprint->dbSet('modpackchecker', 'check_interval', 'daily');
|
||||||
$check_interval = 'daily';
|
$check_interval = 'daily';
|
||||||
}
|
}
|
||||||
if ($tier == '') {
|
|
||||||
$this->blueprint->dbSet('modpackchecker', 'tier', 'standard');
|
$license = $this->licenseService->getState();
|
||||||
$tier = 'standard';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->view->make(
|
return $this->view->make(
|
||||||
'admin.extensions.modpackchecker.index', [
|
'admin.extensions.modpackchecker.index', [
|
||||||
'curseforge_api_key' => $curseforge_api_key,
|
'curseforge_api_key' => $curseforge_api_key,
|
||||||
'discord_webhook_url' => $discord_webhook_url,
|
'discord_webhook_url' => $discord_webhook_url,
|
||||||
'check_interval' => $check_interval,
|
'check_interval' => $check_interval,
|
||||||
'tier' => $tier,
|
'license' => $license,
|
||||||
'root' => '/admin/extensions/modpackchecker',
|
'root' => '/admin/extensions/modpackchecker',
|
||||||
'blueprint' => $this->blueprint,
|
'blueprint' => $this->blueprint,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
|
||||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
|
||||||
*/
|
|
||||||
public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse
|
public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
|
// Handle license activation
|
||||||
|
$orderId = $request->input('order_id');
|
||||||
|
if (!empty($orderId)) {
|
||||||
|
$currentOrderId = $this->blueprint->dbGet('modpackchecker', 'order_id');
|
||||||
|
if ($orderId !== $currentOrderId) {
|
||||||
|
$result = $this->licenseService->activate($orderId);
|
||||||
|
if (!$result['success']) {
|
||||||
|
return redirect()
|
||||||
|
->route('admin.extensions.modpackchecker.index')
|
||||||
|
->with('error', 'License activation failed: ' . ($result['error'] ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle license deactivation
|
||||||
|
if ($request->input('deactivate_license') === '1') {
|
||||||
|
$this->licenseService->deactivate();
|
||||||
|
return redirect()
|
||||||
|
->route('admin.extensions.modpackchecker.index')
|
||||||
|
->with('success', 'License deactivated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save standard settings
|
||||||
$this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key') ?? '');
|
$this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key') ?? '');
|
||||||
|
|
||||||
// Only save PRO-tier fields if the user is on the pro tier
|
// Only save PRO-tier fields if licensed as professional
|
||||||
$tier = $this->blueprint->dbGet('modpackchecker', 'tier') ?: 'standard';
|
if ($this->licenseService->isProFeatureAllowed()) {
|
||||||
if ($tier === 'pro') {
|
|
||||||
$this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? '');
|
$this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? '');
|
||||||
$this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily');
|
$this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily');
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->route('admin.extensions.modpackchecker.index')->with('success', 'Settings saved successfully.');
|
return redirect()
|
||||||
|
->route('admin.extensions.modpackchecker.index')
|
||||||
|
->with('success', 'Settings saved successfully.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +87,8 @@ class modpackcheckerSettingsFormRequest extends AdminFormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'order_id' => 'nullable|string|max:64',
|
||||||
|
'deactivate_license' => 'nullable|string',
|
||||||
'curseforge_api_key' => 'nullable|string|max:500',
|
'curseforge_api_key' => 'nullable|string|max:500',
|
||||||
'discord_webhook_url' => 'nullable|url|max:500',
|
'discord_webhook_url' => 'nullable|url|max:500',
|
||||||
'check_interval' => 'nullable|in:daily,12h,6h',
|
'check_interval' => 'nullable|in:daily,12h,6h',
|
||||||
@@ -79,6 +98,7 @@ class modpackcheckerSettingsFormRequest extends AdminFormRequest
|
|||||||
public function attributes(): array
|
public function attributes(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'order_id' => 'Order ID',
|
||||||
'curseforge_api_key' => 'CurseForge API Key',
|
'curseforge_api_key' => 'CurseForge API Key',
|
||||||
'discord_webhook_url' => 'Discord Webhook URL',
|
'discord_webhook_url' => 'Discord Webhook URL',
|
||||||
'check_interval' => 'Check Interval',
|
'check_interval' => 'Check Interval',
|
||||||
|
|||||||
@@ -61,7 +61,81 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- License Status Banner -->
|
||||||
|
@if($license['status'] === 'expired')
|
||||||
|
<div class="row" style="margin-bottom: 15px;">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div style="background: #4a1a1a; border-left: 4px solid #e74c3c; border-radius: 4px; padding: 15px;">
|
||||||
|
<h4 style="margin: 0 0 5px 0; color: #e74c3c;"><i class="fa fa-exclamation-triangle"></i> License Expired</h4>
|
||||||
|
<p style="margin: 0; color: #ccc;">Your license could not be validated. Pro features are disabled. Please check your order ID or contact support.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@elseif($license['status'] === 'grace')
|
||||||
|
<div class="row" style="margin-bottom: 15px;">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div style="background: #4a3a1a; border-left: 4px solid #f39c12; border-radius: 4px; padding: 15px;">
|
||||||
|
<h4 style="margin: 0 0 5px 0; color: #f39c12;"><i class="fa fa-clock-o"></i> License Validation Warning</h4>
|
||||||
|
<p style="margin: 0; color: #ccc;">Could not reach license server. Grace period expires: <strong>{{ $license['grace_expires'] }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- License Activation -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">
|
||||||
|
<i class="fa fa-id-card"></i> License
|
||||||
|
@if($license['status'] === 'active')
|
||||||
|
<span class="label label-success" style="margin-left: 10px;">Active</span>
|
||||||
|
@elseif($license['status'] === 'grace')
|
||||||
|
<span class="label label-warning" style="margin-left: 10px;">Grace Period</span>
|
||||||
|
@elseif($license['status'] === 'expired')
|
||||||
|
<span class="label label-danger" style="margin-left: 10px;">Expired</span>
|
||||||
|
@else
|
||||||
|
<span class="label label-default" style="margin-left: 10px;">Not Activated</span>
|
||||||
|
@endif
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label">BuiltByBit Order ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="order_id"
|
||||||
|
id="order_id"
|
||||||
|
value="{{ $license['order_id'] }}"
|
||||||
|
placeholder="Enter your order ID..."
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
<p class="text-muted small" style="margin-top: 8px;">
|
||||||
|
Enter the order ID from your BuiltByBit purchase to activate.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@if($license['status'] === 'active' || $license['status'] === 'grace')
|
||||||
|
<div style="margin-top: 8px; padding: 10px; background: #2a2a3e; border-radius: 4px;">
|
||||||
|
<p style="margin: 0; color: #aaa; font-size: 12px;">
|
||||||
|
<strong>Tier:</strong> {{ ucfirst($license['tier']) }}
|
||||||
|
|
|
||||||
|
<strong>Last Validated:</strong> {{ $license['last_validated'] ?: 'Never' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@if(!empty($license['order_id']))
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<button type="submit" name="deactivate_license" value="1" class="btn btn-danger btn-xs"
|
||||||
|
onclick="return confirm('Are you sure you want to deactivate? This frees the activation slot.')">
|
||||||
|
Deactivate License
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- CurseForge API Key -->
|
<!-- CurseForge API Key -->
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<div class="box box-primary">
|
<div class="box box-primary">
|
||||||
@@ -91,7 +165,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<!-- Check Interval (PRO TIER) -->
|
<!-- Check Interval (PRO TIER) -->
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<div class="box box-info">
|
<div class="box box-info">
|
||||||
@@ -104,22 +180,25 @@
|
|||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label">Automatic Check Frequency</label>
|
<label class="control-label">Automatic Check Frequency</label>
|
||||||
<select class="form-control" name="check_interval" id="check_interval" disabled>
|
<select class="form-control" name="check_interval" id="check_interval"
|
||||||
|
{{ $license['tier'] !== 'professional' || $license['status'] === 'expired' ? 'disabled' : '' }}>
|
||||||
<option value="daily" {{ $check_interval === 'daily' ? 'selected' : '' }}>Daily (24 Hours)</option>
|
<option value="daily" {{ $check_interval === 'daily' ? 'selected' : '' }}>Daily (24 Hours)</option>
|
||||||
<option value="12h" {{ $check_interval === '12h' ? 'selected' : '' }}>Every 12 Hours</option>
|
<option value="12h" {{ $check_interval === '12h' ? 'selected' : '' }}>Every 12 Hours</option>
|
||||||
<option value="6h" {{ $check_interval === '6h' ? 'selected' : '' }}>Every 6 Hours</option>
|
<option value="6h" {{ $check_interval === '6h' ? 'selected' : '' }}>Every 6 Hours</option>
|
||||||
</select>
|
</select>
|
||||||
<p class="text-muted small" style="margin-top: 8px;">
|
<p class="text-muted small" style="margin-top: 8px;">
|
||||||
Standard tier is locked to daily cron checks.
|
@if($license['tier'] === 'professional' && $license['status'] !== 'expired')
|
||||||
Upgrade to Professional for more frequent automated checks.
|
Professional tier — custom check intervals enabled.
|
||||||
|
@else
|
||||||
|
Standard tier is locked to daily checks.
|
||||||
|
Upgrade to Professional for more frequent automated checks.
|
||||||
|
@endif
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<!-- Discord Webhook (PRO TIER) -->
|
<!-- Discord Webhook (PRO TIER) -->
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<div class="box box-success">
|
<div class="box box-success">
|
||||||
@@ -139,16 +218,22 @@
|
|||||||
value="{{ $discord_webhook_url }}"
|
value="{{ $discord_webhook_url }}"
|
||||||
placeholder="https://discord.com/api/webhooks/..."
|
placeholder="https://discord.com/api/webhooks/..."
|
||||||
class="form-control"
|
class="form-control"
|
||||||
disabled
|
{{ $license['tier'] !== 'professional' || $license['status'] === 'expired' ? 'disabled' : '' }}
|
||||||
/>
|
/>
|
||||||
<p class="text-muted small" style="margin-top: 8px;">
|
<p class="text-muted small" style="margin-top: 8px;">
|
||||||
Upgrade to Professional to receive automated update alerts in your Discord server.
|
@if($license['tier'] === 'professional' && $license['status'] !== 'expired')
|
||||||
|
Professional tier — Discord webhook alerts enabled.
|
||||||
|
@else
|
||||||
|
Upgrade to Professional to receive automated update alerts in your Discord server.
|
||||||
|
@endif
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
<!-- Supported Platforms -->
|
<!-- Supported Platforms -->
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<div class="box box-default">
|
<div class="box box-default">
|
||||||
@@ -177,12 +262,37 @@
|
|||||||
<li style="padding: 8px 0;">
|
<li style="padding: 8px 0;">
|
||||||
<i class="fa fa-cube" style="color: #e04e39; width: 20px;"></i>
|
<i class="fa fa-cube" style="color: #e04e39; width: 20px;"></i>
|
||||||
<strong>FTB (modpacks.ch)</strong>
|
<strong>FTB (modpacks.ch)</strong>
|
||||||
<span class="text-muted small">(No key required)</span>
|
<span class="text-muted small">
|
||||||
|
@if($license['tier'] === 'professional')
|
||||||
|
(Enabled — Professional)
|
||||||
|
@else
|
||||||
|
(PRO TIER only)
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Update Available -->
|
||||||
|
@if(!empty($license['latest_version']) && $license['latest_version'] !== '1.0.0')
|
||||||
|
<div class="col-xs-12 col-md-6">
|
||||||
|
<div class="box box-warning">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">
|
||||||
|
<i class="fa fa-download"></i> Update Available
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<p>A new version (<strong>{{ $license['latest_version'] }}</strong>) is available.</p>
|
||||||
|
<a href="https://builtbybit.com/" target="_blank" class="btn btn-warning btn-sm">
|
||||||
|
Download from BuiltByBit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer Info -->
|
<!-- Footer Info -->
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MVC License Validation — daily phone-home cron command.
|
||||||
|
*
|
||||||
|
* USAGE:
|
||||||
|
* php artisan mvc:validate
|
||||||
|
*
|
||||||
|
* CRON SCHEDULE:
|
||||||
|
* 0 4 * * * cd /var/www/pterodactyl && php artisan mvc:validate >> /dev/null 2>&1
|
||||||
|
*
|
||||||
|
* Calls Arbiter /api/mvc/validate to confirm license is still active.
|
||||||
|
* On failure, enters 7-day grace period. After grace expires, marks expired.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Pterodactyl\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Pterodactyl\Services\LicenseService;
|
||||||
|
|
||||||
|
class ValidateLicense extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'mvc:validate';
|
||||||
|
protected $description = 'Validate ModpackChecker license (daily phone-home)';
|
||||||
|
|
||||||
|
public function __construct(private LicenseService $licenseService)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Validating ModpackChecker license...');
|
||||||
|
|
||||||
|
$result = $this->licenseService->validate();
|
||||||
|
|
||||||
|
match ($result['status']) {
|
||||||
|
'active' => $this->info("✅ License active — tier: {$result['tier']}"),
|
||||||
|
'grace' => $this->warn("⚠️ Grace period — expires: {$result['expires']}"),
|
||||||
|
'expired' => $this->error("❌ License expired"),
|
||||||
|
'inactive' => $this->line("No license configured"),
|
||||||
|
default => $this->line("Status: {$result['status']}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
$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') ?: '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user