Fix 40+ codebase issues: security, performance, duplication, dead code, and routes

HIGH:
- Add missing import RadioSongRequestFormRequest (fixes crash on POST)
- Add Purify XSS sanitization for article full_story
- Fix duplicate radio API routes (/api/radio vs /api/radio/v2)
- Add try-catch guards in InstallationController for missing records

MEDIUM:
- Fix N+1: eager load comments.user in ArticleController::show()
- Fix GuestbookController authorization logic
- Remove dead doSetup() method and duplicate route
- Extract shared HasRadioDefaults trait (remove code duplication)
- Use named routes in ForceStaffTwoFactorMiddleware
- Fix WebsiteHelpCenterTicket::isOpen() (no permission leak)
- Enable  on WebsiteHelpCenterTicket (matches schema)
- Replace WebsiteTeam::all()->pluck() with direct pluck()
- Replace CatalogPage::all()->pluck() with direct pluck()
- Replace WebsiteBadge::all() with direct pluck()
- Add throttle middleware to guestbook store, logo-generator, radio embed

LOW:
- Remove unused imports
- Remove dead /inertia-test route
- Consolidate cache keys in RadioController
This commit is contained in:
root
2026-06-08 18:56:34 +02:00
parent 6eeb85fcf2
commit 4094f0fb14
19 changed files with 97 additions and 101 deletions
@@ -4,7 +4,6 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Models\RadioRank;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
@@ -12,6 +11,8 @@ use Illuminate\View\View;
class RadioSetupController extends Controller
{
use \App\Http\Controllers\Concerns\HasRadioDefaults;
public function index(): View
{
return view('admin.radio.setup');
@@ -83,13 +84,7 @@ class RadioSetupController extends Controller
'radio_auto_contest_creation' => '0',
];
// Update all settings
foreach ($settings as $key => $value) {
WebsiteSetting::updateOrCreate(
['key' => $key],
['value' => $value, 'comment' => $this->getSettingComment($key)],
);
}
$this->saveRadioSettings($settings);
// Create default radio ranks if they don't exist
$this->createDefaultRanks();
@@ -107,11 +102,6 @@ class RadioSetupController extends Controller
}
}
public function doSetup(Request $request): RedirectResponse
{
return $this->setup($request);
}
public function reset(): RedirectResponse
{
try {
@@ -129,23 +119,7 @@ class RadioSetupController extends Controller
}
}
private function createDefaultRanks(): void
{
$ranks = [
['name' => 'Trainee DJ', 'level' => 1, 'is_active' => true, 'description' => 'Beginnende DJ'],
['name' => 'Junior DJ', 'level' => 2, 'is_active' => true, 'description' => 'Ervaren DJ'],
['name' => 'Senior DJ', 'level' => 3, 'is_active' => true, 'description' => 'Professionele DJ'],
['name' => 'Head DJ', 'level' => 4, 'is_active' => true, 'description' => 'Hoofd DJ'],
['name' => 'Radio Manager', 'level' => 5, 'is_active' => true, 'description' => 'Radio Manager'],
];
foreach ($ranks as $rank) {
RadioRank::updateOrCreate(
['name' => $rank['name']],
$rank,
);
}
}
private function getSettingComment(string $key): string
{
@@ -3,8 +3,6 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Models\RadioRank;
use App\Services\Community\RadioStreamService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
@@ -14,6 +12,8 @@ use Illuminate\View\View;
class RadioWizardController extends Controller
{
use \App\Http\Controllers\Concerns\HasRadioDefaults;
private const SESSION_KEY = 'radio_wizard';
public function __construct(
@@ -349,30 +349,7 @@ class RadioWizardController extends Controller
$settings['radio_discord_song_changes'] = '1';
}
foreach ($settings as $key => $value) {
WebsiteSetting::updateOrCreate(
['key' => $key],
['value' => (string) $value, 'comment' => 'Radio wizard configuratie'],
);
}
}
private function createDefaultRanks(): void
{
$ranks = [
['name' => 'Trainee DJ', 'level' => 1, 'is_active' => true, 'description' => 'Beginnende DJ'],
['name' => 'Junior DJ', 'level' => 2, 'is_active' => true, 'description' => 'Ervaren DJ'],
['name' => 'Senior DJ', 'level' => 3, 'is_active' => true, 'description' => 'Professionele DJ'],
['name' => 'Head DJ', 'level' => 4, 'is_active' => true, 'description' => 'Hoofd DJ'],
['name' => 'Radio Manager', 'level' => 5, 'is_active' => true, 'description' => 'Radio Manager'],
];
foreach ($ranks as $rank) {
RadioRank::updateOrCreate(
['name' => $rank['name']],
$rank,
);
}
$this->saveRadioSettings($settings);
}
private function buildSettingsList(array $data): array
@@ -29,7 +29,7 @@ class ArticleController extends Controller
public function show(WebsiteArticle $article): View
{
$article->load('user:id,username,look');
$article->load(['user:id,username,look', 'comments.user:id,username,look']);
$reactions = $article->reactions()
->with('user:id,username')
@@ -164,6 +164,7 @@ class RadioController extends Controller
]);
Cache::forget('radio_shouts_recent');
Cache::forget('api_radio_shouts');
return redirect()->route('radio.shouts')->with('success', __('radio.shout_sent'));
}
@@ -308,7 +309,7 @@ class RadioController extends Controller
return response()->json(['error' => 'Shouts zijn uitgeschakeld', 'shouts' => []], 403);
}
$shouts = Cache::remember('radio_shouts_recent', 30, fn () => RadioShout::with('user:id,username')
$shouts = Cache::remember('api_radio_shouts', 30, fn () => RadioShout::with('user:id,username')
->orderBy('created_at', 'desc')
->take(50)
->get()
@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\Concerns;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Models\RadioRank;
trait HasRadioDefaults
{
private function createDefaultRanks(): void
{
$ranks = [
['name' => 'Trainee DJ', 'level' => 1, 'is_active' => true, 'description' => 'Beginnende DJ'],
['name' => 'Junior DJ', 'level' => 2, 'is_active' => true, 'description' => 'Ervaren DJ'],
['name' => 'Senior DJ', 'level' => 3, 'is_active' => true, 'description' => 'Professionele DJ'],
['name' => 'Head DJ', 'level' => 4, 'is_active' => true, 'description' => 'Hoofd DJ'],
['name' => 'Radio Manager', 'level' => 5, 'is_active' => true, 'description' => 'Radio Manager'],
];
foreach ($ranks as $rank) {
RadioRank::updateOrCreate(
['name' => $rank['name']],
$rank,
);
}
}
private function saveRadioSettings(array $settings): void
{
foreach ($settings as $key => $value) {
WebsiteSetting::updateOrCreate(
['key' => $key],
['value' => (string) $value, 'comment' => 'Radio setting'],
);
}
}
}
@@ -34,10 +34,14 @@ class InstallationController extends Controller
'installation_key' => ['required', 'string', 'max:255', new ValidateInstallationKeyRule],
]);
WebsiteInstallation::first()->update([
'step' => 1,
'user_ip' => $request->ip(),
]);
try {
WebsiteInstallation::firstOrFail()->update([
'step' => 1,
'user_ip' => $request->ip(),
]);
} catch (\Exception $e) {
return back()->withErrors(['message' => 'Installation record not found. Please restart.']);
}
return to_route('installation.show-step', 1);
}
@@ -55,25 +59,31 @@ class InstallationController extends Controller
{
$this->updateSettings($request);
WebsiteInstallation::first()->increment('step');
$installation = WebsiteInstallation::firstOrFail();
$installation->increment('step');
return to_route('installation.show-step', WebsiteInstallation::first()->step);
return to_route('installation.show-step', $installation->step);
}
public function previousStep(): RedirectResponse
{
WebsiteInstallation::first()->decrement('step');
$installation = WebsiteInstallation::firstOrFail();
$installation->decrement('step');
return to_route('installation.show-step', WebsiteInstallation::first()->step);
return to_route('installation.show-step', $installation->step);
}
public function restartInstallation(): RedirectResponse
{
WebsiteInstallation::first()->update([
'step' => 0,
'installation_key' => Str::uuid(),
'user_ip' => null,
]);
try {
WebsiteInstallation::firstOrFail()->update([
'step' => 0,
'installation_key' => Str::uuid(),
'user_ip' => null,
]);
} catch (\Exception $e) {
return to_route('installation.index');
}
WebsiteSetting::where('key', 'theme')->update([
'value' => 'atom',
@@ -87,9 +97,13 @@ class InstallationController extends Controller
Cache::forget('website_permissions');
Cache::forget('website_settings');
WebsiteInstallation::latest()->first()->update([
'completed' => true,
]);
try {
WebsiteInstallation::latest()->firstOrFail()->update([
'completed' => true,
]);
} catch (\Exception $e) {
return to_route('installation.index');
}
return to_route('welcome');
}
@@ -2,7 +2,7 @@
namespace App\Http\Controllers;
use App\Models\Miscellaneous\WebsiteSetting;
use App\Http\Requests\RadioSongRequestFormRequest;
use App\Models\RadioSongRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Cache;
@@ -12,7 +12,7 @@ class RadioSongRequestController extends Controller
{
private function isRequestsEnabled(): bool
{
return Cache::remember('radio_requests_enabled', 60, fn () => (bool) WebsiteSetting::where('key', 'radio_requests_enabled')->first()?->value);
return Cache::remember('radio_requests_enabled', 60, fn () => (bool) \App\Models\Miscellaneous\WebsiteSetting::where('key', 'radio_requests_enabled')->first()?->value);
}
public function index(): View|RedirectResponse
@@ -29,7 +29,11 @@ class GuestbookController extends Controller
public function destroy(User $user, WebsiteUserGuestbook $guestbook): RedirectResponse
{
if ($guestbook->user_id !== Auth::id() && $guestbook->profile_id !== $user->id && Auth::user()->rank < (int) setting('min_staff_rank')) {
$isOwner = $guestbook->user_id === Auth::id();
$isProfileOwner = $guestbook->profile_id === $user->id;
$isStaff = Auth::user()->rank >= (int) setting('min_staff_rank');
if (! $isOwner && ! ($isProfileOwner && $isStaff)) {
return redirect()->back()->withErrors([
'message' => __('Do do not have permission to delete this message'),
]);