Files
BetiX/routes/web.php
Dolo 0280278978
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
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

671 lines
38 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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