Initialer Laravel Commit für BetiX
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
2026-04-04 18:01:50 +02:00
commit 0280278978
374 changed files with 65210 additions and 0 deletions

82
routes/api.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BonusApiController;
use App\Http\Controllers\UserRestrictionApiController;
use App\Http\Controllers\VaultApiController;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
| These routes are intended for machine-to-machine access from an external
| dashboard. Authentication is enforced inside controllers via a static
| Bearer token configured in config/services.php.
|
| Notes:
| - All routes here are automatically prefixed with /api by RouteServiceProvider.
| - We expose both unversioned and /v1/ versioned endpoints for flexibility.
*/
// Health & diagnostics
Route::get('/health', function () {
return response()->json([
'status' => 'ok',
'timestamp' => now()->toIso8601String(),
'version' => 'v1',
]);
})->middleware('throttle:60,1')->name('api.health');
Route::get('/ping', function () {
return response('pong', 200);
})->middleware('throttle:60,1')->name('api.ping');
// CORS preflight for external admin dashboard or services
Route::options('/{any}', function () {
return response()->noContent(204);
})->where('any', '.*');
$registerExternalApi = function () {
// Read endpoints
Route::get('/bonuses', [BonusApiController::class, 'index'])->middleware('throttle:60,1');
Route::get('/bonuses/{id}', [BonusApiController::class, 'show'])->whereNumber('id')->middleware('throttle:60,1');
// Mutating endpoints (stricter throttle)
Route::post('/bonuses', [BonusApiController::class, 'store'])->middleware('throttle:20,1');
Route::patch('/bonuses/{id}', [BonusApiController::class, 'update'])->whereNumber('id')->middleware('throttle:20,1');
Route::delete('/bonuses/{id}', [BonusApiController::class, 'destroy'])->whereNumber('id')->middleware('throttle:20,1');
// Moderation API for user restrictions (token auth inside controller)
Route::get('/users/{id}/restrictions', [UserRestrictionApiController::class, 'listForUser'])->whereNumber('id')->middleware('throttle:60,1');
Route::get('/users/{id}/restrictions/check', [UserRestrictionApiController::class, 'checkForUser'])->whereNumber('id')->middleware('throttle:60,1');
Route::post('/users/{id}/restrictions', [UserRestrictionApiController::class, 'createForUser'])->whereNumber('id')->middleware('throttle:20,1');
Route::post('/users/{id}/restrictions/upsert', [UserRestrictionApiController::class, 'upsertForUser'])->whereNumber('id')->middleware('throttle:20,1');
Route::patch('/restrictions/{id}', [UserRestrictionApiController::class, 'update'])->whereNumber('id')->middleware('throttle:20,1');
Route::delete('/restrictions/{id}', [UserRestrictionApiController::class, 'destroy'])->whereNumber('id')->middleware('throttle:20,1');
// External Vault API (token auth inside controller)
Route::get('/users/{id}/vault', [VaultApiController::class, 'showForUser'])->whereNumber('id')->middleware('throttle:60,1');
Route::post('/users/{id}/vault/deposit', [VaultApiController::class, 'depositForUser'])->whereNumber('id')->middleware('throttle:20,1');
Route::post('/users/{id}/vault/withdraw', [VaultApiController::class, 'withdrawForUser'])->whereNumber('id')->middleware('throttle:20,1');
};
// NOWPayments IPN webhook (public). Note: The API middleware group has CSRF disabled by default.
Route::post('/webhooks/nowpayments', [\App\Http\Controllers\NowPaymentsWebhookController::class, '__invoke'])
->middleware('throttle:60,1')
->name('api.webhooks.nowpayments');
// BetiX Originals: session-ended webhook (public, HMAC-verified inside controller)
Route::post('/betix/session-ended', [\App\Http\Controllers\BetiXWebhookController::class, 'sessionEnded'])
->middleware('throttle:120,1')
->name('api.webhooks.betix');
// BetiX Originals: per-round balance update (public, HMAC-verified inside controller)
Route::post('/betix/round', [\App\Http\Controllers\BetiXWebhookController::class, 'roundUpdate'])
->middleware('throttle:600,1')
->name('api.webhooks.betix.round');
// Unversioned (backward compatibility)
Route::group([], $registerExternalApi);
// Versioned alias
Route::prefix('v1')->group($registerExternalApi);

12
routes/console.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use App\Console\Commands\ReencryptUserData;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
// Note: Registering commands via Artisan::starting is not supported in this framework version.
// If needed, register commands in app/Console/Kernel.php or rely on auto-discovery.

40
routes/operator.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
use App\Http\Controllers\OperatorController;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| B2B Operator / Casino Integration Routes
|--------------------------------------------------------------------------
|
| These routes are loaded without the /api prefix by bootstrap/app.php,
| so they are available at /operator/* as documented.
|
| All routes are protected by the license.key middleware which:
| - Reads X-License-Key header OR license_key body/query field
| - Hashes the key and looks it up in operator_casinos
| - Checks the casino is active
| - Enforces IP whitelist if configured
| - Binds the OperatorCasino instance as $request->attributes->get('operator_casino')
|
*/
Route::middleware(['license.key'])->group(function () {
// Game catalog — list all available games with metadata
Route::get('/games', [OperatorController::class, 'games'])
->middleware('throttle:120,1')
->name('operator.games');
// Launch a game session for a player — returns launch_url + session_token
Route::post('/launch', [OperatorController::class, 'launch'])
->middleware('throttle:30,1')
->name('operator.launch');
// Query the current / final state of a session (balance delta, status)
Route::get('/session/{token}', [OperatorController::class, 'session'])
->middleware('throttle:120,1')
->where('token', '[0-9a-f\-]{36}')
->name('operator.session');
});

