Initialer Laravel Commit für BetiX
This commit is contained in:
25
app/Http/Middleware/CheckBanned.php
Normal file
25
app/Http/Middleware/CheckBanned.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class CheckBanned
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (Auth::check() && Auth::user()->is_banned) {
|
||||
// Get reason
|
||||
$ban = Auth::user()->restrictions()->where('type', 'account_ban')->where('active', true)->first();
|
||||
|
||||
return Inertia::render('Errors/Banned', [
|
||||
'reason' => $ban ? $ban->reason : 'No reason provided.'
|
||||
])->toResponse($request)->setStatusCode(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
43
app/Http/Middleware/DetectCiphertextInJson.php
Normal file
43
app/Http/Middleware/DetectCiphertextInJson.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DetectCiphertextInJson
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// Only act in non-production environments
|
||||
if (app()->environment(['local', 'development', 'staging'])) {
|
||||
$contentType = (string) $response->headers->get('Content-Type', '');
|
||||
if (str_contains($contentType, 'application/json')) {
|
||||
$body = (string) $response->getContent();
|
||||
if ($this->containsLaravelCiphertext($body)) {
|
||||
// Log minimal info without PII values
|
||||
logger()->warning('Ciphertext detected in JSON response', [
|
||||
'path' => $request->path(),
|
||||
'method' => $request->getMethod(),
|
||||
]);
|
||||
|
||||
// If you prefer to hard-block in dev/stage, uncomment:
|
||||
// return response()->json(['error' => 'Ciphertext detected in response'], 422);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function containsLaravelCiphertext(string $json): bool
|
||||
{
|
||||
// Heuristic: look for base64-encoded JSON blobs and typical Laravel keys iv/value/mac
|
||||
if (!str_contains($json, 'eyJ')) {
|
||||
return false;
|
||||
}
|
||||
return (str_contains($json, '"iv"') && str_contains($json, '"value"') && str_contains($json, '"mac"'));
|
||||
}
|
||||
}
|
||||
67
app/Http/Middleware/EnforceRestriction.php
Normal file
67
app/Http/Middleware/EnforceRestriction.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\BackendHttpClient;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class EnforceRestriction
|
||||
{
|
||||
public function __construct(private readonly BackendHttpClient $client)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* @param array<int,string> $types One or more restriction types to check
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, ...$types)
|
||||
{
|
||||
$user = $request->user();
|
||||
if (!$user) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (empty($types)) {
|
||||
$types = ['account_ban'];
|
||||
}
|
||||
|
||||
try {
|
||||
// Ask upstream whether any of the requested types are active for the current user
|
||||
$query = ['types' => implode(',', $types)];
|
||||
$res = $this->client->get($request, '/users/me/restrictions/check', $query, retry: true);
|
||||
if ($res->successful()) {
|
||||
$json = $res->json() ?: [];
|
||||
$data = $json['data'] ?? $json; // support either {data:{...}} or flat map
|
||||
$isRestricted = false;
|
||||
$hitType = null;
|
||||
foreach ($types as $t) {
|
||||
if (!empty($data[$t])) { $isRestricted = true; $hitType = $t; break; }
|
||||
}
|
||||
|
||||
if ($isRestricted) {
|
||||
// For web requests: log out if account is banned and block access
|
||||
if (!$request->expectsJson() && in_array('account_ban', $types, true)) {
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'message' => 'Access is restricted for this action.',
|
||||
'type' => $hitType,
|
||||
];
|
||||
|
||||
return response()->json($payload, 403);
|
||||
}
|
||||
}
|
||||
// On client/server error we fail open to avoid locking out users due to upstream outage
|
||||
} catch (\Throwable $e) {
|
||||
// swallow and continue
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
168
app/Http/Middleware/GeoBlockMiddleware.php
Normal file
168
app/Http/Middleware/GeoBlockMiddleware.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class GeoBlockMiddleware
|
||||
{
|
||||
private const BYPASS_PATHS = [
|
||||
'blocked', 'favicon.ico', 'up',
|
||||
'admin', 'login', 'register',
|
||||
];
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// Skip for admin users, static paths, and the blocked page itself
|
||||
if ($this->shouldBypass($request)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$settings = AppSetting::get('geo.settings', []);
|
||||
|
||||
if (empty($settings['enabled'])) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$ip = $request->ip();
|
||||
|
||||
// Never block localhost in dev
|
||||
if (in_array($ip, ['127.0.0.1', '::1', '::ffff:127.0.0.1'])) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$geoData = $this->fetchGeoData($ip);
|
||||
$country = $geoData['countryCode'] ?? null;
|
||||
$isProxy = $geoData['proxy'] ?? false;
|
||||
$isHosting = $geoData['hosting'] ?? false;
|
||||
|
||||
// VPN / Proxy check
|
||||
if (!empty($settings['vpn_block'])) {
|
||||
$vpnProvider = $settings['vpn_provider'] ?? 'none';
|
||||
|
||||
$isVpn = match ($vpnProvider) {
|
||||
'ipqualityscore' => Cache::remember("geo_vpn_iqs_{$ip}", 3600, fn() => $this->checkIpQualityScore($ip, $settings['vpn_api_key'] ?? '')),
|
||||
'proxycheck' => Cache::remember("geo_vpn_pc_{$ip}", 3600, fn() => $this->checkProxyCheck($ip, $settings['vpn_api_key'] ?? '')),
|
||||
default => ($isProxy || $isHosting), // free ip-api.com fallback
|
||||
};
|
||||
|
||||
if ($isVpn) {
|
||||
return $this->blockResponse($request, $settings, 'vpn');
|
||||
}
|
||||
}
|
||||
|
||||
// Country check
|
||||
if ($country) {
|
||||
$mode = $settings['mode'] ?? 'blacklist';
|
||||
$blocked = array_map('strtoupper', $settings['blocked_countries'] ?? []);
|
||||
$allowed = array_map('strtoupper', $settings['allowed_countries'] ?? []);
|
||||
$upper = strtoupper($country);
|
||||
|
||||
$isBlocked = match ($mode) {
|
||||
'whitelist' => !empty($allowed) && !in_array($upper, $allowed),
|
||||
default => !empty($blocked) && in_array($upper, $blocked),
|
||||
};
|
||||
|
||||
if ($isBlocked) {
|
||||
return $this->blockResponse($request, $settings, 'country');
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function shouldBypass(Request $request): bool
|
||||
{
|
||||
$path = ltrim($request->path(), '/');
|
||||
|
||||
foreach (self::BYPASS_PATHS as $bypass) {
|
||||
if ($path === $bypass || str_starts_with($path, $bypass . '/')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip API routes
|
||||
if ($request->is('api/*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function blockResponse(Request $request, array $settings, string $reason)
|
||||
{
|
||||
$message = $settings['block_message'] ?? 'This service is not available in your region.';
|
||||
$redirectUrl = $settings['redirect_url'] ?? '';
|
||||
|
||||
if ($redirectUrl) {
|
||||
return redirect()->away($redirectUrl);
|
||||
}
|
||||
|
||||
if ($request->header('X-Inertia')) {
|
||||
return Inertia::render('GeoBlocked', [
|
||||
'message' => $message,
|
||||
'reason' => $reason,
|
||||
])->toResponse($request)->setStatusCode(403);
|
||||
}
|
||||
|
||||
return Inertia::render('GeoBlocked', [
|
||||
'message' => $message,
|
||||
'reason' => $reason,
|
||||
])->toResponse($request)->setStatusCode(403);
|
||||
}
|
||||
|
||||
private function fetchGeoData(string $ip): array
|
||||
{
|
||||
return Cache::remember("geo_data_{$ip}", 3600, function () use ($ip) {
|
||||
try {
|
||||
$res = Http::timeout(3)->get("http://ip-api.com/json/{$ip}", [
|
||||
'fields' => 'countryCode,proxy,hosting',
|
||||
]);
|
||||
if ($res->ok()) {
|
||||
return $res->json() ?? [];
|
||||
}
|
||||
} catch (\Throwable) {}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
private function checkIpQualityScore(string $ip, string $apiKey): bool
|
||||
{
|
||||
if (!$apiKey) return false;
|
||||
try {
|
||||
$res = Http::timeout(4)->get("https://ipqualityscore.com/api/json/ip/{$apiKey}/{$ip}", [
|
||||
'strictness' => 1,
|
||||
'allow_public_access_points' => 'true',
|
||||
'fast' => 'true',
|
||||
]);
|
||||
if ($res->ok()) {
|
||||
$data = $res->json();
|
||||
return (bool) ($data['vpn'] ?? $data['proxy'] ?? $data['tor'] ?? false);
|
||||
}
|
||||
} catch (\Throwable) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function checkProxyCheck(string $ip, string $apiKey): bool
|
||||
{
|
||||
if (!$apiKey) return false;
|
||||
try {
|
||||
$res = Http::timeout(4)->get("https://proxycheck.io/v2/{$ip}", [
|
||||
'key' => $apiKey,
|
||||
'vpn' => 1,
|
||||
'asn' => 0,
|
||||
]);
|
||||
if ($res->ok()) {
|
||||
$data = $res->json();
|
||||
$entry = $data[$ip] ?? [];
|
||||
return strtolower($entry['proxy'] ?? 'no') === 'yes';
|
||||
}
|
||||
} catch (\Throwable) {}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
23
app/Http/Middleware/HandleAppearance.php
Normal file
23
app/Http/Middleware/HandleAppearance.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class HandleAppearance
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
View::share('appearance', $request->cookie('appearance') ?? 'system');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
84
app/Http/Middleware/HandleInertiaRequests.php
Normal file
84
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
/**
|
||||
* The root template that's loaded on the first page visit.
|
||||
*
|
||||
* @see https://inertiajs.com/server-side-setup#root-template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rootView = 'app';
|
||||
|
||||
/**
|
||||
* Determines the current asset version.
|
||||
*
|
||||
* @see https://inertiajs.com/asset-versioning
|
||||
*/
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that are shared by default.
|
||||
*
|
||||
* @see https://inertiajs.com/shared-data
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function share(Request $request): array
|
||||
{
|
||||
$u = $request->user();
|
||||
|
||||
// Fully externalized mode: do not load local Eloquent relations here.
|
||||
// All feature data (stats, wallets, restrictions, etc.) must be fetched via
|
||||
// the external API through proxy controllers/endpoints.
|
||||
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'api_url' => config('app.api_url'), // Pass API URL to frontend
|
||||
'locale' => app()->getLocale(),
|
||||
'availableLocales' => [
|
||||
['code' => 'en', 'label' => 'English', 'flag' => 'https://flagcdn.com/w20/gb.png'],
|
||||
['code' => 'de', 'label' => 'Deutsch', 'flag' => 'https://flagcdn.com/w20/de.png'],
|
||||
['code' => 'es', 'label' => 'Español', 'flag' => 'https://flagcdn.com/w20/es.png'],
|
||||
['code' => 'pt_BR', 'label' => 'Português (Brasil)', 'flag' => 'https://flagcdn.com/w20/br.png'],
|
||||
['code' => 'tr', 'label' => 'Türkçe', 'flag' => 'https://flagcdn.com/w20/tr.png'],
|
||||
['code' => 'pl', 'label' => 'Polski', 'flag' => 'https://flagcdn.com/w20/pl.png'],
|
||||
],
|
||||
'dir' => 'ltr',
|
||||
'auth' => [
|
||||
'user' => $u ? [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'username' => $u->username,
|
||||
'email' => $u->email,
|
||||
// Avatar: prefer uploaded DB avatar, fall back to OAuth avatar_url
|
||||
'avatar' => $u->avatar,
|
||||
'avatar_url' => $u->avatar_url,
|
||||
'role' => $u->role,
|
||||
'clan_tag' => $u->clan_tag,
|
||||
'vip_level' => (int) ($u->vip_level ?? 0),
|
||||
'balance' => (string) ($u->balance ?? '0'),
|
||||
'stats' => $u->stats ? [
|
||||
'vip_level' => (int) ($u->stats->vip_level ?? 0),
|
||||
'vip_points' => (int) ($u->stats->vip_points ?? 0),
|
||||
] : null,
|
||||
'restrictions' => $u->restrictions()
|
||||
->where('active', true)
|
||||
->where(fn($q) => $q->whereNull('ends_at')->orWhere('ends_at', '>', now()))
|
||||
->get(['type', 'reason', 'ends_at', 'starts_at', 'active']),
|
||||
] : null,
|
||||
],
|
||||
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
|
||||
];
|
||||
}
|
||||
}
|
||||
50
app/Http/Middleware/MaintenanceModeMiddleware.php
Normal file
50
app/Http/Middleware/MaintenanceModeMiddleware.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\AppSetting;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class MaintenanceModeMiddleware
|
||||
{
|
||||
private const BYPASS_PATHS = ['up', 'login', 'logout', 'blocked'];
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// Admins always pass through
|
||||
if (Auth::check() && strtolower((string) Auth::user()->role) === 'admin') {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip API webhooks and bypass paths
|
||||
if ($request->is('api/webhooks/*') || $this->shouldBypass($request)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Skip admin routes for auth
|
||||
if (str_starts_with($request->path(), 'admin')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$settings = AppSetting::get('site.settings', []);
|
||||
|
||||
if (!empty($settings['maintenance_mode'])) {
|
||||
return Inertia::render('Maintenance', [
|
||||
'message' => 'Wir führen gerade Wartungsarbeiten durch. Bitte komm später zurück.',
|
||||
])->toResponse($request)->setStatusCode(503);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function shouldBypass(Request $request): bool
|
||||
{
|
||||
foreach (self::BYPASS_PATHS as $path) {
|
||||
if ($request->is($path) || $request->is($path . '/*')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
81
app/Http/Middleware/SetLocale.php
Normal file
81
app/Http/Middleware/SetLocale.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
/** @var array<string> */
|
||||
private array $available = [
|
||||
'en','de','es','pt_BR','tr','pl',
|
||||
// prepared, not yet enabled in UI
|
||||
'fr','it','ru','uk','vi','id','zh_CN','ja','ko','sv','no','fi','nl',
|
||||
];
|
||||
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$locale = $this->resolveLocale($request);
|
||||
|
||||
// Apply
|
||||
app()->setLocale($locale);
|
||||
|
||||
// Persist for guests as well (1 year)
|
||||
$request->session()->put('locale', $locale);
|
||||
Cookie::queue(cookie('locale', $locale, 60 * 24 * 365));
|
||||
|
||||
// If logged in and preference changed, consider persisting upstream (no local DB writes)
|
||||
if ($user = $request->user()) {
|
||||
if (($user->preferred_locale ?? null) !== $locale) {
|
||||
// Defer persistence to external API; keep request-scoped preference only.
|
||||
// Optionally, emit an event or queue a task to sync.
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function resolveLocale(Request $request): string
|
||||
{
|
||||
// 1) explicit query param ?lang=xx
|
||||
$q = $request->query('lang');
|
||||
if ($q && $this->isAllowed($q)) return $this->normalize($q);
|
||||
|
||||
// 2) user preference
|
||||
$u = $request->user();
|
||||
if ($u && $this->isAllowed($u->preferred_locale ?? null)) return $this->normalize($u->preferred_locale);
|
||||
|
||||
// 3) session
|
||||
$s = $request->session()->get('locale');
|
||||
if ($this->isAllowed($s)) return $this->normalize((string) $s);
|
||||
|
||||
// 4) cookie
|
||||
$c = $request->cookie('locale');
|
||||
if ($this->isAllowed($c)) return $this->normalize((string) $c);
|
||||
|
||||
// 5) Accept-Language best effort
|
||||
$preferred = $request->getPreferredLanguage($this->available);
|
||||
if ($this->isAllowed($preferred)) return $this->normalize((string) $preferred);
|
||||
|
||||
// 6) fallback
|
||||
return config('app.locale', 'en');
|
||||
}
|
||||
|
||||
private function isAllowed($code): bool
|
||||
{
|
||||
if (!$code) return false;
|
||||
$norm = $this->normalize((string) $code);
|
||||
return in_array($norm, $this->available, true);
|
||||
}
|
||||
|
||||
private function normalize(string $code): string
|
||||
{
|
||||
// Normalize e.g. pt-br → pt_BR, zh-cn → zh_CN; others lower-case
|
||||
$code = str_replace([' ', '-'], ['','_'], trim($code));
|
||||
if (strtolower($code) === 'pt_br') return 'pt_BR';
|
||||
if (strtolower($code) === 'zh_cn') return 'zh_CN';
|
||||
return strtolower($code);
|
||||
}
|
||||
}
|
||||
46
app/Http/Middleware/ValidateLicenseKey.php
Normal file
46
app/Http/Middleware/ValidateLicenseKey.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\OperatorCasino;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ValidateLicenseKey
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Accept the key from X-License-Key header, body field, or query parameter
|
||||
$key = $request->header('X-License-Key')
|
||||
?? $request->input('license_key')
|
||||
?? $request->query('license_key');
|
||||
|
||||
if (!$key) {
|
||||
return response()->json(['error' => 'License key required'], 401);
|
||||
}
|
||||
|
||||
$casino = OperatorCasino::findByKey((string) $key);
|
||||
|
||||
if (!$casino) {
|
||||
return response()->json(['error' => 'Invalid license key'], 401);
|
||||
}
|
||||
|
||||
if (!$casino->isActive()) {
|
||||
return response()->json(['error' => 'Casino license inactive'], 403);
|
||||
}
|
||||
|
||||
// IP whitelist — skip when the list is empty
|
||||
if (!empty($casino->ip_whitelist)) {
|
||||
$ip = $request->ip();
|
||||
if (!in_array($ip, $casino->ip_whitelist, true)) {
|
||||
return response()->json(['error' => "IP not authorized: {$ip}"], 403);
|
||||
}
|
||||
}
|
||||
|
||||
// Attach casino to request so controllers can use it without re-querying
|
||||
$request->attributes->set('operator_casino', $casino);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user