['charge', 'tahesab', 'zarin', 'pargar', 'accountingCode', 'info', 'edit', 'referral', 'sms', 'approve', 'archive', 'active', 'trade'], 'customerMain_archive' => ['restore', 'info'], 'customerMain_specialPrice' => ['edit', 'toggle', 'delete'], 'customerMain_goldCredit' => ['detail', 'edit', 'approve', 'reject'], 'customerMain_digipayInstallment' => ['edit'], 'productsMain_limitedOrder' => ['buy', 'sell', 'list', 'tv', 'edit', 'archive'], 'productsMain_archive' => ['restore', 'info'], 'ordersMain' => ['info'], 'transactionsMain' => ['info'], 'accountsMain' => ['toggle', 'delete'], 'walletMain' => ['info'], 'physicalWithdrawMain' => ['amount', 'approve', 'status', 'factor'], 'withdrawMain' => ['confirm', 'factor', 'status', 'weight', 'tahesab', 'zarin'], 'pricingMain' => ['details'], 'priceTriggers' => ['edit', 'toggle', 'delete'], 'conditionalOrders' => ['edit', 'toggle', 'delete'], 'accountingMain' => ['doc', 'register', 'send'], 'appNotifications' => ['recipients', 'readers', 'edit', 'toggle', 'delete'], 'automations' => ['preview', 'log', 'edit', 'toggle', 'delete'], 'giftCardMain' => ['info', 'codes', 'edit', 'delete'], 'discounts' => ['edit'], 'usersMain' => ['info', 'edit', 'delete'], ]; private const ALIASES = [ 'dashboard' => 'dashboardMain', 'main' => 'dashboardMain', 'orders' => 'ordersMain', 'copperConvert' => 'copperConvertMain', 'transactions' => 'transactionsMain', 'support' => 'supportMain', 'factor' => 'withdrawMain', ]; public static function hasFullAccess(?User $user): bool { if (!$user) { return false; } if (self::isDeployAdmin($user)) { return true; } $access = $user->page_access; return $access === null; } public static function isDeployAdmin(?User $user): bool { if (!$user) { return false; } return self::normalizeMobile($user->mobile ?? '') === self::DEPLOY_ADMIN_MOBILE_CORE; } /** * null = دسترسی کامل. آرایه = فقط کلیدهای شناخته‌شده. */ public static function normalize($value): ?array { if ($value === null || $value === '' || $value === false) { return null; } if (is_string($value)) { $decoded = json_decode($value, true); $value = is_array($decoded) ? $decoded : null; if ($value === null) { return null; } } if (!is_array($value)) { return null; } $out = []; foreach (self::LEAF_KEYS as $key) { $row = $value[$key] ?? null; if (!is_array($row)) { continue; } $view = self::toBool($row['view'] ?? false); $edit = self::toBool($row['edit'] ?? false); $delete = self::toBool($row['delete'] ?? false); $actions = null; if (is_array($row['actions'] ?? null)) { $actions = []; foreach (self::ACTIONS[$key] ?? [] as $actionKey) { $actions[$actionKey] = self::toBool($row['actions'][$actionKey] ?? false); } if (array_key_exists('edit', $actions)) { $edit = $actions['edit']; } if (array_key_exists('delete', $actions)) { $delete = $actions['delete']; } } if ($edit || $delete || ($actions && in_array(true, $actions, true))) { $view = true; } if (!$view) { $edit = false; $delete = false; if (is_array($actions)) { foreach ($actions as $actionKey => $enabled) { $actions[$actionKey] = false; } } } if (!$view && !$edit && !$delete && !($actions && in_array(true, $actions, true))) { continue; } $stored = [ 'view' => $view, 'edit' => $edit, 'delete' => $delete, ]; if (is_array($actions) && $actions) { $stored['actions'] = $actions; } $out[$key] = $stored; } return $out; } public static function hasAnyView(?array $access): bool { if ($access === null) { return true; } foreach ($access as $row) { if (!empty($row['view'])) { return true; } } return false; } public static function allows(?User $user, string $routeName, string $action): bool { if (self::hasFullAccess($user)) { return true; } $leaf = self::ALIASES[$routeName] ?? $routeName; if (!in_array($leaf, self::LEAF_KEYS, true)) { return false; } $access = $user->page_access; if (!is_array($access)) { return false; } $row = $access[$leaf] ?? null; if (!is_array($row)) { return false; } if ($action === 'delete' || $action === 'edit') { $actions = $row['actions'] ?? null; if (is_array($actions) && array_key_exists($action, $actions)) { return !empty($actions[$action]); } return !empty($row[$action]); } return !empty($row['view']); } public static function isReadPost(string $path): bool { $path = strtolower(trim($path, '/')); if (str_contains($path, '/table') || substr($path, -5) === 'table') { return true; } if (str_contains($path, 'preview')) { return true; } if (str_contains($path, 'get_customer_list') || str_contains($path, 'products-by-categories')) { return true; } if (str_contains($path, 'mande-by-codes')) { return true; } $exact = [ 'getbalance', 'prices', 'prices/chart', 'gateway', 'finance', 'check_card', 'dashboard', 'dashboard/finance', 'dashboard/customer/all_sell', 'dashboard/customer/single/all_sell', 'dashboard/customer/flow', 'dashboard/customer/order-peak-hours', 'dashboard/profit-conversions', 'dashboard/montzar-reconcile', ]; if (in_array($path, $exact, true)) { return true; } if (preg_match('#^customers/\d+/(referrals|funds|piggy-funds)$#', $path)) { return true; } if (preg_match('#^customers/\d+/assets/\d+/(piggy-funds|transactions)$#', $path)) { return true; } if (preg_match('#^customers/\d+/piggy-funds/\d+/transactions$#', $path)) { return true; } if (preg_match('#^customers/\d+/gold_credit/installments$#', $path)) { return true; } if (preg_match('#^chat/(focus|unfocus|ensure-webhook)$#', $path)) { return true; } return false; } private static function toBool($value): bool { if (is_bool($value)) { return $value; } if (is_numeric($value)) { return (int) $value === 1; } $text = strtolower(trim((string) $value)); return in_array($text, ['1', 'true', 'yes', 'on'], true); } private static function normalizeMobile(?string $mobile): string { $digits = preg_replace('/\D+/', '', (string) $mobile) ?? ''; if (str_starts_with($digits, '98') && strlen($digits) >= 12) { $digits = substr($digits, 2); } if (strlen($digits) === 11 && str_starts_with($digits, '0')) { $digits = substr($digits, 1); } return $digits; } }