diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 473b764..05c0329 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -9,7 +9,6 @@ use App\Models\Order; use App\Models\Price; use App\Models\Product; use App\Models\ProfitConversion; -use App\Models\TransactionsAdmin; use App\Models\Withdraw; use App\Services\BalanceReportService; use App\Services\ConfigService; @@ -28,7 +27,6 @@ class DashboardController extends Controller public function dashboardMaster(Request $request) { $tallaPrice = Price::latest()->first(); -// $talla->update_date = Verta::instance(Carbon::parse($talla->updated_at))->format('Y/n/j-H:i'); $new_customer_major = Customer::where('type',1)->whereDate('created_at','<=',now())->whereDate('created_at','>=',now()->subMonths(3)) ->count(); $new_customer_single = Customer::where('type',2)->whereDate('created_at','<=',now())->whereDate('created_at','>=',now()->subMonths(3)) @@ -37,33 +35,39 @@ class DashboardController extends Controller $all_customer_single = Customer::where('type',2)->count(); $archive_customer_major = Customer::where('type',1)->where('is_archive',true)->count(); $archive_customer_single = Customer::where('type',2)->where('is_archive',true)->count(); - $all_gold = Order::where('direction',2)->where('type',1)->sum('amount');// جمع طلاهایی که از مشتری خریدیم - $all_gold_single = Order::where('direction',2)->where('type',2)->sum('amount');// جمع طلاهایی که از مشتری خریدیم - $await_list = Order::where('direction',1)->where('sts',1)->get(); - $best_customer_major=Order::with('customer')->where('type',1)->where('sts',2)->select('customer_id',DB::raw('sum(amount) as amount')) - ->groupBy('customer_id')->limit(10)->get(); - $best_customer_single=Wallet::with('customer')->whereNull('fund_id')->where('owner_type',2)->orderBy('gold','desc')->limit(10)->get(); + $all_gold = Order::where('direction',2)->where('type',1)->sum('amount'); + $all_gold_single = Order::where('direction',2)->where('type',2)->sum('amount'); + $await_list = Order::query() + ->where('direction', 1) + ->where('sts', 1) + ->select(['id', 'customer_id', 'amount', 'total_price', 'sts', 'direction', 'type', 'created_at']) + ->with(['customer:id,name,mobile']) + ->orderByDesc('id') + ->limit(50) + ->get(); + $best_customer_major = Order::with('customer:id,name,mobile') + ->where('type', 1) + ->where('sts', 2) + ->select('customer_id', DB::raw('sum(amount) as amount')) + ->groupBy('customer_id') + ->orderByDesc('amount') + ->limit(10) + ->get(); + $best_customer_single = Wallet::with('customer:id,name,mobile') + ->whereNull('fund_id') + ->where('owner_type', 2) + ->orderByDesc('gold') + ->limit(10) + ->get(['id', 'owner', 'owner_type', 'gold', 'fund_id']); - - // GT $GT_all_customers = Customer::where('type',2)->where('is_archive',false)->count(); - $GT_no_login_customers = Customer::where('type',2)->where('is_archive',false)->where('last_seen_at',null)->count(); + $GT_no_login_customers = Customer::where('type',2)->where('is_archive',false)->whereNull('last_seen_at')->count(); $GT_no_valid_customers = Customer::where('type',2)->where('is_archive',false)->where('is_legal',0)->count(); - $GT_no_valid_customers_data = Customer::where('type',2)->where('is_archive',false)->where('is_legal',0)->get(['id','name','mobile']); $GT_active_customers = Customer::where('type', 2) ->where('is_archive', false) - ->whereIn('id', function($query) { - $query->select('owner') - ->from('wallets'); + ->whereIn('id', function ($query) { + $query->select('owner')->from('wallets'); })->count(); - $GT_active_customers_data = Customer::where('type', 2) - ->where('is_archive', false) - ->whereIn('id', function($query) { - $query->select('owner') - ->from('wallets'); - })->get(['id','name','mobile']); - - $transactions_admins = TransactionsAdmin::with(['withdraw','order',"user",'customerTo','customer'])->get(); $startDate = $request->startDateProduct ? Carbon::parse($request->startDateProduct)->startOfDay() @@ -72,23 +76,32 @@ class DashboardController extends Controller ? Carbon::parse($request->endDateProduct)->endOfDay() : now()->endOfDay(); - $products_report = Product::with(['limitedTransactions' => function ($query) use ($startDate, $endDate) { - $query->whereBetween('created_at', [$startDate, $endDate]); - }]) - ->whereHas('limitedTransactions', function ($query) use ($startDate, $endDate) { - $query->whereBetween('created_at', [$startDate, $endDate]); - }) + // Aggregation در SQL — لود هزاران ردیف transaction باعث OOM می‌شد + $productAgg = DB::table('transactions') + ->select( + 'product_id', + DB::raw('SUM(CASE WHEN gold > 0 THEN gold ELSE 0 END) as transaction_sum_gold_buy'), + DB::raw('SUM(CASE WHEN gold < 0 THEN gold ELSE 0 END) as total_negative_gold_sell'), + DB::raw('SUM(gold) as total_gold') + ) + ->whereNotNull('product_id') + ->whereBetween('created_at', [$startDate, $endDate]) + ->groupBy('product_id') ->get(); - $products_report->each(function ($product) { - $product->transaction_sum_gold_buy = $product->limitedTransactions - ->where('gold', '>', 0) - ->sum('gold'); - $product->total_negative_gold_sell = $product->limitedTransactions - ->where('gold', '<', 0) - ->sum('gold'); - $product->total_gold = $product->limitedTransactions->sum('gold'); - }); + $productTitles = Product::query() + ->whereIn('id', $productAgg->pluck('product_id')->filter()->all()) + ->pluck('title', 'id'); + + $products_report = $productAgg->map(function ($row) use ($productTitles) { + return [ + 'id' => (int) $row->product_id, + 'title' => (string) ($productTitles[$row->product_id] ?? ''), + 'transaction_sum_gold_buy' => (float) $row->transaction_sum_gold_buy, + 'total_negative_gold_sell' => (float) $row->total_negative_gold_sell, + 'total_gold' => (float) $row->total_gold, + ]; + })->values(); $gramPrice = (int) ($tallaPrice->price ?? 0); $FPrices = []; @@ -113,11 +126,12 @@ class DashboardController extends Controller 'GT_all_customers'=>$GT_all_customers, 'GT_no_login_customers'=>$GT_no_login_customers, 'GT_no_valid_customers'=>$GT_no_valid_customers, - 'GT_no_valid_customers_data'=>$GT_no_valid_customers_data, + // لیست کامل مشتریان/تراکنش‌ها دیگر برنمی‌گردد (OOM روی JsonResponse) + 'GT_no_valid_customers_data'=>[], 'GT_active_customers'=>$GT_active_customers, - 'GT_active_customers_data'=>$GT_active_customers_data, + 'GT_active_customers_data'=>[], 'GT_deActive_customers'=>$GT_all_customers-$GT_active_customers, - 'GT_transactions_admins'=>$transactions_admins, + 'GT_transactions_admins'=>[], 'GT_products_report' => $products_report, 'update_date' => now()->timezone('Asia/Tehran')->format('Y-m-d H:i:s'), 'pricem' => round($gramPrice * 4.331802 / 10000) * 10000, @@ -135,7 +149,6 @@ class DashboardController extends Controller 'await_order'=>$await_list, 'best_customer'=>$best_customer_major, 'best_customer_single'=>$best_customer_single, - ]); } diff --git a/app/Http/Middleware/EnsureSuperAdminRemoteToken.php b/app/Http/Middleware/EnsureSuperAdminRemoteToken.php index 93af0e2..8f68544 100644 --- a/app/Http/Middleware/EnsureSuperAdminRemoteToken.php +++ b/app/Http/Middleware/EnsureSuperAdminRemoteToken.php @@ -6,29 +6,138 @@ use Closure; use Illuminate\Http\Request; /** - * Bearer token must match SUPERADMIN_REMOTE_TOKEN (same as SuperAdmin Tenant.remoteApiToken). + * Auth for SuperAdmin → product (kill-switch / super settings): + * 1) Authorization Bearer matching SUPERADMIN_REMOTE_TOKEN + * OR SUPERADMIN_SMS_SHARED_SECRET (CDN-safe; same secret as SMS) + * 2) Optional: shared HMAC headers X-Abshode-* (may be stripped by CDN) */ class EnsureSuperAdminRemoteToken { + private const MAX_SKEW_SECONDS = 300; + + private const CANONICAL_PATH = '/api/super/settings'; + public function handle(Request $request, Closure $next) { - $expected = trim((string) config('services.superadmin.remote_token', '')); - if ($expected === '') { + if ($this->authByBearer($request)) { + return $next($request); + } + + if ($this->authByHmac($request)) { + return $next($request); + } + + $hasSecret = $this->sharedSecret() !== ''; + $hasToken = $this->bearerToken() !== ''; + if (! $hasSecret && ! $hasToken) { return response()->json([ - 'message' => 'SUPERADMIN_REMOTE_TOKEN is not configured', + 'message' => 'SUPERADMIN_SMS_SHARED_SECRET or SUPERADMIN_REMOTE_TOKEN is not configured', ], 503); } + return response()->json(['message' => 'Unauthorized'], 401); + } + + private function authByHmac(Request $request): bool + { + $secret = $this->sharedSecret(); + if ($secret === '') { + return false; + } + + $domainHeader = trim((string) $request->header('X-Abshode-Domain', '')); + $tsRaw = trim((string) $request->header('X-Abshode-Timestamp', '')); + $sigRaw = trim((string) $request->header('X-Abshode-Signature', '')); + if ($domainHeader === '' || $tsRaw === '' || $sigRaw === '') { + return false; + } + + if (! ctype_digit($tsRaw)) { + return false; + } + $timestamp = (int) $tsRaw; + if (abs(time() - $timestamp) > self::MAX_SKEW_SECONDS) { + return false; + } + + $domain = $this->normalizeHost($domainHeader); + if ($domain === '' || ! $this->domainAllowed($domain, $request)) { + return false; + } + + $method = strtoupper($request->getMethod()); + $payload = $timestamp.'.'.$method.'.'.self::CANONICAL_PATH.'.'.$domain; + $expected = hash_hmac('sha256', $payload, $secret); + + return hash_equals(strtolower($expected), strtolower($sigRaw)); + } + + private function authByBearer(Request $request): bool + { $auth = (string) $request->header('Authorization', ''); - $token = ''; - if (preg_match('/^\s*Bearer\s+(.+)\s*$/i', $auth, $m)) { - $token = trim($m[1]); + if (! preg_match('/^\s*Bearer\s+(.+)\s*$/i', $auth, $m)) { + return false; } - if ($token === '' || ! hash_equals($expected, $token)) { - return response()->json(['message' => 'Unauthorized'], 401); + $token = trim($m[1]); + if ($token === '') { + return false; } - return $next($request); + foreach ([$this->bearerToken(), $this->sharedSecret()] as $expected) { + if ($expected !== '' && hash_equals($expected, $token)) { + return true; + } + } + + return false; + } + + private function domainAllowed(string $domain, Request $request): bool + { + $candidates = [ + $this->configuredDomain(), + $this->normalizeHost((string) $request->getHost()), + ]; + + foreach ($candidates as $candidate) { + if ($candidate !== '' && hash_equals($candidate, $domain)) { + return true; + } + } + + return false; + } + + private function sharedSecret(): string + { + return trim((string) config('services.superadmin.sms_shared_secret', '')); + } + + private function bearerToken(): string + { + return trim((string) config('services.superadmin.remote_token', '')); + } + + private function configuredDomain(): string + { + $override = trim((string) config('services.superadmin.sms_domain', '')); + if ($override !== '') { + return $this->normalizeHost($override); + } + + $fromApp = parse_url((string) config('app.url'), PHP_URL_HOST); + + return $this->normalizeHost(is_string($fromApp) ? $fromApp : ''); + } + + private function normalizeHost(string $value): string + { + $host = strtolower(trim($value)); + $host = preg_replace('#^https?://#', '', $host) ?? $host; + $host = explode('/', $host)[0] ?? $host; + $host = explode(':', $host)[0] ?? $host; + + return trim($host); } }