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

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AppSetting;
use Illuminate\Http\Request;
use Inertia\Inertia;
class GeoBlockController extends Controller
{
private const KEY = 'geo.settings';
private array $defaults = [
'enabled' => false,
'blocked_countries' => [],
'allowed_countries' => [],
'mode' => 'blacklist', // 'blacklist' or 'whitelist'
'vpn_block' => false,
'vpn_provider' => 'none', // 'none', 'ipqualityscore', 'proxycheck'
'vpn_api_key' => '',
'block_message' => 'This service is not available in your region.',
'redirect_url' => '',
];
public function show()
{
$saved = AppSetting::get(self::KEY, []);
$settings = array_merge($this->defaults, is_array($saved) ? $saved : []);
return Inertia::render('Admin/GeoBlock', [
'settings' => $settings,
]);
}
public function save(Request $request)
{
$data = $request->validate([
'enabled' => 'boolean',
'mode' => 'required|in:blacklist,whitelist',
'blocked_countries' => 'array',
'blocked_countries.*' => 'string|size:2',
'allowed_countries' => 'array',
'allowed_countries.*' => 'string|size:2',
'vpn_block' => 'boolean',
'vpn_provider' => 'required|in:none,ipqualityscore,proxycheck',
'vpn_api_key' => 'nullable|string|max:200',
'block_message' => 'required|string|max:500',
'redirect_url' => 'nullable|url|max:500',
]);
AppSetting::put(self::KEY, $data);
return back()->with('success', 'GeoBlock settings saved.');
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AppSetting;
use App\Services\DepositService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Inertia\Inertia;
class PaymentsSettingsController extends Controller
{
public function __construct(private readonly DepositService $deposits)
{
}
/**
* GET /admin/payments/settings
*/
public function show()
{
$user = Auth::user();
abort_unless($user && in_array(strtolower((string) $user->role), ['admin', 'owner']), 403);
$settings = $this->deposits->getSettings();
return Inertia::render('Admin/PaymentsSettings', [
'settings' => $settings,
'defaults' => [
'commonCurrencies' => ['BTC','ETH','LTC','SOL','USDT_ERC20','USDT_TRC20','BCH','DOGE'],
'modes' => ['live','sandbox'],
'addressModes' => ['per_payment','per_user'],
],
]);
}
/**
* POST /admin/payments/settings
*/
public function save(Request $request)
{
$user = Auth::user();
abort_unless($user && in_array(strtolower((string) $user->role), ['admin', 'owner']), 403);
$data = $request->validate([
'mode' => ['required','in:live,sandbox'],
'api_key' => ['nullable','string','max:200'],
'ipn_secret' => ['nullable','string','max:200'],
'enabled_currencies' => ['required','array','min:1'],
'enabled_currencies.*' => ['string','max:32'],
'global_min_usd' => ['required','numeric','min:0'],
'global_max_usd' => ['required','numeric','gt:global_min_usd'],
'btx_per_usd' => ['required','numeric','min:0.00000001'],
'per_currency_overrides' => ['sometimes','array'],
'per_currency_overrides.*.min_usd' => ['nullable','numeric','min:0'],
'per_currency_overrides.*.max_usd' => ['nullable','numeric'],
'per_currency_overrides.*.btx_per_usd' => ['nullable','numeric','min:0.00000001'],
'success_url' => ['required','string','max:255'],
'cancel_url' => ['required','string','max:255'],
'address_mode' => ['required','in:per_payment,per_user'],
]);
// Normalize overrides structure as map keyed by currency
$overrides = [];
if (!empty($data['per_currency_overrides']) && is_array($data['per_currency_overrides'])) {
foreach ($data['per_currency_overrides'] as $cur => $vals) {
if (is_array($vals)) {
$entry = [];
if (array_key_exists('min_usd', $vals) && $vals['min_usd'] !== null) $entry['min_usd'] = (float) $vals['min_usd'];
if (array_key_exists('max_usd', $vals) && $vals['max_usd'] !== null) $entry['max_usd'] = (float) $vals['max_usd'];
if (array_key_exists('btx_per_usd', $vals) && $vals['btx_per_usd'] !== null) $entry['btx_per_usd'] = (float) $vals['btx_per_usd'];
if (!empty($entry)) {
$overrides[strtoupper($cur)] = $entry;
}
}
}
}
// Preserve existing api_key/ipn_secret if not re-submitted (masked fields)
$existing = AppSetting::get('payments.nowpayments', []);
$apiKey = $data['api_key'] ?? null;
$ipnSecret = $data['ipn_secret'] ?? null;
$payload = [
'mode' => $data['mode'],
'api_key' => $apiKey ?: ($existing['api_key'] ?? ''),
'ipn_secret' => $ipnSecret ?: ($existing['ipn_secret'] ?? ''),
'enabled_currencies' => array_values(array_map('strtoupper', $data['enabled_currencies'])),
'global_min_usd' => (float) $data['global_min_usd'],
'global_max_usd' => (float) $data['global_max_usd'],
'btx_per_usd' => (float) $data['btx_per_usd'],
'per_currency_overrides' => $overrides,
'success_url' => (string) $data['success_url'],
'cancel_url' => (string) $data['cancel_url'],
'address_mode' => (string) $data['address_mode'],
];
AppSetting::put('payments.nowpayments', $payload);
return back()->with('success', 'Payment settings saved.');
}
/**
* POST /admin/payments/test
*/
public function test(Request $request)
{
$user = Auth::user();
abort_unless($user && in_array(strtolower((string) $user->role), ['admin', 'owner']), 403);
$data = $request->validate(['api_key' => 'required|string|max:200']);
try {
$res = Http::timeout(8)->withHeaders([
'x-api-key' => $data['api_key'],
'Accept' => 'application/json',
])->get('https://api.nowpayments.io/v1/status');
if ($res->ok()) {
return response()->json(['ok' => true, 'message' => 'Verbindung erfolgreich! NOWPayments API erreichbar.']);
}
return response()->json(['ok' => false, 'message' => 'API antwortet mit Status ' . $res->status() . '. API Key prüfen.'], 422);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Verbindung fehlgeschlagen: ' . $e->getMessage()], 422);
}
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Concerns\ProxiesBackend;
use App\Http\Controllers\Controller;
use App\Services\BackendHttpClient;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
use Inertia\Response;
class PromoAdminController extends Controller
{
use ProxiesBackend;
public function __construct(private readonly BackendHttpClient $client)
{
}
private function assertAdmin(): void
{
if (!Auth::check() || strtolower((string) Auth::user()->role) !== 'admin') {
abort(403, 'Nur für Admins');
}
}
/**
* Show simple admin page to manage promos (data from upstream)
*/
public function index(Request $request): Response
{
$this->assertAdmin();
$promos = [];
try {
$res = $this->client->get($request, '/admin/promos', ['per_page' => 20], retry: true);
if ($res->successful()) {
$j = $res->json() ?: [];
$promos = $j['data'] ?? $j['promos'] ?? $j;
}
} catch (\Throwable $e) {
// show empty list with error message on page via flash if desired
}
return Inertia::render('Admin/Promos', [
'promos' => $promos,
]);
}
/**
* Create a new promo via upstream
*/
public function store(Request $request)
{
$this->assertAdmin();
$data = $this->validateData($request);
$data['code'] = strtoupper(trim($data['code']));
$data['is_active'] = $data['is_active'] ?? true;
try {
$res = $this->client->post($request, '/admin/promos', $data);
if ($res->successful()) {
return back()->with('success', 'Promo erstellt.');
}
if ($res->clientError()) {
$msg = data_get($res->json(), 'message', 'Ungültige Eingabe');
return back()->withErrors(['promo' => $msg]);
}
if ($res->serverError()) {
return back()->withErrors(['promo' => 'Service temporär nicht verfügbar']);
}
return back()->withErrors(['promo' => 'API Server nicht erreichbar']);
} catch (\Throwable $e) {
return back()->withErrors(['promo' => 'API Server nicht erreichbar']);
}
}
/**
* Update an existing promo via upstream
*/
public function update(Request $request, int $id)
{
$this->assertAdmin();
$data = $this->validateData($request, $id);
if (isset($data['code'])) {
$data['code'] = strtoupper(trim($data['code']));
}
try {
$res = $this->client->patch($request, "/admin/promos/{$id}", $data);
if ($res->successful()) {
return back()->with('success', 'Promo aktualisiert.');
}
if ($res->clientError()) {
$msg = data_get($res->json(), 'message', 'Ungültige Eingabe');
return back()->withErrors(['promo' => $msg]);
}
if ($res->serverError()) {
return back()->withErrors(['promo' => 'Service temporär nicht verfügbar']);
}
return back()->withErrors(['promo' => 'API Server nicht erreichbar']);
} catch (\Throwable $e) {
return back()->withErrors(['promo' => 'API Server nicht erreichbar']);
}
}
private function validateData(Request $request, ?int $ignoreId = null): array
{
$isUpdate = $request->isMethod('patch') || $request->isMethod('put');
$rules = [
'code' => [($isUpdate ? 'sometimes' : 'required'), 'string', 'max:64'],
'description' => [($isUpdate ? 'sometimes' : 'nullable'), 'nullable', 'string', 'max:255'],
'bonus_amount' => [($isUpdate ? 'sometimes' : 'required'), 'numeric', 'min:0'],
'wager_multiplier' => [($isUpdate ? 'sometimes' : 'required'), 'integer', 'min:0', 'max:1000'],
'per_user_limit' => [($isUpdate ? 'sometimes' : 'required'), 'integer', 'min:1', 'max:1000'],
'global_limit' => [($isUpdate ? 'sometimes' : 'nullable'), 'nullable', 'integer', 'min:1', 'max:1000000'],
'starts_at' => [($isUpdate ? 'sometimes' : 'nullable'), 'nullable', 'date'],
'ends_at' => [($isUpdate ? 'sometimes' : 'nullable'), 'nullable', 'date', 'after_or_equal:starts_at'],
'min_deposit' => [($isUpdate ? 'sometimes' : 'nullable'), 'nullable', 'numeric', 'min:0'],
'bonus_expires_days' => [($isUpdate ? 'sometimes' : 'nullable'), 'nullable', 'integer', 'min:1', 'max:365'],
'is_active' => [($isUpdate ? 'sometimes' : 'nullable'), 'boolean'],
];
return $request->validate($rules);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AppSetting;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SiteSettingsController extends Controller
{
private const KEY = 'site.settings';
private array $defaults = [
'site_name' => 'BetiX Casino',
'site_tagline' => 'Play. Win. Repeat.',
'primary_color' => '#df006a',
'logo_url' => '',
'favicon_url' => '',
'maintenance_mode' => false,
'registration_open' => true,
'min_deposit_usd' => 10,
'max_deposit_usd' => 50000,
'min_withdrawal_usd' => 20,
'max_withdrawal_usd' => 100000,
'max_bet_usd' => 5000,
'house_edge_percent' => 1.0,
'footer_text' => '',
'support_email' => '',
'terms_url' => '/terms',
'privacy_url' => '/privacy',
'currency_symbol' => 'BTX',
];
public function show()
{
$saved = AppSetting::get(self::KEY, []);
$settings = array_merge($this->defaults, is_array($saved) ? $saved : []);
return Inertia::render('Admin/SiteSettings', [
'settings' => $settings,
]);
}
public function save(Request $request)
{
// Normalize empty strings to null so URL/email validation doesn't fail on blank fields
foreach (['logo_url', 'favicon_url', 'terms_url', 'privacy_url', 'support_email', 'site_tagline', 'footer_text'] as $field) {
if ($request->input($field) === '') {
$request->merge([$field => null]);
}
}
$data = $request->validate([
'site_name' => 'required|string|max:100',
'site_tagline' => 'nullable|string|max:200',
'primary_color' => 'required|regex:/^#[0-9a-fA-F]{6}$/',
'logo_url' => 'nullable|url|max:500',
'favicon_url' => 'nullable|url|max:500',
'maintenance_mode' => 'boolean',
'registration_open' => 'boolean',
'min_deposit_usd' => 'required|numeric|min:0',
'max_deposit_usd' => 'required|numeric|min:0',
'min_withdrawal_usd' => 'required|numeric|min:0',
'max_withdrawal_usd' => 'required|numeric|min:0',
'max_bet_usd' => 'required|numeric|min:0',
'house_edge_percent' => 'required|numeric|min:0|max:100',
'footer_text' => 'nullable|string|max:1000',
'support_email' => 'nullable|email|max:200',
'terms_url' => 'nullable|string|max:500',
'privacy_url' => 'nullable|string|max:500',
'currency_symbol' => 'required|string|max:10',
]);
AppSetting::put(self::KEY, $data);
return back()->with('success', 'Site settings saved.');
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
class SupportAdminController extends Controller
{
private function assertAdmin(): void
{
if (!Auth::check() || strtolower((string) Auth::user()->role) !== 'admin') {
abort(403, 'Nur für Admins');
}
}
private function ollamaStatus(): array
{
$host = rtrim(env('OLLAMA_HOST', 'http://127.0.0.1:11434'), '/');
$model = env('OLLAMA_MODEL', 'llama3');
try {
$res = Http::timeout(2)->get($host . '/api/tags');
return ['healthy' => $res->ok(), 'host' => $host, 'model' => $model, 'error' => $res->ok() ? null : 'Keine Verbindung'];
} catch (\Throwable $e) {
return ['healthy' => false, 'host' => $host, 'model' => $model, 'error' => $e->getMessage()];
}
}
private function getThreads(): array
{
$index = cache()->get('support_threads_index', []);
$threads = [];
foreach (array_reverse($index) as $row) {
$full = cache()->get('support_threads:' . $row['id']);
if (is_array($full)) {
$threads[] = $full;
} else {
$threads[] = $row;
}
}
return $threads;
}
public function index(Request $request): Response
{
$this->assertAdmin();
$enabled = (bool) (cache()->get('support_chat_enabled') ?? config('app.support_chat_enabled', true));
return Inertia::render('Admin/Support', [
'enabled' => $enabled,
'threads' => $this->getThreads(),
'ollama' => $this->ollamaStatus(),
]);
}
public function settings(Request $request)
{
$this->assertAdmin();
$data = $request->validate(['enabled' => 'required|boolean']);
cache()->put('support_chat_enabled', (bool) $data['enabled'], now()->addYear());
return back()->with('success', 'Support-Chat Einstellungen gespeichert.');
}
public function reply(Request $request, string $thread)
{
$this->assertAdmin();
$data = $request->validate(['text' => 'required|string|min:1|max:1000']);
$record = cache()->get('support_threads:' . $thread);
if (!is_array($record)) {
return back()->withErrors(['text' => 'Thread nicht gefunden.']);
}
$record['messages'][] = [
'id' => Str::uuid()->toString(),
'sender' => 'agent',
'body' => $data['text'],
'at' => now()->toIso8601String(),
];
$record['status'] = 'agent';
$record['updated_at'] = now()->toIso8601String();
cache()->put('support_threads:' . $thread, $record, now()->addDay());
// Update index entry
$index = cache()->get('support_threads_index', []);
foreach ($index as &$row) {
if (($row['id'] ?? null) === $thread) {
$row['status'] = 'agent';
$row['updated_at'] = $record['updated_at'];
break;
}
}
cache()->put('support_threads_index', $index, now()->addDay());
return back()->with('success', 'Nachricht gesendet.');
}
public function close(Request $request, string $thread)
{
$this->assertAdmin();
$record = cache()->get('support_threads:' . $thread);
if (is_array($record)) {
$record['status'] = 'closed';
$record['updated_at'] = now()->toIso8601String();
cache()->put('support_threads:' . $thread, $record, now()->addDay());
}
$index = cache()->get('support_threads_index', []);
foreach ($index as &$row) {
if (($row['id'] ?? null) === $thread) {
$row['status'] = 'closed';
break;
}
}
cache()->put('support_threads_index', $index, now()->addDay());
return back()->with('success', 'Chat geschlossen.');
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\AppSetting;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Inertia\Inertia;
class WalletsAdminController extends Controller
{
/**
* GET /admin/wallets/settings Wallet/Vault policies and limits.
*/
public function show()
{
$user = Auth::user();
abort_unless($user && ($user->role === 'Admin' || $user->role === 'Owner'), 403);
$defaults = [
'pin_max_attempts' => 5,
'pin_lock_minutes' => 15,
'min_tx_btx' => 0.0001,
'max_tx_btx' => 100000,
'daily_max_btx' => 100000,
'actions_per_minute' => 20,
'reason_required' => true,
];
$settings = AppSetting::get('wallet.settings', $defaults) ?: $defaults;
// Ensure defaults filled
$settings = array_replace($defaults, is_array($settings) ? $settings : []);
return Inertia::render('Admin/WalletsSettings', [
'settings' => $settings,
'defaults' => $defaults,
]);
}
/**
* POST /admin/wallets/settings Save policies and limits.
*/
public function save(Request $request)
{
$user = Auth::user();
abort_unless($user && ($user->role === 'Admin' || $user->role === 'Owner'), 403);
$data = $request->validate([
'pin_max_attempts' => ['required','integer','min:1','max:20'],
'pin_lock_minutes' => ['required','integer','min:1','max:1440'],
'min_tx_btx' => ['required','numeric','min:0'],
'max_tx_btx' => ['required','numeric','gt:min_tx_btx'],
'daily_max_btx' => ['required','numeric','gte:max_tx_btx'],
'actions_per_minute' => ['required','integer','min:1','max:600'],
'reason_required' => ['required','boolean'],
]);
// Normalize numeric precision (BTX uses 4 decimals commonly)
$payload = [
'pin_max_attempts' => (int) $data['pin_max_attempts'],
'pin_lock_minutes' => (int) $data['pin_lock_minutes'],
'min_tx_btx' => round((float) $data['min_tx_btx'], 4),
'max_tx_btx' => round((float) $data['max_tx_btx'], 4),
'daily_max_btx' => round((float) $data['daily_max_btx'], 4),
'actions_per_minute' => (int) $data['actions_per_minute'],
'reason_required' => (bool) $data['reason_required'],
];
AppSetting::put('wallet.settings', $payload);
return back()->with('success', 'Wallet settings saved.');
}
}