fix(blueprint): Review fixes — API key lookup, tier enforcement, safety
Fixes 10 issues from Blueprint extension code review: - CurseForge API key now reads via Blueprint dbGet() matching admin save - PRO-tier fields (webhook, interval) enforced server-side, not just UI - json_decode results validated before accessing parsed data - Null user guard on getStatus() endpoint - 429 response uses consistent error key format - Modrinth slug derivation strips special chars, documented as fallback - Check interval dropdown reflects saved value - API key input changed to password type - TypeScript error typing narrowed from any to unknown - Removed unused DB import from ModpackApiService Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3457b87aef
commit
b0aa52c2c8
@@ -53,8 +53,13 @@ class modpackcheckerExtensionController extends Controller
|
||||
public function update(modpackcheckerSettingsFormRequest $request): RedirectResponse
|
||||
{
|
||||
$this->blueprint->dbSet('modpackchecker', 'curseforge_api_key', $request->input('curseforge_api_key') ?? '');
|
||||
$this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? '');
|
||||
$this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily');
|
||||
|
||||
// Only save PRO-tier fields if the user is on the pro tier
|
||||
$tier = $this->blueprint->dbGet('modpackchecker', 'tier') ?: 'standard';
|
||||
if ($tier === 'pro') {
|
||||
$this->blueprint->dbSet('modpackchecker', 'discord_webhook_url', $request->input('discord_webhook_url') ?? '');
|
||||
$this->blueprint->dbSet('modpackchecker', 'check_interval', $request->input('check_interval') ?? 'daily');
|
||||
}
|
||||
|
||||
return redirect()->route('admin.extensions.modpackchecker.index')->with('success', 'Settings saved successfully.');
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label">API Key (BYOK)</label>
|
||||
<input
|
||||
type="text"
|
||||
type="password"
|
||||
name="curseforge_api_key"
|
||||
id="curseforge_api_key"
|
||||
value="{{ $curseforge_api_key }}"
|
||||
@@ -105,9 +105,9 @@
|
||||
<div class="form-group">
|
||||
<label class="control-label">Automatic Check Frequency</label>
|
||||
<select class="form-control" name="check_interval" id="check_interval" disabled>
|
||||
<option value="daily" selected>Daily (24 Hours)</option>
|
||||
<option value="12h">Every 12 Hours</option>
|
||||
<option value="6h">Every 6 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="6h" {{ $check_interval === '6h' ? 'selected' : '' }}>Every 6 Hours</option>
|
||||
</select>
|
||||
<p class="text-muted small" style="margin-top: 8px;">
|
||||
Standard tier is locked to daily cron checks.
|
||||
|
||||
@@ -72,7 +72,7 @@ class ModpackAPIController extends Controller
|
||||
$seconds = RateLimiter::availableIn($limitKey);
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => "Too many requests. Please wait {$seconds} seconds before checking again."
|
||||
'error' => "Too many requests. Please wait {$seconds} seconds before checking again.",
|
||||
], 429);
|
||||
}
|
||||
RateLimiter::hit($limitKey, 60);
|
||||
@@ -161,7 +161,7 @@ class ModpackAPIController extends Controller
|
||||
$manifest = $this->readServerFile($server, 'manifest.json');
|
||||
if ($manifest) {
|
||||
$data = json_decode($manifest, true);
|
||||
if (isset($data['manifestType']) && $data['manifestType'] === 'minecraftModpack') {
|
||||
if (is_array($data) && isset($data['manifestType']) && $data['manifestType'] === 'minecraftModpack') {
|
||||
return [
|
||||
'platform' => 'curseforge',
|
||||
'modpack_id' => $data['projectID'] ?? null,
|
||||
@@ -175,10 +175,12 @@ class ModpackAPIController extends Controller
|
||||
$modrinthIndex = $this->readServerFile($server, 'modrinth.index.json');
|
||||
if ($modrinthIndex) {
|
||||
$data = json_decode($modrinthIndex, true);
|
||||
if (isset($data['formatVersion'])) {
|
||||
// Use the pack name as the Modrinth project slug for API lookups.
|
||||
if (is_array($data) && isset($data['formatVersion'])) {
|
||||
// Best-effort slug derivation from pack name for API lookups.
|
||||
// dependencies.minecraft is a MC version (e.g. "1.20.1"), NOT a project ID.
|
||||
$slug = isset($data['name']) ? strtolower(str_replace(' ', '-', $data['name'])) : null;
|
||||
// NOTE: This may not match the actual Modrinth slug if the project name
|
||||
// differs from its URL slug. Set MODPACK_ID egg variable for reliability.
|
||||
$slug = isset($data['name']) ? preg_replace('/[^a-z0-9-]/', '', strtolower(str_replace(' ', '-', $data['name']))) : null;
|
||||
return [
|
||||
'platform' => 'modrinth',
|
||||
'modpack_id' => $slug,
|
||||
@@ -225,7 +227,10 @@ class ModpackAPIController extends Controller
|
||||
public function getStatus(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json([], 401);
|
||||
}
|
||||
|
||||
// Get all server UUIDs the user has access to
|
||||
$serverUuids = $user->accessibleServers()->pluck('uuid')->toArray();
|
||||
|
||||
|
||||
@@ -32,11 +32,15 @@ namespace Pterodactyl\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary;
|
||||
use Exception;
|
||||
|
||||
class ModpackApiService
|
||||
{
|
||||
public function __construct(
|
||||
private BlueprintExtensionLibrary $blueprint
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetch the latest version info for a modpack from its platform API.
|
||||
*
|
||||
@@ -106,9 +110,7 @@ class ModpackApiService
|
||||
*/
|
||||
private function checkCurseForge(string $modpackId): array
|
||||
{
|
||||
$apiKey = DB::table('settings')
|
||||
->where('key', 'modpackchecker::curseforge_api_key')
|
||||
->value('value');
|
||||
$apiKey = $this->blueprint->dbGet('modpackchecker', 'curseforge_api_key');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
throw new Exception('CurseForge API key not configured');
|
||||
|
||||
@@ -30,10 +30,11 @@ const ModpackVersionCard: React.FC = () => {
|
||||
const response = await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/check`);
|
||||
setData(response.data);
|
||||
setStatus(response.data.success ? 'success' : 'error');
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 429) {
|
||||
} catch (error: unknown) {
|
||||
const err = error as { response?: { status?: number } };
|
||||
if (err.response?.status === 429) {
|
||||
setData({ success: false, error: 'rate_limited' });
|
||||
} else if (error.response?.status === 404) {
|
||||
} else if (err.response?.status === 404) {
|
||||
setData({ success: false, error: 'not_found' });
|
||||
} else {
|
||||
setData({ success: false, error: 'api_error' });
|
||||
|
||||
Reference in New Issue
Block a user