42
routes/settings.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
use App\Http\Controllers\Settings\PasswordController;
use App\Http\Controllers\Settings\ProfileController;
use App\Http\Controllers\Settings\TwoFactorAuthenticationController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware(['auth'])->group(function () {
// Route::redirect('settings', '/settings/profile'); // Removed to allow /settings for public profile config
Route::get('settings/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('settings/profile', [ProfileController::class, 'update'])->middleware('throttle:20,1')->name('profile.update');
// KYC pages & APIs
Route::get('settings/kyc', [\App\Http\Controllers\Settings\KycController::class, 'index'])->name('settings.kyc');
Route::post('settings/kyc', [\App\Http\Controllers\Settings\KycController::class, 'store'])->middleware('throttle:10,1')->name('settings.kyc.store');
Route::delete('settings/kyc/{doc}', [\App\Http\Controllers\Settings\KycController::class, 'destroy'])->middleware('throttle:20,1')->name('settings.kyc.destroy');
Route::get('settings/kyc/{doc}/download', [\App\Http\Controllers\Settings\KycController::class, 'download'])->name('settings.kyc.download');
// Security pages & APIs
Route::get('settings/security', [\App\Http\Controllers\Settings\SecurityController::class, 'index'])->name('settings.security');
Route::get('settings/security/sessions', [\App\Http\Controllers\Settings\SecurityController::class, 'sessions'])->name('settings.security.sessions');
Route::delete('settings/security/sessions/{id}', [\App\Http\Controllers\Settings\SecurityController::class, 'revoke'])->middleware('throttle:20,1')->name('settings.security.sessions.revoke');
});
Route::middleware(['auth', 'verified'])->group(function () {
Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
Route::get('settings/password', [PasswordController::class, 'edit'])->name('user-password.edit');
Route::put('settings/password', [PasswordController::class, 'update'])
->middleware('throttle:6,1')
->name('user-password.update');
Route::get('settings/appearance', function () {
return Inertia::render('settings/Appearance');
})->name('appearance.edit');
Route::get('settings/two-factor', [TwoFactorAuthenticationController::class, 'show'])
->name('two-factor.show');
});

670
routes/web.php Normal file
View File

@@ -0,0 +1,670 @@
<?php
use App\Http\Controllers\AdminController;
use App\Http\Controllers\Auth\AvailabilityController;
use App\Http\Controllers\Auth\EmailVerificationCodeController;
use App\Http\Controllers\GuildController;
use App\Http\Controllers\GuildActionController;
use App\Http\Controllers\LocaleController;
use App\Http\Controllers\SocialController;
use App\Http\Controllers\WalletController;
use App\Http\Controllers\VaultController;
use App\Http\Controllers\VaultPinController;
use App\Http\Controllers\DepositController;
use App\Http\Controllers\VipController;
use App\Http\Controllers\Admin\PromoAdminController;
use App\Http\Controllers\Admin\SupportAdminController;
use App\Http\Controllers\EmbedController;
use App\Http\Controllers\FeedbackController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', function () {
return redirect()->route('dashboard');
})->name('home');
// Public Pages
Route::get('/blocked', function () {
return Inertia::render('GeoBlocked', [
'message' => \App\Models\AppSetting::get('geo.settings', [])['block_message'] ?? 'This service is not available in your region.',
'reason' => 'country',
]);
})->name('geo.blocked')->withoutMiddleware([\App\Http\Middleware\GeoBlockMiddleware::class]);
Route::get('/maintenance', function () {
return Inertia::render('Maintenance', ['message' => 'Wir führen gerade Wartungsarbeiten durch.']);
})->name('maintenance')->withoutMiddleware([\App\Http\Middleware\MaintenanceModeMiddleware::class]);
Route::get('/faq', function () { return Inertia::render('Faq'); })->name('faq');
Route::get('/api/auth/availability', AvailabilityController::class)->name('api.auth.availability');
Route::get('/legal/terms', function () { return Inertia::render('policies/Terms'); })->name('legal.terms');
Route::get('/legal/cookies', function () { return Inertia::render('policies/Cookies'); })->name('legal.cookies');
Route::get('/legal/privacy', function () { return Inertia::render('policies/Privacy'); })->name('legal.privacy');
Route::get('/legal/bonus-policy', function () { return Inertia::render('policies/BonusPolicy'); })->name('legal.bonus');
Route::get('/legal/disputes', function () { return Inertia::render('policies/Disputes'); })->name('legal.disputes');
Route::get('/legal/responsible-gaming', function () { return Inertia::render('policies/ResponsibleGaming'); })->name('legal.responsible');
Route::get('/legal/aml', function () { return Inertia::render('policies/Aml'); })->name('legal.aml');
Route::get('/legal/risk-warnings', function () { return Inertia::render('policies/RiskWarnings'); })->name('legal.risks');
// Email verification via code (must be logged in but not yet verified)
Route::post('/email/verify/code', EmailVerificationCodeController::class)
->middleware(['auth:web', 'throttle:10,1'])
->name('verification.verify.code');
// Local-only email preview routes
if (app()->environment('local')) {
Route::get('/dev/mail/preview/{type}', function (Request $request, string $type) {
$payload = $request->except(['_token']);
$mailable = new \App\Mail\SystemNotificationMail($type, $payload);
return view('emails.notification', $mailable->data);
})->name('dev.mail.preview');
Route::get('/dev/mail/preview/verify-email', function () {
$user = (object) ['name' => 'Demo User'];
$url = url('/email/verify/sample-token');
$code = '123456';
return view('emails.verify-email', compact('user', 'url', 'code'));
})->name('dev.mail.preview.verify');
Route::get('/dev/mail', function () {
$base = url('/dev/mail');
$types = [
'deposit', 'withdrawal', 'bonus_available', 'banned', 'chat_banned',
'level_up', 'near_level_up', 'inactivity_check', 'friend_request',
'kyc_error', 'kyc_accepted', 'email_2fa',
'terms_updated', 'cookie_policy_updated', 'privacy_policy_updated',
'bonus_policy_updated', 'dispute_policy_updated', 'responsible_gaming_updated',
'aml_policy_updated', 'risk_warnings_updated', 'new_support_message', 'casino_updated',
];
$links = array_map(fn ($t) => "<li><a href='{$base}/preview/{$t}'>{$t}</a></li>", $types);
$html = "<html><head><title>Mail Previews</title><style>body{background:#0b0b0b;color:#ddd;font-family:Arial,sans-serif;padding:30px} a{color:#ff007a;text-decoration:none} a:hover{text-decoration:underline} .card{background:#0f0f0f;border:1px solid #191919;border-radius:12px;padding:20px;max-width:720px} h1{margin-top:0;color:#fff} ul{columns:2;gap:30px}</style></head><body><div class='card'><h1>Mail Previews (local)</h1><p>Diese Seite ist nur in APP_ENV=local verfügbar.</p><h3>SystemNotificationMail</h3><ul>" . implode('', $links) . "</ul><h3>Weitere Templates</h3><ul><li><a href='{$base}/preview/verify-email'>verify-email.blade.php</a></li></ul></div></body></html>";
return response($html)->header('Content-Type', 'text/html');
})->name('dev.mail.index');
// Gameplay preview (no auth required visual QA only)
Route::get('/dev/gameplay/{slug?}', function (string $slug = 'book-of-ra') {
$games = [
'book-of-ra' => [
'name' => 'Book of Ra Deluxe',
'provider' => 'Demo',
'description' => 'Free Slot Demo',
'src' => 'https://free-slots.games/greenslots/BookOfRaDX/index.php',
],
];
$game = $games[$slug] ?? $games['book-of-ra'];
return Inertia::render('GamePlay', [
'title' => $game['name'],
'slug' => $slug,
'src' => $game['src'],
'provider' => $game['provider'],
'description' => $game['description'],
]);
});
// Gameplay preview index (no auth required)
Route::get('/dev/gameplay', function () {
$base = url('/dev/gameplay');
$games = [
'dice', 'crash', 'mines', 'plinko',
'gates-of-olympus', 'sweet-bonanza', 'razor-shark', 'mental', 'wanted-dead-or-a-wild',
];
$links = array_map(fn ($g) => "<li><a href='{$base}/{$g}'>{$g}</a></li>", $games);
$html = "<html><head><title>Gameplay Previews</title><style>body{background:#0b0b0b;color:#ddd;font-family:Arial,sans-serif;padding:30px} a{color:#ff007a;text-decoration:none} a:hover{text-decoration:underline} .card{background:#0f0f0f;border:1px solid #191919;border-radius:12px;padding:20px;max-width:720px} h1{margin-top:0;color:#fff} ul{list-style:none;padding:0;display:flex;flex-direction:column;gap:8px}</style></head><body><div class='card'><h1>Gameplay Previews (local)</h1><p>Kein Login erforderlich nur in APP_ENV=local verfügbar.</p><ul>" . implode('', $links) . "</ul></div></body></html>";
return response($html)->header('Content-Type', 'text/html');
})->name('dev.gameplay.index');
}
// Note: /login POST and /register POST are handled by Laravel Fortify (FortifyServiceProvider)
// Rate limiting is configured in FortifyServiceProvider via RateLimiter::for('login', ...)
// Authenticated Routes (Inertia Pages)
Route::middleware([
'restrict:account_ban',
'throttle:1200,1',
])->group(function () {
// Authenticated API-ish routes for Vault & User Bonuses (local, no external API)
// IMPORTANT: These MUST match exactly the requests made in Gateway tests (e.g. /api/wallet/vault)
Route::group(['prefix' => 'api'], function() {
Route::get('/wallet/vault', [VaultController::class, 'show'])->name('api.vault.show');
Route::get('/wallet/balance', [WalletController::class, 'balance'])->name('api.wallet.balance');
Route::get('/wallet/bets', function () {
$user = \Illuminate\Support\Facades\Auth::user();
if (!$user) return response()->json(['error' => 'Unauthorized'], 401);
$bets = \App\Models\GameBet::where('user_id', $user->id)
->orderByDesc('created_at')
->limit(50)
->get()
->map(fn ($b) => [
'id' => $b->id,
'game' => $b->game_name,
'wager' => (float) $b->wager_amount,
'payout' => (float) $b->payout_amount,
'net' => (float) $b->payout_amount - (float) $b->wager_amount,
'currency' => $b->currency,
'round' => $b->round_number,
'session_token' => $b->session_token,
'server_seed_hash' => $b->server_seed_hash,
'created_at' => $b->created_at?->toIso8601String(),
]);
return response()->json(['bets' => $bets]);
})->middleware('throttle:60,1')->name('api.wallet.bets');
Route::post('/wallet/vault/deposit', [VaultController::class, 'deposit'])->middleware('throttle:30,1')->name('api.vault.deposit');
Route::post('/wallet/vault/withdraw', [VaultController::class, 'withdraw'])->middleware('throttle:30,1')->name('api.vault.withdraw');
Route::post('/wallet/vault/pin/verify', [VaultPinController::class, 'verify'])->middleware('throttle:60,1')->name('api.vault.pin.verify');
Route::post('/wallet/vault/pin/set', [VaultPinController::class, 'set'])->middleware('throttle:20,1')->name('api.vault.pin.set');
// User Bonuses (Local)
Route::get('/user/bonuses', [\App\Http\Controllers\UserBonusController::class, 'index'])->name('api.user.bonuses');
// Explicit API Routes (formerly via Proxy)
Route::get('/chat', [\App\Http\Controllers\ChatController::class, 'index'])->middleware('throttle:600,1')->name('api.chat.index');
Route::post('/chat', [\App\Http\Controllers\ChatController::class, 'store'])->middleware('throttle:60,1')->name('api.chat.store');
Route::post('/chat/{id}/react', [\App\Http\Controllers\ChatController::class, 'react'])->middleware('throttle:120,1')->name('api.chat.react');
Route::post('/chat/{id}/report', [\App\Http\Controllers\ChatController::class, 'report'])->whereNumber('id')->middleware('throttle:20,1')->name('api.chat.report');
Route::delete('/chat/{id}', [\App\Http\Controllers\ChatController::class, 'destroy'])->whereNumber('id')->name('api.chat.destroy');
Route::post('/promos/apply', [\App\Http\Controllers\PromoController::class, 'apply'])->middleware('throttle:10,1')->name('api.promos.apply');
Route::get('/users/search', [\App\Http\Controllers\SocialController::class, 'search'])->middleware('throttle:60,1')->name('api.users.search');
Route::get('/bonuses/app', [\App\Http\Controllers\BonusesController::class, 'appIndex'])->name('api.bonuses.app');
// Favorites
Route::get('/favorites', [\App\Http\Controllers\FavoriteController::class, 'index'])->middleware('throttle:60,1')->name('api.favorites.index');
Route::post('/favorites', [\App\Http\Controllers\FavoriteController::class, 'store'])->middleware('throttle:30,1')->name('api.favorites.store');
Route::delete('/favorites/{slug}', [\App\Http\Controllers\FavoriteController::class, 'destroy'])->middleware('throttle:30,1')->name('api.favorites.destroy');
// Recently Played
Route::get('/recently-played', [\App\Http\Controllers\RecentlyPlayedController::class, 'index'])->middleware('throttle:60,1')->name('api.recently-played');
// Games catalog — used by SearchModal and Dashboard (provider: slug, name, image, type)
Route::get('/games', function () {
$baseUrl = rtrim((string) config('games.game_base_url', config('app.url')), '/');
$games = collect(config('games.catalog', []))->values()->map(fn ($g) => [
'slug' => $g['slug'],
'name' => $g['name'],
'provider' => 'BetiX',
'image' => "{$baseUrl}/assets/games/{$g['slug']}.png",
'type' => 'original',
]);
return response()->json(['games' => $games]);
})->middleware('throttle:120,1')->name('api.games');
// BetiX Originals: game catalog (public, no external backend required)
Route::get('/originals', function () {
$baseUrl = rtrim((string) config('games.game_base_url', config('app.url')), '/');
$catalog = collect(config('games.catalog', []))->values()->map(fn ($g) => [
'id' => $g['id'],
'slug' => $g['slug'],
'name' => $g['name'],
'rtp' => $g['rtp'],
'volatility' => $g['volatility'] ?? null,
'min_bet' => $g['min_bet'] ?? null,
'max_bet' => $g['max_bet'] ?? null,
'thumbnail_url' => "{$baseUrl}/assets/games/{$g['slug']}.png",
'image' => "{$baseUrl}/assets/games/{$g['slug']}.png",
'provider' => 'BetiX',
'tag' => 'ORIGINAL',
]);
return response()->json(['games' => $catalog]);
})->middleware('throttle:60,1')->name('api.originals');
// BetiX Originals: launch a real session via BetiX API
Route::post('/originals/launch', function (\Illuminate\Http\Request $request) {
$request->validate([
'game' => ['required', 'string', 'in:dice,crash,mines,plinko'],
]);
/** @var \App\Models\User $user */
$user = \Illuminate\Support\Facades\Auth::user();
$client = app(\App\Services\BetiXClient::class);
$balance = (float) $user->balance;
if ($balance <= 0) {
return response()->json(['error' => 'Kein Guthaben vorhanden. Bitte lade dein Konto auf.'], 422);
}
$payload = [
'license_key' => config('services.betix.key'),
'player_id' => (string) $user->id,
'balance' => $balance,
'currency' => 'EUR',
'game' => $request->input('game'),
'session_timeout_seconds' => 14400,
];
\Illuminate\Support\Facades\Log::debug('[BetiX] launch attempt', [
'game' => $request->input('game'),
'player_id' => (string) $user->id,
'balance' => (float) $user->balance,
'api_url' => config('services.betix.url'),
'key_prefix' => substr((string) config('services.betix.key'), 0, 12) . '...',
]);
// Reuse an existing valid session to skip the BetiX API call (speeds up reload)
$gameBase = rtrim((string) config('games.game_base_url', 'http://localhost:3100'), '/');
$existing = \App\Models\OperatorSession::where('player_id', (string) $user->id)
->where('game_slug', $request->input('game'))
->where('status', 'active')
->where('expires_at', '>', now())
->latest()
->first();
if ($existing) {
\Illuminate\Support\Facades\Log::debug('[BetiX] reusing existing session', [
'session_token' => substr($existing->session_token, 0, 16) . '...',
'game' => $existing->game_slug,
'expires_at' => $existing->expires_at->toIso8601String(),
]);
$launchUrl = "{$gameBase}/{$request->input('game')}?session={$existing->session_token}";
return response()->json([
'launch_url' => $launchUrl,
'session_token' => $existing->session_token,
'server_seed_hash' => $existing->server_seed_hash,
]);
}
// No valid session — expire stale ones and create a fresh session
\App\Models\OperatorSession::where('player_id', (string) $user->id)
->where('game_slug', $request->input('game'))
->where('status', 'active')
->update(['status' => 'expired']);
// Prepare the self-casino record (needed for both remote and local fallback)
$selfKey = 'betix.self.' . config('app.key');
$casino = \App\Models\OperatorCasino::firstOrCreate(
['license_key_hash' => hash('sha256', $selfKey)],
['name' => 'BetiX Self', 'status' => 'active']
);
// Try the external BetiX API; fall back to local session generation if unreachable
$result = null;
try {
$result = $client->launch($payload);
\Illuminate\Support\Facades\Log::debug('[BetiX] launch success (remote)', [
'launch_url' => $result['launch_url'] ?? '(missing)',
'session_token' => isset($result['session_token']) ? substr($result['session_token'], 0, 16) . '...' : '(missing)',
]);
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::warning('[BetiX] remote launch unavailable — using local session fallback', [
'error' => $e->getMessage(),
]);
}
if ($result === null) {
// Local fallback: generate a provably-fair session without the external API
$serverSeed = bin2hex(random_bytes(32));
$serverSeedHash = hash('sha256', $serverSeed);
$token = (string) \Illuminate\Support\Str::uuid();
$expiresAt = now()->addHours(4);
\App\Models\OperatorSession::create([
'session_token' => $token,
'operator_casino_id' => $casino->id,
'player_id' => (string) $user->id,
'game_slug' => $request->input('game'),
'currency' => 'EUR',
'start_balance' => $balance,
'current_balance' => $balance,
'server_seed' => encrypt($serverSeed),
'server_seed_hash' => $serverSeedHash,
'status' => 'active',
'expires_at' => $expiresAt,
]);
$launchUrl = "{$gameBase}/{$request->input('game')}?session={$token}";
return response()->json([
'launch_url' => $launchUrl,
'session_token' => $token,
'server_seed_hash' => $serverSeedHash,
]);
}
try {
\App\Models\OperatorSession::create([
'session_token' => $result['session_token'],
'operator_casino_id' => $casino->id,
'player_id' => (string) $user->id,
'game_slug' => $request->input('game'),
'currency' => 'EUR',
'start_balance' => (float) $user->balance,
'current_balance' => (float) $user->balance,
'server_seed' => encrypt($result['session_token']),
'server_seed_hash' => $result['server_seed_hash'] ?? hash('sha256', $result['session_token']),
'status' => 'active',
'expires_at' => now()->addHours(4),
]);
} catch (\Throwable $e) {
\Illuminate\Support\Facades\Log::error('[BetiX] OperatorSession::create failed', [
'error' => $e->getMessage(),
'file' => $e->getFile() . ':' . $e->getLine(),
]);
// Still return the launch_url — session tracking failure should not block the player
}
// Rewrite launch_url origin to match GAME_BASE_URL (fixes Mixed Content on HTTPS)
$launchUrl = preg_replace('#^https?://localhost:3100#', $gameBase, $result['launch_url']);
return response()->json([
'launch_url' => $launchUrl,
'session_token' => $result['session_token'],
'server_seed_hash' => $result['server_seed_hash'] ?? null,
]);
})->middleware(['auth:web', 'throttle:30,1'])->name('api.originals.launch');
});
// Support Chat Routes (must be before the proxy catch-all)
Route::middleware(['auth:web'])->group(function () {
Route::post('/api/support/start', [\App\Http\Controllers\SupportChatController::class, 'start'])->middleware('throttle:30,1')->name('api.support.start');
Route::post('/api/support/message', [\App\Http\Controllers\SupportChatController::class, 'message'])->middleware('throttle:60,1')->name('api.support.message');
Route::get('/api/support/status', [\App\Http\Controllers\SupportChatController::class, 'status'])->middleware('throttle:60,1')->name('api.support.status');
Route::get('/api/support/stream', [\App\Http\Controllers\SupportChatController::class, 'stream'])->name('api.support.stream');
Route::post('/api/support/stop', [\App\Http\Controllers\SupportChatController::class, 'stop'])->middleware('throttle:30,1')->name('api.support.stop');
Route::post('/api/support/handoff', [\App\Http\Controllers\SupportChatController::class, 'handoff'])->middleware('throttle:10,1')->name('api.support.handoff');
Route::post('/api/support/close', [\App\Http\Controllers\SupportChatController::class, 'close'])->middleware('throttle:10,1')->name('api.support.close');
});
// Live wins feed — last 15 positive payouts
Route::get('/api/wins/live', function () {
$wins = \App\Models\GameBet::orderByDesc('created_at')
->where('payout_amount', '>', 0)
->limit(15)
->get(['id', 'user_id', 'game_name', 'payout_amount', 'payout_multiplier', 'currency']);
return response()->json($wins->values()->map(fn ($b) => [
'id' => $b->id,
'user' => 'Player#' . $b->user_id,
'game' => $b->game_name,
'amount' => number_format((float) $b->payout_amount, 4) . ' ' . $b->currency,
'multiplier' => (float) $b->payout_multiplier,
'isWin' => true,
]));
})->middleware('throttle:60,1')->name('api.wins.live');
// Hall of Fame — all-time top wins by multiplier
Route::get('/api/wins/top', function () {
$wins = \App\Models\GameBet::orderByDesc('payout_multiplier')
->where('payout_multiplier', '>', 1)
->limit(5)
->get(['id', 'user_id', 'game_name', 'payout_amount', 'payout_multiplier', 'currency']);
return response()->json($wins->values()->map(fn ($b, $i) => [
'id' => $b->id,
'rank' => $i + 1,
'user' => 'Player#' . $b->user_id,
'game' => $b->game_name,
'amount' => number_format((float) $b->payout_amount, 4) . ' ' . $b->currency,
'multiplier' => number_format((float) $b->payout_multiplier, 2) . 'x',
'image' => null,
]));
})->middleware('throttle:30,1')->name('api.wins.top');
// Trophy Room
Route::get('/trophy', [\App\Http\Controllers\TrophyController::class, 'index'])->middleware('auth:web')->name('trophy');
Route::get('/trophy/{username}', [\App\Http\Controllers\TrophyController::class, 'show'])->name('trophy.user');
Route::get('/dashboard', function () {
$baseUrl = rtrim((string) config('games.game_base_url', config('app.url')), '/');
$games = collect(config('games.catalog', []))->values()->map(fn ($g) => [
'id' => $g['id'],
'slug' => $g['slug'],
'name' => $g['name'],
'rtp' => $g['rtp'],
'image' => "{$baseUrl}/assets/games/{$g['slug']}.png",
'provider' => 'BetiX',
'tag' => 'ORIGINAL',
]);
return Inertia::render('Dashboard', ['initialGames' => $games]);
})->name('dashboard');
Route::get('/bonuses', function () { return Inertia::render('Bonus'); })->name('bonuses');
Route::get('/vip-levels', [VipController::class, 'index'])->name('vip-levels');
Route::get('/self-exclusion', function () { return Inertia::render('responsible/SelfExclusion'); })->name('self-exclusion');
Route::middleware(['auth:web', 'verified'])->group(function () {
Route::get('/wallet', [WalletController::class, 'index'])->name('wallet');
Route::get('/wallet/bets', [WalletController::class, 'bets'])->name('wallet.bets');
Route::post('/vip-levels/claim', [VipController::class, 'claim'])->name('vip.claim');
Route::post('/responsible/limits', function (Request $request) {
return back()->with('success', 'Settings received (Mock).');
})->name('responsible.limits');
// Deposits via NOWPayments (user-authenticated)
Route::get('/wallet/deposits/currencies', [DepositController::class, 'currencies'])
->middleware('throttle:60,1')
->name('wallet.deposits.currencies');
Route::post('/wallet/deposits', [DepositController::class, 'create'])
->middleware('throttle:20,1')
->name('wallet.deposits.create');
Route::get('/wallet/deposits/history', [DepositController::class, 'history'])
->middleware('throttle:60,1')
->name('wallet.deposits.history');
Route::get('/wallet/deposits/{order_id}', [DepositController::class, 'show'])
->middleware('throttle:60,1')
->whereUuid('order_id')
->name('wallet.deposits.show');
Route::delete('/wallet/deposits/{order_id}', [DepositController::class, 'cancel'])
->middleware('throttle:20,1')
->whereUuid('order_id')
->name('wallet.deposits.cancel');
Route::get('/profile', [SocialController::class, 'me'])->name('profile.me');
Route::post('/profile/update', [SocialController::class, 'update'])->name('profile.update');
Route::get('/profile/{id}', [SocialController::class, 'show'])->name('profile.show');
Route::post('/profile/{id}/tip', [SocialController::class, 'tip'])->name('profile.tip');
});
Route::get('/profile/{username}', [SocialController::class, 'show'])->name('profile.show');
Route::post('/profile/update', [SocialController::class, 'update'])->name('social.profile.update');
Route::post('/profile/upload', [SocialController::class, 'uploadImage'])->name('profile.upload');
Route::post('/profile/{id}/like', [SocialController::class, 'like'])->name('profile.like');
Route::post('/profile/{id}/comment', [SocialController::class, 'comment'])->name('profile.comment');
Route::post('/profile/{id}/report', [SocialController::class, 'report'])->name('profile.report');
Route::middleware(['auth:web'])->group(function () {
Route::get('/feedback', [FeedbackController::class, 'showForm'])->name('feedback');
Route::post('/feedback', [FeedbackController::class, 'store'])->name('feedback.store');
});
Route::post('/profile/{id}/tip', [SocialController::class, 'tip'])->middleware('throttle:10,1')->name('profile.tip');
Route::middleware('throttle:20,1')->group(function () {
Route::post('/friends/request', [SocialController::class, 'requestFriend'])->name('friends.request');
Route::post('/friends/{id}/accept', [SocialController::class, 'acceptFriend'])->name('friends.accept');
Route::post('/friends/{id}/decline', [SocialController::class, 'declineFriend'])->name('friends.decline');
});
// Social Hub
Route::get('/social', [SocialController::class, 'hub'])->name('social.hub');
// Guild Chat API
Route::middleware('auth:web')->prefix('api/guild-chat')->group(function () {
Route::get('/me', [\App\Http\Controllers\GuildChatController::class, 'myGuild']);
Route::get('/{guildId}/members', [\App\Http\Controllers\GuildChatController::class, 'members'])->whereNumber('guildId');
Route::get('/{guildId}', [\App\Http\Controllers\GuildChatController::class, 'messages'])->whereNumber('guildId');
Route::post('/{guildId}', [\App\Http\Controllers\GuildChatController::class, 'send'])->whereNumber('guildId');
});
// Direct Messages API (auth-guarded, web session)
Route::middleware('auth:web')->prefix('api/dm')->group(function () {
Route::get('/conversations', [\App\Http\Controllers\DirectMessageController::class, 'conversations']);
Route::get('/friends', [\App\Http\Controllers\DirectMessageController::class, 'friends']);
Route::get('/friends/requests', [\App\Http\Controllers\DirectMessageController::class, 'friendRequests']);
Route::get('/{userId}', [\App\Http\Controllers\DirectMessageController::class, 'messages'])->whereNumber('userId');
Route::post('/{userId}', [\App\Http\Controllers\DirectMessageController::class, 'send'])->whereNumber('userId');
Route::post('/messages/{id}/report', [\App\Http\Controllers\DirectMessageController::class, 'report'])->whereNumber('id');
});
Route::get('/settings', function () {
return Inertia::render('Social/Settings', [
'user' => \Illuminate\Support\Facades\Auth::user(),
]);
})->name('settings');
// Admin Routes (Inertia Pages)
// NOTE: Replacing the old /admin prefix with the new structure
Route::prefix('admin')->middleware(['auth:web'])->group(function () {
Route::get('/casino', [AdminController::class, 'casinoDashboard'])->name('admin.casino');
Route::get('/users', [AdminController::class, 'usersIndex'])->name('admin.users.index');
Route::get('/users/{id}', [AdminController::class, 'userShow'])->name('admin.users.show');
Route::post('/users/{id}', [AdminController::class, 'updateUser'])->name('admin.users.update');
Route::get('/users/{id}/history', [AdminController::class, 'userHistory'])->name('admin.users.history');
Route::get('/chat', [AdminController::class, 'chatIndex'])->name('admin.chat.index');
Route::post('/chat/toggle-ai', [AdminController::class, 'toggleAi'])->name('admin.chat.toggle-ai');
Route::delete('/chat/{id}', [AdminController::class, 'deleteChatMessage'])->name('admin.chat.delete');
// Report management
Route::get('/reports/chat', [AdminController::class, 'chatReports'])->name('admin.reports.chat');
Route::get('/reports/chat/{id}', [AdminController::class, 'chatReportShow'])->whereNumber('id')->name('admin.reports.chat.show');
Route::post('/reports/chat/{id}', [AdminController::class, 'updateChatReport'])->whereNumber('id')->name('admin.reports.chat.update');
Route::post('/reports/chat/{id}/punish', [AdminController::class, 'punishFromChatReport'])->whereNumber('id')->name('admin.reports.chat.punish');
Route::get('/reports/profiles', [AdminController::class, 'profileReports'])->name('admin.reports.profiles');
Route::get('/reports/profiles/{id}', [AdminController::class, 'profileReportShow'])->whereNumber('id')->name('admin.reports.profiles.show');
Route::post('/reports/profiles/{id}', [AdminController::class, 'updateProfileReport'])->whereNumber('id')->name('admin.reports.profiles.update');
Route::post('/reports/profiles/{id}/punish', [AdminController::class, 'punishFromProfileReport'])->whereNumber('id')->name('admin.reports.profiles.punish');
// Feedback management
Route::get('/feedback', [FeedbackController::class, 'adminIndex'])->name('admin.feedback.index');
Route::get('/feedback/{id}', [FeedbackController::class, 'adminShow'])->whereNumber('id')->name('admin.feedback.show');
Route::post('/feedback/{id}', [FeedbackController::class, 'adminUpdate'])->whereNumber('id')->name('admin.feedback.update');
// Restriction management
Route::post('/restrictions/{id}/lift', [AdminController::class, 'liftRestriction'])->whereNumber('id')->name('admin.restrictions.lift');
Route::post('/restrictions/{id}/extend', [AdminController::class, 'extendRestriction'])->whereNumber('id')->name('admin.restrictions.extend');
// Old settings routes
Route::get('/promos', [PromoAdminController::class, 'index'])->name('admin.promos.index');
Route::post('/promos', [PromoAdminController::class, 'store'])->name('admin.promos.store');
Route::patch('/promos/{id}', [PromoAdminController::class, 'update'])->name('admin.promos.update');
Route::get('/support', [SupportAdminController::class, 'index'])->name('admin.support.index');
Route::post('/support/settings', [SupportAdminController::class, 'settings'])->name('admin.support.settings');
Route::post('/support/threads/{thread}/message', [SupportAdminController::class, 'reply'])->name('admin.support.reply');
Route::post('/support/threads/{thread}/close', [SupportAdminController::class, 'close'])->name('admin.support.close');
// Admin Payments Settings (NOWPayments)
Route::get('/payments/settings', [\App\Http\Controllers\Admin\PaymentsSettingsController::class, 'show'])->name('admin.payments.settings');
Route::post('/payments/settings', [\App\Http\Controllers\Admin\PaymentsSettingsController::class, 'save'])->name('admin.payments.settings.save');
Route::post('/payments/test', [\App\Http\Controllers\Admin\PaymentsSettingsController::class, 'test'])->name('admin.payments.test');
// Admin Wallets Settings
Route::get('/wallets/settings', [\App\Http\Controllers\Admin\WalletsAdminController::class, 'show'])->name('admin.wallets.settings');
Route::post('/wallets/settings', [\App\Http\Controllers\Admin\WalletsAdminController::class, 'save'])->name('admin.wallets.settings.save');
// Site & GeoBlock Settings
Route::get('/settings/site', [\App\Http\Controllers\Admin\SiteSettingsController::class, 'show'])->name('admin.settings.site');
Route::post('/settings/site', [\App\Http\Controllers\Admin\SiteSettingsController::class, 'save'])->name('admin.settings.site.save');
Route::get('/settings/geo', [\App\Http\Controllers\Admin\GeoBlockController::class, 'show'])->name('admin.settings.geo');
Route::post('/settings/geo', [\App\Http\Controllers\Admin\GeoBlockController::class, 'save'])->name('admin.settings.geo.save');
});
// Guilds Pages
Route::get('/guilds', [GuildController::class, 'index'])->name('guilds.index');
Route::get('/guilds/top', [GuildController::class, 'top'])->name('guilds.top');
// Guild Actions
Route::post('/guilds', [GuildActionController::class, 'store'])->name('guilds.store');
Route::post('/guilds/join', [GuildActionController::class, 'join'])->name('guilds.join');
Route::post('/guilds/leave', [GuildActionController::class, 'leave'])->name('guilds.leave');
Route::post('/guilds/kick', [GuildActionController::class, 'kick'])->name('guilds.kick');
Route::post('/guilds/invite/regenerate', [GuildActionController::class, 'regenerateInvite'])->name('guilds.invite.regenerate');
Route::post('/guilds/update', [GuildActionController::class, 'update'])->name('guilds.update');
// Game utility: local fallback Originals list (used if provider API is unavailable)
Route::get('/games/list', function () {
return response()->json([
[
'id' => 'plinko',
'slug' => 'plinko',
'name' => 'Plinko',
'provider' => 'BetiX',
'image' => 'https://placehold.co/600x400?text=Plinko',
'tag' => 'ORIGINAL',
'rtp' => 96.0,
],
[
'id' => 'dice',
'slug' => 'dice',
'name' => 'Dice',
'provider' => 'BetiX',
'image' => 'https://placehold.co/600x400?text=Dice',
'tag' => 'ORIGINAL',
'rtp' => 99.0,
],
[
'id' => 'mines',
'slug' => 'mines',
'name' => 'Mines',
'provider' => 'BetiX',
'image' => 'https://placehold.co/600x400?text=Mines',
'tag' => 'ORIGINAL',
'rtp' => 96.0,
],
]);
})->name('games.list');
// Game Play Routes (Inertia Pages)
Route::get('/games/play/{provider}/{slug}', function (string $provider, string $slug) {
$title = ucwords(str_replace(['-', '_'], ' ', $slug));
$game = collect(config('games.catalog', []))->first(fn ($g) => ($g['slug'] ?? '') === $slug);
if ($game) {
$title = $game['name'] ?? $title;
$provider = $game['provider'] ?? $provider;
$description = $game['description'] ?? null;
} else {
$description = null;
}
return Inertia::render('GamePlay', [
'title' => $title,
'slug' => $slug,
'src' => null,
'provider' => $provider,
'description' => $description,
]);
})->name('games.play');
// Legacy single-segment route — redirect to new format
Route::get('/games/play/{slug}', function (string $slug) {
$game = collect(config('games.catalog', []))->first(fn ($g) => ($g['slug'] ?? '') === $slug);
$provider = $game['provider'] ?? 'betix';
return redirect()->route('games.play', [
'provider' => \Illuminate\Support\Str::slug($provider),
'slug' => $slug,
], 301);
})->name('games.play.legacy');
// Optional direct launch via query (for future third-party providers)
Route::get('/games/play', function (Request $request) {
$src = $request->query('src');
return Inertia::render('GamePlay', [
'title' => $request->query('title', 'Game'),
'slug' => null,
'src' => $src,
]);
})->name('games.play.direct');
// Internal embed: secure server-side provider launch (keeps provider origin hidden)
Route::get('/games/embed/{slug}', [EmbedController::class, 'show'])
->middleware('throttle:60,1')
->name('games.embed');
});
Route::post('/locale', [LocaleController::class, 'set'])->middleware('throttle:30,1')->name('locale.set');
require __DIR__.'/settings.php';
// Authenticated API-ish routes for Vault (local, no external API)
// Moved to web.php main group to ensure they match BEFORE the api.proxy catch-all