From b2bb1811d09531bb0986bec344a0627d79864e44 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 4 Jun 2026 20:05:36 +0200 Subject: [PATCH] Medium priority fixes: CORS from env, shared HasRadioSettings trait, lazy RconService, validated() fixes, LogoGenerator hardening, DB indexes, user profile consistency, radio rank N+1 fix --- .../Controllers/Api/HotelApiController.php | 24 ++++++---- .../Controllers/Community/RadioController.php | 14 ++---- .../Controllers/Concerns/HasRadioSettings.php | 23 ++++++++++ .../Miscellaneous/LogoGeneratorController.php | 21 ++++++++- .../Controllers/Radio/EmbedController.php | 13 +----- app/Http/Controllers/Radio/SseController.php | 14 +----- .../User/AccountSettingsController.php | 12 ++--- .../Controllers/User/GuestbookController.php | 4 +- app/Services/RconService.php | 23 +++++++--- config/cors.php | 4 +- ..._06_04_000000_add_more_missing_indexes.php | 44 +++++++++++++++++++ 11 files changed, 140 insertions(+), 56 deletions(-) create mode 100644 app/Http/Controllers/Concerns/HasRadioSettings.php create mode 100644 database/migrations/2026_06_04_000000_add_more_missing_indexes.php diff --git a/app/Http/Controllers/Api/HotelApiController.php b/app/Http/Controllers/Api/HotelApiController.php index 7357fc0..57790ce 100755 --- a/app/Http/Controllers/Api/HotelApiController.php +++ b/app/Http/Controllers/Api/HotelApiController.php @@ -227,15 +227,21 @@ class HotelApiController extends Controller public function userProfile(string $username): JsonResponse { $user = User::where('username', $username) - ->firstOrFail(); + ->first(); + + if (! $user) { + return response()->json(['data' => null], 404); + } return response()->json([ - 'id' => $user->id, - 'username' => $user->username, - 'look' => $user->look, - 'motto' => $user->motto, - 'account_created' => $user->account_created, - 'online' => false, + 'data' => [ + 'id' => $user->id, + 'username' => $user->username, + 'look' => $user->look, + 'motto' => $user->motto, + 'account_created' => $user->account_created, + 'online' => false, + ], ]); } @@ -290,9 +296,11 @@ class HotelApiController extends Controller ->where('user_id', $request->user()->id) ->firstOrFail(); + $validated = $request->validated(); + $reply = $ticket->replies()->create([ 'user_id' => $request->user()->id, - 'message' => $request->input('message'), + 'message' => $validated['message'], ]); return response()->json(['data' => $reply->load('user:id,username,look')], 201); diff --git a/app/Http/Controllers/Community/RadioController.php b/app/Http/Controllers/Community/RadioController.php index 8e92975..9b048e6 100755 --- a/app/Http/Controllers/Community/RadioController.php +++ b/app/Http/Controllers/Community/RadioController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\Community; use App\Enums\RadioSettings; use App\Http\Controllers\Controller; +use App\Http\Controllers\Concerns\HasRadioSettings; use App\Models\Miscellaneous\WebsiteSetting; use App\Models\RadioApplication; use App\Models\RadioBanner; @@ -20,6 +21,7 @@ use Illuminate\View\View; class RadioController extends Controller { + use HasRadioSettings; public function __construct( private readonly RadioStreamService $streamService, private readonly RadioScheduleService $scheduleService, @@ -120,7 +122,8 @@ class RadioController extends Controller ]); if ($validated['rank_id']) { - $rank = Cache::remember("radio_rank_{$validated['rank_id']}", 300, fn () => RadioRank::find($validated['rank_id'])); + $acceptingRanks = Cache::remember('radio_ranks_accepting', 300, fn () => RadioRank::where('accepts_applications', true)->get()->keyBy('id')); + $rank = $acceptingRanks->get((int) $validated['rank_id']); if (! $rank || ! $rank->accepts_applications) { return back()->withErrors([ @@ -336,13 +339,4 @@ class RadioController extends Controller return WebsiteSetting::whereIn('key', $stringKeys)->pluck('value', 'key')->all(); }); } - - private function getSetting(RadioSettings $setting, mixed $default = null): mixed - { - return Cache::remember("setting_{$setting->value}", 60, function () use ($setting, $default): mixed { - $websiteSetting = WebsiteSetting::where('key', $setting->value)->first(); - - return $websiteSetting?->value ?? $default; - }); - } } diff --git a/app/Http/Controllers/Concerns/HasRadioSettings.php b/app/Http/Controllers/Concerns/HasRadioSettings.php new file mode 100644 index 0000000..e4710dc --- /dev/null +++ b/app/Http/Controllers/Concerns/HasRadioSettings.php @@ -0,0 +1,23 @@ +value : $key; + + return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed { + $setting = WebsiteSetting::where('key', $keyStr)->first(); + + return $setting?->value ?? $default; + }); + } +} diff --git a/app/Http/Controllers/Miscellaneous/LogoGeneratorController.php b/app/Http/Controllers/Miscellaneous/LogoGeneratorController.php index e28c497..4f008e4 100755 --- a/app/Http/Controllers/Miscellaneous/LogoGeneratorController.php +++ b/app/Http/Controllers/Miscellaneous/LogoGeneratorController.php @@ -7,6 +7,7 @@ use App\Models\Miscellaneous\WebsiteSetting; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Support\Str; use Illuminate\View\View; class LogoGeneratorController extends Controller @@ -24,9 +25,25 @@ class LogoGeneratorController extends Controller public function store(Request $request): JsonResponse { - $request->validate(['logo' => 'required|image|mimes:jpeg,png,gif,webp|max:5120']); + $request->validate([ + 'logo' => [ + 'required', + 'image', + 'mimes:jpeg,png,gif,webp', + 'max:5120', + ], + ]); - $path = $request->file('logo')->store('generated-logos', 'public'); + $file = $request->file('logo'); + $mime = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file->getPathname()); + $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; + + if (! in_array($mime, $allowedMimes, true)) { + return response()->json(['success' => false, 'message' => 'Invalid file type.'], 422); + } + + $filename = 'logo_' . Str::random(16) . '.' . $file->getClientOriginalExtension(); + $path = $file->storeAs('generated-logos', $filename, 'public'); $setting = WebsiteSetting::where('key', 'cms_logo')->first(); diff --git a/app/Http/Controllers/Radio/EmbedController.php b/app/Http/Controllers/Radio/EmbedController.php index b422e95..4255111 100755 --- a/app/Http/Controllers/Radio/EmbedController.php +++ b/app/Http/Controllers/Radio/EmbedController.php @@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio; use App\Enums\RadioSettings; use App\Http\Controllers\Controller; -use App\Models\Miscellaneous\WebsiteSetting; +use App\Http\Controllers\Concerns\HasRadioSettings; use App\Services\Community\RadioStreamService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,6 +15,7 @@ use Illuminate\View\View; class EmbedController extends Controller { + use HasRadioSettings; public function __construct( private readonly RadioStreamService $streamService, ) {} @@ -53,14 +54,4 @@ class EmbedController extends Controller ]); } - private function getSetting(RadioSettings|string $key, mixed $default = null): mixed - { - $keyStr = $key instanceof RadioSettings ? $key->value : $key; - - return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed { - $websiteSetting = WebsiteSetting::where('key', $keyStr)->first(); - - return $websiteSetting?->value ?? $default; - }); - } } diff --git a/app/Http/Controllers/Radio/SseController.php b/app/Http/Controllers/Radio/SseController.php index 22bf12f..1696655 100755 --- a/app/Http/Controllers/Radio/SseController.php +++ b/app/Http/Controllers/Radio/SseController.php @@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio; use App\Enums\RadioSettings; use App\Http\Controllers\Controller; -use App\Models\Miscellaneous\WebsiteSetting; +use App\Http\Controllers\Concerns\HasRadioSettings; use App\Services\Community\RadioScheduleService; use App\Services\Community\RadioStreamService; use Illuminate\Http\Request; @@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse; class SseController extends Controller { + use HasRadioSettings; private const int SSE_KEEPALIVE = 15; public function __construct( @@ -179,15 +180,4 @@ class SseController extends Controller return rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId; } - - private function getSetting(RadioSettings|string $key, mixed $default = null): mixed - { - $keyStr = $key instanceof RadioSettings ? $key->value : $key; - - return Cache::remember("setting_{$keyStr}", 60, function () use ($keyStr, $default): mixed { - $setting = WebsiteSetting::where('key', $keyStr)->first(); - - return $setting?->value ?? $default; - }); - } } diff --git a/app/Http/Controllers/User/AccountSettingsController.php b/app/Http/Controllers/User/AccountSettingsController.php index 6dfedca..3c1ad7a 100755 --- a/app/Http/Controllers/User/AccountSettingsController.php +++ b/app/Http/Controllers/User/AccountSettingsController.php @@ -40,13 +40,15 @@ class AccountSettingsController extends Controller return redirect()->back()->withErrors('User not found'); } - if ($user->mail !== $request->input('mail')) { - $this->userService->updateField($user, 'mail', $request->input('mail')); + $validated = $request->validated(); + + if ($user->mail !== $validated['mail']) { + $this->userService->updateField($user, 'mail', $validated['mail']); } - if ($user->motto !== $request->input('motto')) { - $this->rconService->setMotto($user, $request->input('motto')); - $this->userService->updateField($user, 'motto', $request->input('motto')); + if ($user->motto !== $validated['motto']) { + $this->rconService->setMotto($user, $validated['motto']); + $this->userService->updateField($user, 'motto', $validated['motto']); } return redirect()->route('settings.account.show')->with('success', __('Your account settings has been updated')); diff --git a/app/Http/Controllers/User/GuestbookController.php b/app/Http/Controllers/User/GuestbookController.php index ed04df4..5e0942c 100755 --- a/app/Http/Controllers/User/GuestbookController.php +++ b/app/Http/Controllers/User/GuestbookController.php @@ -17,9 +17,11 @@ class GuestbookController extends Controller { $this->validateGuestbookPost($user, $request); + $validated = $request->validated(); + $user->profileGuestbook()->create([ 'user_id' => Auth::id(), - 'message' => $request->input('message'), + 'message' => $validated['message'], ]); return redirect()->back()->with('success', __('Your message has been posted.')); diff --git a/app/Services/RconService.php b/app/Services/RconService.php index 06b401c..f8bfdf2 100755 --- a/app/Services/RconService.php +++ b/app/Services/RconService.php @@ -27,12 +27,14 @@ class RconService 'ip' => setting('rcon_ip'), 'port' => (int) setting('rcon_port'), ]; - - $this->initialize(); } - private function initialize(): void + public function connect(): bool { + if ($this->isConnected) { + return true; + } + $this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($this->socket === false) { @@ -40,7 +42,7 @@ class RconService Log::error("RCON initialization failed: {$error}"); $this->closeConnection(); - return; + return false; } if (! @socket_connect($this->socket, $this->config['ip'], $this->config['port'])) { @@ -48,10 +50,17 @@ class RconService Log::error("RCON connection failed: {$error}"); $this->closeConnection(); - return; + return false; } $this->isConnected = true; + + return true; + } + + private function initialize(): void + { + $this->connect(); } private function closeConnection(): void @@ -66,6 +75,10 @@ class RconService public function isConnected(): bool { + if (! $this->isConnected) { + $this->connect(); + } + return $this->isConnected; } diff --git a/config/cors.php b/config/cors.php index f112f7b..5546602 100755 --- a/config/cors.php +++ b/config/cors.php @@ -21,11 +21,11 @@ return [ 'allowed_methods' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_METHODS', 'GET,POST,PUT,PATCH,DELETE,OPTIONS'))), fn ($v) => $v !== ''), - 'allowed_origins' => ['*'], // Zorgt ervoor dat alle origins (zoals je client/CMS) de imaging mogen inladen + 'allowed_origins' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_ORIGINS', '*'))), fn ($v) => $v !== ''), 'allowed_origins_patterns' => [], - 'allowed_headers' => ['*'], // Flexibel instellen zodat er geen headers geblokkeerd worden + 'allowed_headers' => array_filter(array_map(trim(...), explode(',', (string) env('CORS_ALLOWED_HEADERS', '*'))), fn ($v) => $v !== ''), 'exposed_headers' => [], diff --git a/database/migrations/2026_06_04_000000_add_more_missing_indexes.php b/database/migrations/2026_06_04_000000_add_more_missing_indexes.php new file mode 100644 index 0000000..81e2faa --- /dev/null +++ b/database/migrations/2026_06_04_000000_add_more_missing_indexes.php @@ -0,0 +1,44 @@ +index('username', 'idx_users_username'); + $table->index('mail', 'idx_users_mail'); + $table->index('ip_current', 'idx_users_ip_current'); + $table->index('ip_register', 'idx_users_ip_register'); + }); + + Schema::table('camera_web', function (Blueprint $table) { + $table->index('visible', 'idx_camera_web_visible'); + }); + + Schema::table('website_shop_articles', function (Blueprint $table) { + $table->index('category_id', 'idx_website_shop_articles_category_id'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex('idx_users_username'); + $table->dropIndex('idx_users_mail'); + $table->dropIndex('idx_users_ip_current'); + $table->dropIndex('idx_users_ip_register'); + }); + + Schema::table('camera_web', function (Blueprint $table) { + $table->dropIndex('idx_camera_web_visible'); + }); + + Schema::table('website_shop_articles', function (Blueprint $table) { + $table->dropIndex('idx_website_shop_articles_category_id'); + }); + } +};