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

This commit is contained in:
root
2026-06-04 20:05:36 +02:00
parent 4b6872e5e0
commit b2bb1811d0
11 changed files with 140 additions and 56 deletions
@@ -227,15 +227,21 @@ class HotelApiController extends Controller
public function userProfile(string $username): JsonResponse public function userProfile(string $username): JsonResponse
{ {
$user = User::where('username', $username) $user = User::where('username', $username)
->firstOrFail(); ->first();
if (! $user) {
return response()->json(['data' => null], 404);
}
return response()->json([ return response()->json([
'data' => [
'id' => $user->id, 'id' => $user->id,
'username' => $user->username, 'username' => $user->username,
'look' => $user->look, 'look' => $user->look,
'motto' => $user->motto, 'motto' => $user->motto,
'account_created' => $user->account_created, 'account_created' => $user->account_created,
'online' => false, 'online' => false,
],
]); ]);
} }
@@ -290,9 +296,11 @@ class HotelApiController extends Controller
->where('user_id', $request->user()->id) ->where('user_id', $request->user()->id)
->firstOrFail(); ->firstOrFail();
$validated = $request->validated();
$reply = $ticket->replies()->create([ $reply = $ticket->replies()->create([
'user_id' => $request->user()->id, 'user_id' => $request->user()->id,
'message' => $request->input('message'), 'message' => $validated['message'],
]); ]);
return response()->json(['data' => $reply->load('user:id,username,look')], 201); return response()->json(['data' => $reply->load('user:id,username,look')], 201);
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Community;
use App\Enums\RadioSettings; use App\Enums\RadioSettings;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Controllers\Concerns\HasRadioSettings;
use App\Models\Miscellaneous\WebsiteSetting; use App\Models\Miscellaneous\WebsiteSetting;
use App\Models\RadioApplication; use App\Models\RadioApplication;
use App\Models\RadioBanner; use App\Models\RadioBanner;
@@ -20,6 +21,7 @@ use Illuminate\View\View;
class RadioController extends Controller class RadioController extends Controller
{ {
use HasRadioSettings;
public function __construct( public function __construct(
private readonly RadioStreamService $streamService, private readonly RadioStreamService $streamService,
private readonly RadioScheduleService $scheduleService, private readonly RadioScheduleService $scheduleService,
@@ -120,7 +122,8 @@ class RadioController extends Controller
]); ]);
if ($validated['rank_id']) { 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) { if (! $rank || ! $rank->accepts_applications) {
return back()->withErrors([ return back()->withErrors([
@@ -336,13 +339,4 @@ class RadioController extends Controller
return WebsiteSetting::whereIn('key', $stringKeys)->pluck('value', 'key')->all(); 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;
});
}
} }
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Concerns;
use App\Enums\RadioSettings;
use App\Models\Miscellaneous\WebsiteSetting;
use Illuminate\Support\Facades\Cache;
trait HasRadioSettings
{
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;
});
}
}
@@ -7,6 +7,7 @@ use App\Models\Miscellaneous\WebsiteSetting;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View; use Illuminate\View\View;
class LogoGeneratorController extends Controller class LogoGeneratorController extends Controller
@@ -24,9 +25,25 @@ class LogoGeneratorController extends Controller
public function store(Request $request): JsonResponse 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(); $setting = WebsiteSetting::where('key', 'cms_logo')->first();
+2 -11
View File
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio;
use App\Enums\RadioSettings; use App\Enums\RadioSettings;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Miscellaneous\WebsiteSetting; use App\Http\Controllers\Concerns\HasRadioSettings;
use App\Services\Community\RadioStreamService; use App\Services\Community\RadioStreamService;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -15,6 +15,7 @@ use Illuminate\View\View;
class EmbedController extends Controller class EmbedController extends Controller
{ {
use HasRadioSettings;
public function __construct( public function __construct(
private readonly RadioStreamService $streamService, 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;
});
}
} }
+2 -12
View File
@@ -6,7 +6,7 @@ namespace App\Http\Controllers\Radio;
use App\Enums\RadioSettings; use App\Enums\RadioSettings;
use App\Http\Controllers\Controller; 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\RadioScheduleService;
use App\Services\Community\RadioStreamService; use App\Services\Community\RadioStreamService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -15,6 +15,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
class SseController extends Controller class SseController extends Controller
{ {
use HasRadioSettings;
private const int SSE_KEEPALIVE = 15; private const int SSE_KEEPALIVE = 15;
public function __construct( public function __construct(
@@ -179,15 +180,4 @@ class SseController extends Controller
return rtrim($baseUrl, '/') . '/api/nowplaying/' . $stationId; 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;
});
}
} }
@@ -40,13 +40,15 @@ class AccountSettingsController extends Controller
return redirect()->back()->withErrors('User not found'); return redirect()->back()->withErrors('User not found');
} }
if ($user->mail !== $request->input('mail')) { $validated = $request->validated();
$this->userService->updateField($user, 'mail', $request->input('mail'));
if ($user->mail !== $validated['mail']) {
$this->userService->updateField($user, 'mail', $validated['mail']);
} }
if ($user->motto !== $request->input('motto')) { if ($user->motto !== $validated['motto']) {
$this->rconService->setMotto($user, $request->input('motto')); $this->rconService->setMotto($user, $validated['motto']);
$this->userService->updateField($user, 'motto', $request->input('motto')); $this->userService->updateField($user, 'motto', $validated['motto']);
} }
return redirect()->route('settings.account.show')->with('success', __('Your account settings has been updated')); return redirect()->route('settings.account.show')->with('success', __('Your account settings has been updated'));
@@ -17,9 +17,11 @@ class GuestbookController extends Controller
{ {
$this->validateGuestbookPost($user, $request); $this->validateGuestbookPost($user, $request);
$validated = $request->validated();
$user->profileGuestbook()->create([ $user->profileGuestbook()->create([
'user_id' => Auth::id(), 'user_id' => Auth::id(),
'message' => $request->input('message'), 'message' => $validated['message'],
]); ]);
return redirect()->back()->with('success', __('Your message has been posted.')); return redirect()->back()->with('success', __('Your message has been posted.'));
+18 -5
View File
@@ -27,12 +27,14 @@ class RconService
'ip' => setting('rcon_ip'), 'ip' => setting('rcon_ip'),
'port' => (int) setting('rcon_port'), '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); $this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($this->socket === false) { if ($this->socket === false) {
@@ -40,7 +42,7 @@ class RconService
Log::error("RCON initialization failed: {$error}"); Log::error("RCON initialization failed: {$error}");
$this->closeConnection(); $this->closeConnection();
return; return false;
} }
if (! @socket_connect($this->socket, $this->config['ip'], $this->config['port'])) { if (! @socket_connect($this->socket, $this->config['ip'], $this->config['port'])) {
@@ -48,10 +50,17 @@ class RconService
Log::error("RCON connection failed: {$error}"); Log::error("RCON connection failed: {$error}");
$this->closeConnection(); $this->closeConnection();
return; return false;
} }
$this->isConnected = true; $this->isConnected = true;
return true;
}
private function initialize(): void
{
$this->connect();
} }
private function closeConnection(): void private function closeConnection(): void
@@ -66,6 +75,10 @@ class RconService
public function isConnected(): bool public function isConnected(): bool
{ {
if (! $this->isConnected) {
$this->connect();
}
return $this->isConnected; return $this->isConnected;
} }
+2 -2
View File
@@ -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_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_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' => [], 'exposed_headers' => [],
@@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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');
});
}
};