diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 0b08785..e9dd3bc 100755 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -43,10 +43,6 @@ class Handler extends ExceptionHandler $this->reportable(function (Throwable $e) { $this->handleExceptionAlert($e); }); - - $this->renderable(function (Throwable $e) { - $this->handleExceptionAlert($e); - }); } private function handleExceptionAlert(Throwable $e): void diff --git a/app/Filament/Pages/Monitoring/Commandocentrum.php b/app/Filament/Pages/Monitoring/Commandocentrum.php index f9474b8..9389c78 100644 --- a/app/Filament/Pages/Monitoring/Commandocentrum.php +++ b/app/Filament/Pages/Monitoring/Commandocentrum.php @@ -104,15 +104,15 @@ final class Commandocentrum extends Page implements HasForms 'emulator_database_username' => $this->getSetting('emulator_database_username', ''), 'emulator_database_password' => $this->getSetting('emulator_database_password', ''), 'emulator_version' => $this->getSetting('emulator_version', 'Onbekend'), - 'nitro_emulator_path' => $this->getSetting('nitro_emulator_path', '/var/www/emulator'), + 'nitro_emulator_path' => $this->getSetting('nitro_emulator_path', $this->emulatorPath()), 'nitro_emulator_service' => $this->getSetting('nitro_emulator_service', 'emulator'), 'nitro_db_name' => $this->getSetting('nitro_db_name', 'habbo'), - 'nitro_sql_dir' => $this->getSetting('nitro_sql_dir', '/var/www/emulator/Database Updates'), - 'nitro_backup_dir' => $this->getSetting('nitro_backup_dir', '/var/www/emulator/Database Updates/backups'), - 'nitro_gamedata_dir' => $this->getSetting('nitro_gamedata_dir', '/var/www/Gamedata/config'), - 'nitro_client_dir' => $this->getSetting('nitro_client_dir', '/var/www/Nitro-V3/public/configuration'), - 'nitro_client_src' => $this->getSetting('nitro_client_src', '/var/www/Nitro-V3'), - 'nitro_renderer_src' => $this->getSetting('nitro_renderer_src', '/var/www/Nitro_Render_V3'), + 'nitro_sql_dir' => $this->getSetting('nitro_sql_dir', $this->emulatorPath('Database Updates')), + 'nitro_backup_dir' => $this->getSetting('nitro_backup_dir', $this->emulatorPath('Database Updates/backups')), + 'nitro_gamedata_dir' => $this->getSetting('nitro_gamedata_dir', $this->gamedataPath()), + 'nitro_client_dir' => $this->getSetting('nitro_client_dir', $this->nitroV3Path('public/configuration')), + 'nitro_client_src' => $this->getSetting('nitro_client_src', $this->nitroV3Path()), + 'nitro_renderer_src' => $this->getSetting('nitro_renderer_src', $this->nitroRendererV3Path()), 'hotel_alert_message' => '', ]; } @@ -350,9 +350,9 @@ final class Commandocentrum extends Page implements HasForms $serviceStatus = $this->runCommand('systemctl is-active ' . escapeshellarg($serviceName) . ' 2>/dev/null') ?: 'inactive'; $serviceColor = $serviceStatus === 'active' ? '#22c55e' : '#ef4444'; - $nitroClientPath = $this->getSetting('nitro_client_path', '/var/www/nitro-client'); - $nitroRendererPath = $this->getSetting('nitro_renderer_path', '/var/www/nitro-renderer'); - $nitroWebroot = $this->getSetting('nitro_webroot', '/var/www/Client'); + $nitroClientPath = $this->getSetting('nitro_client_path', $this->nitroClientPath()); + $nitroRendererPath = $this->getSetting('nitro_renderer_path', $this->nitroRendererPath()); + $nitroWebroot = $this->getSetting('nitro_webroot', $this->clientWebrootPath()); $clientCommit = $this->getGitCommit($nitroClientPath); $rendererCommit = $this->getGitCommit($nitroRendererPath); @@ -685,7 +685,7 @@ final class Commandocentrum extends Page implements HasForms $settings->set('emulator_jar_direct_url', $this->data['emulator_jar_direct_url'] ?? ''); $settings->set('emulator_jar_path', $this->data['emulator_jar_path'] ?? '/root/emulator'); $settings->set('emulator_source_repo', $this->data['emulator_source_repo'] ?? ''); - $settings->set('emulator_source_path', $this->data['emulator_source_path'] ?? '/var/www/emulator-source'); + $settings->set('emulator_source_path', $this->data['emulator_source_path'] ?? $this->emulatorSourcePath()); $settings->set('emulator_github_branch', $this->data['emulator_github_branch'] ?? 'main'); $settings->set('emulator_database_host', $this->data['emulator_database_host'] ?? '127.0.0.1'); $settings->set('emulator_database_name', $this->data['emulator_database_name'] ?? ''); @@ -885,4 +885,60 @@ final class Commandocentrum extends Page implements HasForms { return $this->runCommand('cat ' . escapeshellarg($path) . ' 2>/dev/null'); } + + private function emulatorPath(string $path = ''): string + { + $base = rtrim(env('NITRO_EMULATOR_PATH', '/var/www/emulator'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } + + private function nitroClientPath(string $path = ''): string + { + $base = rtrim(env('NITRO_CLIENT_DIR', '/var/www/nitro-client'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } + + private function gamedataPath(string $path = ''): string + { + $base = rtrim(env('NITRO_GAMEDATA_DIR', '/var/www/Gamedata/config'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } + + private function nitroV3Path(string $path = ''): string + { + $base = rtrim(env('NITRO_V3_DIR', '/var/www/Nitro-V3'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } + + private function nitroRendererV3Path(string $path = ''): string + { + $base = rtrim(env('NITRO_RENDERER_V3_DIR', '/var/www/Nitro_Render_V3'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } + + private function nitroRendererPath(string $path = ''): string + { + $base = rtrim(env('NITRO_RENDERER_DIR', '/var/www/nitro-renderer'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } + + private function clientWebrootPath(string $path = ''): string + { + $base = rtrim(env('NITRO_CLIENT_WEBROOT', '/var/www/Client'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } + + private function emulatorSourcePath(string $path = ''): string + { + $base = rtrim(env('NITRO_EMULATOR_SOURCE_DIR', '/var/www/emulator-source'), '/'); + + return $path !== '' ? $base . '/' . ltrim($path, '/') : $base; + } } diff --git a/app/Filament/Pages/Radio/RadioSettings.php b/app/Filament/Pages/Radio/RadioSettings.php index 59643c9..70af870 100755 --- a/app/Filament/Pages/Radio/RadioSettings.php +++ b/app/Filament/Pages/Radio/RadioSettings.php @@ -1175,9 +1175,12 @@ final class RadioSettings extends Page implements HasForms ); } - Cache::flush(); + Cache::forget('radio_settings_*'); + Cache::forget('radio_ranks'); + Cache::forget('radio_stream_status'); + Cache::forget('radio_nowplaying'); + Cache::forget('radio_listeners'); Artisan::call('config:clear'); - Artisan::call('cache:clear'); Notification::make() ->success() @@ -1264,9 +1267,12 @@ final class RadioSettings extends Page implements HasForms ); } - Cache::flush(); + Cache::forget('radio_settings_*'); + Cache::forget('radio_ranks'); + Cache::forget('radio_stream_status'); + Cache::forget('radio_nowplaying'); + Cache::forget('radio_listeners'); Artisan::call('config:clear'); - Artisan::call('cache:clear'); Notification::make() ->success() @@ -1349,9 +1355,12 @@ final class RadioSettings extends Page implements HasForms ); } - Cache::flush(); + Cache::forget('radio_settings_*'); + Cache::forget('radio_ranks'); + Cache::forget('radio_stream_status'); + Cache::forget('radio_nowplaying'); + Cache::forget('radio_listeners'); Artisan::call('config:clear'); - Artisan::call('cache:clear'); Notification::make() ->success() @@ -1557,6 +1566,10 @@ final class RadioSettings extends Page implements HasForms private function clearCache(): void { Cache::forget(self::CACHE_KEY); + Cache::forget('radio_ranks'); + Cache::forget('radio_stream_status'); + Cache::forget('radio_nowplaying'); + Cache::forget('radio_listeners'); } private function getSettingBool(string $key): bool diff --git a/app/Http/Controllers/Api/AuthController.php b/app/Http/Controllers/Api/AuthController.php index 396ba68..545a99c 100755 --- a/app/Http/Controllers/Api/AuthController.php +++ b/app/Http/Controllers/Api/AuthController.php @@ -124,6 +124,10 @@ class AuthController extends Controller { $article = WebsiteArticle::where('slug', $slug)->firstOrFail(); + $request->validate([ + 'comment' => ['required', 'string', 'max:5000'], + ]); + $comment = $article->comments()->create([ 'user_id' => $request->user()->id, 'comment' => strip_tags((string) $request->input('comment')), diff --git a/app/Http/Controllers/Community/RadioLeaderboardController.php b/app/Http/Controllers/Community/RadioLeaderboardController.php index e7eb18e..ae85ec0 100755 --- a/app/Http/Controllers/Community/RadioLeaderboardController.php +++ b/app/Http/Controllers/Community/RadioLeaderboardController.php @@ -23,14 +23,7 @@ class RadioLeaderboardController extends Controller default => $this->getAllTimeLeaderboard(), }; - // Extract top 3 from already-fetched data - $topUserData = array_slice($users, 0, 3); - $topUserIds = array_column($topUserData, 'id'); - $topUsers = User::whereIn('id', $topUserIds) - ->get() - ->keyBy('id'); - - return view('community.radio.leaderboard', ['users' => $users, 'topUsers' => $topUsers, 'period' => $period]); + return view('community.radio.leaderboard', ['users' => $users, 'period' => $period]); } private function getAllTimeLeaderboard(): array @@ -43,7 +36,7 @@ class RadioLeaderboardController extends Controller 'rank' => $index + 1, 'id' => $user->id, 'username' => $user->username, - 'avatar' => $user->avatar ?? null, + 'avatar' => $user->avatar, 'points' => $user->radio_points, ]) ->toArray(); @@ -79,7 +72,7 @@ class RadioLeaderboardController extends Controller 'rank' => $index + 1, 'id' => $item->user_id, 'username' => $user?->username ?? 'Onbekend', - 'avatar' => $user?->avatar ?? null, + 'avatar' => $user?->avatar, 'points' => (int) $item->total_points, ]; })->toArray(); diff --git a/app/Http/Controllers/Help/TicketController.php b/app/Http/Controllers/Help/TicketController.php index 459ee9c..85d5aa5 100755 --- a/app/Http/Controllers/Help/TicketController.php +++ b/app/Http/Controllers/Help/TicketController.php @@ -9,6 +9,7 @@ use App\Models\Help\WebsiteHelpCenterTicket; use App\Models\Help\WebsiteHelpCenterTicketReply; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\View\View; class TicketController extends Controller @@ -26,10 +27,15 @@ class TicketController extends Controller ]); } + private function getCachedCategories(): \Illuminate\Database\Eloquent\Collection + { + return Cache::remember('help_categories', 3600, fn () => WebsiteHelpCenterCategory::get()); + } + public function create(): View { return view('help-center.tickets.create', [ - 'categories' => WebsiteHelpCenterCategory::get(), + 'categories' => $this->getCachedCategories(), 'openTickets' => WebsiteHelpCenterTicket::where('open', true)->where('user_id', Auth::id())->get(), ]); } @@ -38,6 +44,8 @@ class TicketController extends Controller { Auth::user()->tickets()->create($request->validated()); + Cache::forget('help_categories'); + return redirect()->back()->with('success', __('Ticket submitted!')); } @@ -57,7 +65,7 @@ class TicketController extends Controller return view('help-center.tickets.edit', [ 'ticket' => $ticket, - 'categories' => WebsiteHelpCenterCategory::get(), + 'categories' => $this->getCachedCategories(), 'openTickets' => WebsiteHelpCenterTicket::where('open', true)->where('id', '!=', $ticket->id)->where('user_id', Auth::id())->get(), ]); } @@ -72,6 +80,8 @@ class TicketController extends Controller $ticket->update($request->validated()); + Cache::forget('help_categories'); + return to_route('help-center.ticket.show', $ticket)->with('success', __('Ticket updated!')); } diff --git a/app/Models/User.php b/app/Models/User.php index c5de64e..3b45aba 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,6 +9,7 @@ use App\Models\Articles\WebsiteArticleComment; use App\Models\Community\Staff\WebsiteStaffApplications; use App\Models\Community\Staff\WebsiteTeam; use App\Models\Game\Furniture\Item; +use App\Models\Game\Guild\Guild; use App\Models\Game\Guild\GuildMember; use App\Models\Game\Permission; use App\Models\Game\Player\MessengerFriendship; @@ -35,6 +36,7 @@ use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -294,7 +296,12 @@ class User extends Authenticatable implements FilamentUser, HasName return $this->hasMany(CameraWeb::class); } - public function guilds(): HasMany + public function guilds(): HasManyThrough + { + return $this->hasManyThrough(Guild::class, GuildMember::class, 'user_id', 'id', 'id', 'guild_id'); + } + + public function guildMemberships(): HasMany { return $this->hasMany(GuildMember::class, 'user_id'); } @@ -377,6 +384,16 @@ class User extends Authenticatable implements FilamentUser, HasName $this->save(); } + public function getAvatarAttribute(): ?string + { + $imager = setting('avatar_imager'); + if ($imager && $this->look) { + return $imager . $this->look . '&direction=2&head_direction=3&gesture=sml&action=wav'; + } + + return null; + } + public function getFilamentName(): string { return $this->username ?? 'Guest'; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index e46e081..6df0c08 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -64,10 +64,10 @@ class AppServiceProvider extends ServiceProvider }); $settingsService = app(SettingsService::class); - $badgePath = $settingsService->getOrDefault('badge_path_filesystem', '/var/www/gamedata/c_images/album1584'); + $badgePath = $settingsService->getOrDefault('badge_path_filesystem', dirname(env('NITRO_GAMEDATA_DIR', '/var/www/Gamedata/config')) . '/c_images/album1584'); Config::set('filesystems.disks.badges.root', $badgePath); - $adsPath = $settingsService->getOrDefault('ads_path_filesystem', '/var/www/gamedata/custom'); + $adsPath = $settingsService->getOrDefault('ads_path_filesystem', dirname(env('NITRO_GAMEDATA_DIR', '/var/www/Gamedata/config')) . '/custom'); Config::set('filesystems.disks.ads.root', $adsPath); WebsiteDrawBadge::observe(WebsiteDrawBadgeObserver::class); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 7777d80..977780a 100755 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -62,12 +62,15 @@ class RouteServiceProvider extends ServiceProvider return Limit::perMinute($maxAttempts)->by($key); }); - // Strict rate limit for login attempts (security) - increased for usability - RateLimiter::for('login', fn (Request $request) => Limit::perMinute(20)->by($request->ip())); + // Strict rate limit for registration (prevent spam) + RateLimiter::for('register', fn (Request $request) => Limit::perHour(3)->by($request->ip())); // Two-factor authentication rate limit RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(15)->by($request->ip())); + // Strict photo upload rate limit + RateLimiter::for('upload', fn (Request $request) => Limit::perMinute(10)->by($request->user()?->id ?? $request->ip())); + // Rate limit for radio endpoints (high traffic) RateLimiter::for('radio', function (Request $request) { $key = $request->get('radio_api_key_id') @@ -76,5 +79,8 @@ class RouteServiceProvider extends ServiceProvider return Limit::perMinute(120)->by((string) $key); }); + + // Rate limit for SSE (long-lived connections - low rate) + RateLimiter::for('sse', fn (Request $request) => Limit::perMinute(6)->by($request->ip())); } } diff --git a/app/Services/AutoDetectService.php b/app/Services/AutoDetectService.php index 61da8ba..2ca9064 100755 --- a/app/Services/AutoDetectService.php +++ b/app/Services/AutoDetectService.php @@ -69,7 +69,7 @@ class AutoDetectService return $found; } - return '/var/www/emulator-source'; + return $this->emulatorPath('../emulator-source'); } public function detectEmulatorServiceName(): string @@ -214,9 +214,9 @@ class AutoDetectService } $possiblePaths = [ - '/var/www/Client', '/var/www/client', '/var/www/Nitro', '/var/www/nitro', - '/var/www/html/Client', '/var/www/html/client', - '/var/www/atomcms/public/Client', '/var/www/atomcms/public/nitro', + $this->clientPath('../Client'), $this->clientPath('../client'), $this->clientPath('../Nitro'), $this->clientPath('../nitro'), + $this->clientPath('../../html/Client'), $this->clientPath('../../html/client'), + $this->clientPath('../../../atomcms/public/Client'), $this->clientPath('../../../atomcms/public/nitro'), ]; foreach ($possiblePaths as $path) { @@ -225,7 +225,7 @@ class AutoDetectService } } - return '/var/www/Client'; + return $this->clientPath('../Client'); } public function detectNitroBuildPath(): string @@ -247,8 +247,8 @@ class AutoDetectService } $possiblePaths = [ - '/var/www/nitro-client/dist', '/var/www/nitro-V3/dist', - '/var/www/atomcms/nitro-client/dist', + $this->clientPath('dist'), $this->clientPath('../nitro-V3/dist'), + base_path('nitro-client/dist'), ]; foreach ($possiblePaths as $path) { @@ -290,7 +290,7 @@ class AutoDetectService // Last resort: check common paths $possiblePaths = [ - '/var/www/atomcms/public/gamedata', '/var/www/html/gamedata', + $this->gamedataPath('../../../atomcms/public/gamedata'), $this->gamedataPath('../../html/gamedata'), '/opt/gamedata', '/root/gamedata', ]; @@ -300,7 +300,7 @@ class AutoDetectService } } - return '/var/www/gamedata'; + return $this->gamedataPath('../gamedata'); } public function detectFurnitureIconsPath(): string @@ -317,7 +317,7 @@ class AutoDetectService return $iconsPath; } - return '/var/www/Gamedata/icons'; + return $this->gamedataPath('../icons'); } public function detectCatalogIconsPath(): string @@ -334,7 +334,7 @@ class AutoDetectService return $cataloguePath; } - return '/var/www/Gamedata/catalogue'; + return $this->gamedataPath('../catalogue'); } // ─── Nginx ───────────────────────────────────────────────── @@ -700,6 +700,39 @@ class AutoDetectService return trim($result->output()) === 'yes'; } + private function emulatorPath(string $path = ''): string + { + $base = env('NITRO_EMULATOR_PATH', '/var/www/emulator'); + + if ($path === '') { + return $base; + } + + return rtrim($base, '/') . '/' . ltrim($path, '/'); + } + + private function clientPath(string $path = ''): string + { + $base = env('NITRO_CLIENT_DIR', '/var/www/nitro-client'); + + if ($path === '') { + return $base; + } + + return rtrim($base, '/') . '/' . ltrim($path, '/'); + } + + private function gamedataPath(string $path = ''): string + { + $base = env('NITRO_GAMEDATA_DIR', '/var/www/Gamedata/config'); + + if ($path === '') { + return $base; + } + + return rtrim($base, '/') . '/' . ltrim($path, '/'); + } + private function getSetting(string $key): ?string { try { diff --git a/app/Services/CatalogService.php b/app/Services/CatalogService.php index 193e7ff..c3a9ec7 100755 --- a/app/Services/CatalogService.php +++ b/app/Services/CatalogService.php @@ -60,6 +60,50 @@ class CatalogService ], ]; + private ?array $furnitureDataCache = null; + + private function gamedataPath(string $path = ''): string + { + $base = env('NITRO_GAMEDATA_DIR', '/var/www/Gamedata/config'); + if ($path === '') { + return $base; + } + return $base . '/' . ltrim($path, '/'); + } + + private function storagePath(string $path = ''): string + { + if ($path === '') { + return storage_path(); + } + return storage_path($path); + } + + private function loadFurnitureData(): array + { + if ($this->furnitureDataCache !== null) { + return $this->furnitureDataCache; + } + + $path = $this->gamedataPath('FurnitureData.json'); + if (! file_exists($path)) { + $path = $this->gamedataPath('FurnitureData.json'); + } + if (! file_exists($path)) { + $path = base_path('public/gamedata/config/FurnitureData.json'); + } + + $data = json_decode(file_get_contents($path), true); + $this->furnitureDataCache = $data['roomitemtypes']['furnitype'] ?? []; + + return $this->furnitureDataCache; + } + + public function getFurnitureData(): array + { + return $this->loadFurnitureData(); + } + public function createCatalogPages(): array { $created = 0; @@ -157,21 +201,6 @@ class CatalogService return ['pages_created' => $created]; } - public function getFurnitureData(): array - { - $path = base_path('../Gamedata/config/FurnitureData.json'); - if (! file_exists($path)) { - $path = '/var/www/Gamedata/config/FurnitureData.json'; - } - if (! file_exists($path)) { - $path = base_path('public/gamedata/config/FurnitureData.json'); - } - - $data = json_decode(file_get_contents($path), true); - - return $data['roomitemtypes']['furnitype'] ?? []; - } - private function createMainPage(string $name, int $order): int { $exists = DB::table('catalog_pages')->where('caption', $name)->first(); @@ -362,7 +391,7 @@ class CatalogService // 1. Download FurnitureData.json $furnitureUrl = $baseUrl . '/gamedata/config/FurnitureData.json'; - $localPath = '/var/www/Gamedata/config/FurnitureData.json'; + $localPath = $this->gamedataPath('FurnitureData.json'); try { $content = @file_get_contents($furnitureUrl); @@ -384,7 +413,7 @@ class CatalogService try { $content = @file_get_contents($productUrl); if ($content && json_decode($content, true)) { - file_put_contents('/var/www/Gamedata/config/ProductData.json', $content); + file_put_contents($this->gamedataPath('ProductData.json'), $content); $data = json_decode($content, true); $results['product'] = count($data['productdata'] ?? []); Log::info('[CatalogService] Downloaded ProductData.json'); @@ -407,7 +436,7 @@ class CatalogService private function downloadIconsFromUrl(string $baseUrl): int { $downloaded = 0; - $iconsPath = '/var/www/Gamedata/icons'; + $iconsPath = $this->gamedataPath('../icons'); $url = $baseUrl . '/gamedata/icons'; // Try to list files via HTTP (may not work) @@ -456,7 +485,7 @@ class CatalogService private function downloadCatalogImagesFromUrl(string $baseUrl): int { $downloaded = 0; - $catalogPath = '/var/www/Gamedata/catalogue'; + $catalogPath = $this->gamedataPath('../catalogue'); $url = $baseUrl . '/gamedata/catalogue'; // Download common catalogue images @@ -521,7 +550,7 @@ class CatalogService } // Save to file - $sqlPath = '/var/www/atomcms/storage/logs/catalog_update_' . date('Ymd_His') . '.sql'; + $sqlPath = $this->storagePath('logs/catalog_update_' . date('Ymd_His') . '.sql'); file_put_contents($sqlPath, $sql); return [ @@ -711,7 +740,7 @@ class CatalogService // Step 1: Download FurnitureData.json $furnitureUrl = rtrim($baseUrl, '/') . '/gamedata/config/FurnitureData.json'; - $localFurniturePath = '/var/www/Gamedata/config/FurnitureData.json'; + $localFurniturePath = $this->gamedataPath('FurnitureData.json'); $furnitureContent = @file_get_contents($furnitureUrl); if ($furnitureContent && $furnitureData = json_decode($furnitureContent, true)) { @@ -730,7 +759,7 @@ class CatalogService $productUrl = rtrim($baseUrl, '/') . '/gamedata/config/ProductData.json'; $productContent = @file_get_contents($productUrl); if ($productContent && json_decode($productContent, true)) { - file_put_contents('/var/www/Gamedata/config/ProductData.json', $productContent); + file_put_contents($this->gamedataPath('ProductData.json'), $productContent); $results['product'] = 1; } @@ -877,7 +906,7 @@ class CatalogService private function syncIcons(array $furnitureData, string $baseUrl): int { $downloaded = 0; - $iconsPath = '/var/www/Gamedata/icons'; + $iconsPath = $this->gamedataPath('../icons'); $iconsUrl = rtrim($baseUrl, '/') . '/gamedata/icons'; $iconNames = []; @@ -910,7 +939,7 @@ class CatalogService private function syncCatalogImages(string $baseUrl): int { $downloaded = 0; - $catalogPath = '/var/www/Gamedata/catalogue'; + $catalogPath = $this->gamedataPath('../catalogue'); $catalogUrl = rtrim($baseUrl, '/') . '/gamedata/catalogue'; $commonImages = [ @@ -951,7 +980,7 @@ class CatalogService { Log::info('[CatalogService] Starting catalog_clothing sync'); - $figureMapPath = '/var/www/Gamedata/config/FigureMap.json'; + $figureMapPath = $this->gamedataPath('FigureMap.json'); if (! file_exists($figureMapPath)) { throw new \Exception('FigureMap.json niet gevonden'); @@ -1100,4 +1129,20 @@ class CatalogService 'total' => DB::table('catalog_clothing')->count(), ]; } + + private function estimatePrice(string $category): int + { + return match ($category) { + 'chair', 'table', 'shelf', 'bed', 'rug', 'floor', 'lighting', + 'wall_decoration', 'divider', 'window' => 15, + 'teleport', 'gate', 'roller' => 25, + 'wired', 'wired_effect', 'wired_condition', 'wired_trigger', 'wired_add_on' => 10, + 'music', 'sound_fx' => 20, + 'vending_machine', 'games', 'fortuna' => 30, + 'pets' => 50, + 'food' => 5, + 'trophy', 'present', 'credit', 'tent', 'extras', 'leaderboards' => 10, + default => 10, + }; + } } diff --git a/app/Services/PointsService.php b/app/Services/PointsService.php index 8cb9931..e97a18e 100755 --- a/app/Services/PointsService.php +++ b/app/Services/PointsService.php @@ -92,7 +92,7 @@ readonly class PointsService $collection = User::where('radio_points', '>', 0) ->orderBy('radio_points', 'desc') ->limit($limit) - ->get(['id', 'username', 'avatar', 'radio_points']); + ->get(['id', 'username', 'look', 'radio_points']); return $collection->map(fn (User $user, int $index): array => [ 'rank' => $index + 1, diff --git a/app/Services/TraxService.php b/app/Services/TraxService.php index dc005ac..46d75f0 100755 --- a/app/Services/TraxService.php +++ b/app/Services/TraxService.php @@ -3,18 +3,33 @@ namespace App\Services; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class TraxService { + private function gamedataPath(string $path = ''): string + { + return rtrim(env('NITRO_GAMEDATA_DIR', '/var/www/Gamedata/config'), '/') . '/' . ltrim($path, '/'); + } + + private function soundsPath(string $path = ''): string + { + return rtrim(dirname($this->gamedataPath()), '/') . '/sounds/' . ltrim($path, '/'); + } + + private function loadFurnitureData(): array + { + $path = $this->gamedataPath('FurnitureData.json'); + + return json_decode(file_get_contents($path), true) ?? []; + } + public function syncSoundSets(): array { Log::info('[TraxService] Starting sound sets sync'); - $furnitureData = json_decode( - file_get_contents('/var/www/Gamedata/config/FurnitureData.json'), - true, - ); + $furnitureData = $this->loadFurnitureData(); $inserted = 0; $soundSets = []; @@ -61,7 +76,7 @@ class TraxService { Log::info('[TraxService] Starting soundtracks sync'); - $soundFiles = glob('/var/www/Gamedata/sounds/sound_machine_sample_*.mp3'); + $soundFiles = glob($this->soundsPath('sound_machine_sample_*.mp3')); $existing = DB::table('soundtracks')->pluck('id')->toArray(); $inserted = 0; @@ -102,7 +117,7 @@ class TraxService { return [ 'soundtracks' => DB::table('soundtracks')->count(), - 'sound_samples' => count(glob('/var/www/Gamedata/sounds/sound_machine_sample_*.mp3') ?: []), + 'sound_samples' => count(glob($this->soundsPath('sound_machine_sample_*.mp3')) ?: []), 'room_trax' => DB::table('room_trax')->count(), 'trax_playlist' => DB::table('trax_playlist')->count(), 'soundsets' => $this->countSoundSets(), @@ -111,10 +126,7 @@ class TraxService private function countSoundSets(): int { - $furnitureData = json_decode( - file_get_contents('/var/www/Gamedata/config/FurnitureData.json'), - true, - ); + $furnitureData = $this->loadFurnitureData(); $count = 0; foreach ($furnitureData['roomitemtypes']['furnitype'] ?? [] as $item) { @@ -135,14 +147,10 @@ class TraxService 'failed' => 0, ]; - $soundPath = '/var/www/Gamedata/sounds'; + $soundPath = $this->soundsPath(); $soundUrl = rtrim($baseUrl, '/') . '/gamedata/sounds'; - // Get list of sound samples from FurnitureData - $furnitureData = json_decode( - file_get_contents('/var/www/Gamedata/config/FurnitureData.json'), - true, - ); + $furnitureData = $this->loadFurnitureData(); $sampleIds = []; foreach ($furnitureData['roomitemtypes']['furnitype'] ?? [] as $item) { @@ -162,7 +170,7 @@ class TraxService if (! file_exists($localFile)) { $url = $soundUrl . '/' . $filename; - $content = @file_get_contents($url); + $content = Http::timeout(10)->get($url)->body(); if ($content && strlen($content) > 1000) { file_put_contents($localFile, $content); diff --git a/database/migrations/2026_06_29_000000_add_missing_performance_indexes.php b/database/migrations/2026_06_29_000000_add_missing_performance_indexes.php new file mode 100644 index 0000000..8d118cd --- /dev/null +++ b/database/migrations/2026_06_29_000000_add_missing_performance_indexes.php @@ -0,0 +1,88 @@ +index(['is_approved', 'is_played', 'submitted_at'], 'idx_radio_song_requests_queue'); + }); + + Schema::table('website_help_center_tickets', function (Blueprint $table) { + $table->index(['user_id', 'open'], 'idx_help_tickets_user_open'); + }); + + Schema::table('radio_shouts', function (Blueprint $table) { + $table->index('approved', 'idx_radio_shouts_approved'); + }); + + Schema::table('radio_applications', function (Blueprint $table) { + $table->index('status', 'idx_radio_applications_status'); + }); + + Schema::table('radio_history', function (Blueprint $table) { + $table->index(['user_id', 'ended_at'], 'idx_radio_history_user_ended'); + }); + + Schema::table('radio_song_requests', function (Blueprint $table) { + $table->index(['user_id', 'submitted_at'], 'idx_radio_song_requests_user_date'); + }); + + Schema::table('radio_giveaway_participants', function (Blueprint $table) { + $table->index(['giveaway_id', 'user_id'], 'idx_giveaway_participants_entry'); + }); + + Schema::table('camera_web', function (Blueprint $table) { + $table->index('user_id', 'idx_camera_web_user_id'); + $table->index('timestamp', 'idx_camera_web_timestamp'); + }); + + Schema::table('radio_song_votes', function (Blueprint $table) { + $table->index(['song_request_id', 'user_id'], 'idx_radio_song_votes_entry'); + }); + } + + public function down(): void + { + Schema::table('radio_song_requests', function (Blueprint $table) { + $table->dropIndex('idx_radio_song_requests_queue'); + }); + + Schema::table('website_help_center_tickets', function (Blueprint $table) { + $table->dropIndex('idx_help_tickets_user_open'); + }); + + Schema::table('radio_shouts', function (Blueprint $table) { + $table->dropIndex('idx_radio_shouts_approved'); + }); + + Schema::table('radio_applications', function (Blueprint $table) { + $table->dropIndex('idx_radio_applications_status'); + }); + + Schema::table('radio_history', function (Blueprint $table) { + $table->dropIndex('idx_radio_history_user_ended'); + }); + + Schema::table('radio_song_requests', function (Blueprint $table) { + $table->dropIndex('idx_radio_song_requests_user_date'); + }); + + Schema::table('radio_giveaway_participants', function (Blueprint $table) { + $table->dropIndex('idx_giveaway_participants_entry'); + }); + + Schema::table('camera_web', function (Blueprint $table) { + $table->dropIndex('idx_camera_web_user_id'); + $table->dropIndex('idx_camera_web_timestamp'); + }); + + Schema::table('radio_song_votes', function (Blueprint $table) { + $table->dropIndex('idx_radio_song_votes_entry'); + }); + } +}; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 17b5cc6..ede135f 100755 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -1,22 +1,3 @@ window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; -/** - * Echo exposes an expressive API for subscribing to channels and listening - * for events that are broadcast by Laravel. Echo and event broadcasting - * allows your team to easily build robust real-time web applications. - */ -// import Echo from 'laravel-echo'; - -// import Pusher from 'pusher-js'; -// window.Pusher = Pusher; - -// window.Echo = new Echo({ -// broadcaster: 'pusher', -// key: import.meta.env.VITE_PUSHER_APP_KEY, -// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, -// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, -// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, -// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', -// enabledTransports: ['ws', 'wss'], -// }); diff --git a/resources/themes/atom/views/community/staff-applications.blade.php b/resources/themes/atom/views/community/staff-applications.blade.php index e421c7f..498d84c 100755 --- a/resources/themes/atom/views/community/staff-applications.blade.php +++ b/resources/themes/atom/views/community/staff-applications.blade.php @@ -19,7 +19,7 @@
- {!! $position->description !!} + {{ \Stevebauman\Purify\Facades\Purify::clean($position->description) }}
{{ __('Application Deadline :date', [ diff --git a/resources/themes/atom/views/community/team-applications.blade.php b/resources/themes/atom/views/community/team-applications.blade.php index 024bfbf..1d45aba 100755 --- a/resources/themes/atom/views/community/team-applications.blade.php +++ b/resources/themes/atom/views/community/team-applications.blade.php @@ -43,7 +43,7 @@
- {!! $position->description !!} + {{ \Stevebauman\Purify\Facades\Purify::clean($position->description) }}
{{ __('Application Deadline :date', ['date' => $position->apply_to ? $position->apply_to->format('F j, Y, g:i A') : __('No deadline set')]) }} diff --git a/resources/themes/atom/views/help-center/index.blade.php b/resources/themes/atom/views/help-center/index.blade.php index 034395b..b63710e 100755 --- a/resources/themes/atom/views/help-center/index.blade.php +++ b/resources/themes/atom/views/help-center/index.blade.php @@ -25,38 +25,38 @@
- {!! str_replace(':hotel', $hotelName, $category->content) !!} -
+ {{ \Stevebauman\Purify\Facades\Purify::clean(str_replace(':hotel', $hotelName, $category->content)) }} +
- - - + + + +
+ @endforeach
- @endforeach -
- - {{-- TODO: Selfhost --}} + \ No newline at end of file diff --git a/resources/themes/dusk/views/community/article.blade.php b/resources/themes/dusk/views/community/article.blade.php index d03afe5..8c4f4e0 100755 --- a/resources/themes/dusk/views/community/article.blade.php +++ b/resources/themes/dusk/views/community/article.blade.php @@ -42,7 +42,7 @@
- {!! $article->full_story !!} + {{ \Stevebauman\Purify\Facades\Purify::clean($article->full_story) }}
@include('community.partials.article-reactions') diff --git a/resources/themes/dusk/views/community/staff-applications.blade.php b/resources/themes/dusk/views/community/staff-applications.blade.php index 4a82c81..f55a211 100755 --- a/resources/themes/dusk/views/community/staff-applications.blade.php +++ b/resources/themes/dusk/views/community/staff-applications.blade.php @@ -15,7 +15,7 @@
- {!! $position->description !!} + {{ \Stevebauman\Purify\Facades\Purify::clean($position->description) }}
{{ __('Application Deadline :date', [ diff --git a/resources/themes/dusk/views/community/team-applications.blade.php b/resources/themes/dusk/views/community/team-applications.blade.php index 6dd2381..17de2df 100755 --- a/resources/themes/dusk/views/community/team-applications.blade.php +++ b/resources/themes/dusk/views/community/team-applications.blade.php @@ -38,7 +38,7 @@
- {!! $reply->content !!} + {{ \Stevebauman\Purify\Facades\Purify::clean($reply->content) }}
@else @@ -171,7 +171,7 @@
- {!! $reply->content !!} + {{ \Stevebauman\Purify\Facades\Purify::clean($reply->content) }}
@endif diff --git a/resources/themes/dusk/views/logo-generator.blade.php b/resources/themes/dusk/views/logo-generator.blade.php index c66c24b..f82e7ee 100755 --- a/resources/themes/dusk/views/logo-generator.blade.php +++ b/resources/themes/dusk/views/logo-generator.blade.php @@ -72,7 +72,6 @@ - {{-- TODO: Selfhost --}} + diff --git a/resources/views/community/radio/index.blade.php b/resources/views/community/radio/index.blade.php index d6e164e..338803c 100755 --- a/resources/views/community/radio/index.blade.php +++ b/resources/views/community/radio/index.blade.php @@ -56,7 +56,7 @@ @section('content')
- @if(!$enabled) + @if(!$isOnline)
diff --git a/routes/api.php b/routes/api.php index 7e36990..e6b344e 100755 --- a/routes/api.php +++ b/routes/api.php @@ -29,7 +29,7 @@ Route::prefix('auth')->group(function () { Route::post('/login', [AuthController::class, 'login']); Route::post('/logout', [AuthController::class, 'logout'])->middleware('auth:sanctum'); Route::get('/user', [AuthController::class, 'user'])->middleware('auth:sanctum'); - Route::post('/register', [AuthController::class, 'register']); + Route::post('/register', [AuthController::class, 'register'])->middleware('throttle:register'); Route::put('/user', [AuthController::class, 'updateUser'])->middleware('auth:sanctum'); Route::put('/user/password', [AuthController::class, 'updatePassword'])->middleware('auth:sanctum'); }); @@ -81,7 +81,7 @@ Route::get('/radio/listeners', [RadioController::class, 'listeners'])->middlewar Route::get('/radio/shouts', [RadioController::class, 'getShouts'])->middleware('throttle:100,1')->name('api.radio.shouts'); // Radio SSE (Server-Sent Events) stream -Route::get('/radio/sse', [\App\Http\Controllers\Radio\SseController::class, 'stream'])->name('api.radio.sse'); +Route::get('/radio/sse', [\App\Http\Controllers\Radio\SseController::class, 'stream'])->middleware('throttle:sse')->name('api.radio.sse'); // Radio embed config Route::get('/radio/embed/config', [\App\Http\Controllers\Radio\EmbedController::class, 'config'])->middleware('throttle:100,1')->name('api.radio.embed.config'); @@ -108,7 +108,7 @@ Route::post('/help/tickets', [HelpApiController::class, 'create'])->middleware(' Route::post('/help/tickets/{id}/reply', [HelpApiController::class, 'reply'])->middleware('auth:sanctum'); // Photo Upload -Route::post('/photos/upload', [MediaApiController::class, 'upload'])->middleware('auth:sanctum'); +Route::post('/photos/upload', [MediaApiController::class, 'upload'])->middleware(['auth:sanctum', 'throttle:upload']); // Shop Purchase Route::post('/shop/packages/{packageId}/purchase', [ShopApiController::class, 'purchase'])->middleware('auth:sanctum');