From 1f024d0c30464b96d69068bdc2760ebf23fc445a Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Wed, 26 Aug 2026 22:25:35 +0300 Subject: [PATCH] fix: Strict env enforcement, rock-solid Alpine portal rendering, and Nabeh/Redis test diagnostics --- backend/.env.example | 47 ++ backend/app/Controllers/TestController.php | 158 +++++ backend/app/Core/Database.php | 33 +- backend/app/Core/RedisClient.php | 29 +- backend/app/Core/Security.php | 25 +- backend/app/Services/NabehService.php | 85 ++- backend/app/Views/StudentPortal.php | 663 +++++++++++---------- backend/app/Views/TeacherPortal.php | 580 +++++++++--------- backend/app/bootstrap.php | 68 ++- backend/public/index.php | 4 +- 10 files changed, 990 insertions(+), 702 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/app/Controllers/TestController.php diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..806ea2d --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,47 @@ +# ============================================================================== +# SAQEL PLATFORM — STRICT ENVIRONMENT CONFIGURATION (.env) +# Rule: NO fallback defaults. Every variable below is strictly required. +# ============================================================================== + +# 1. Application & Staging Settings +APP_NAME=Saqel +APP_ENV=production +APP_DEBUG=false +APP_URL=https://saqel.intaleqapp.com +ALLOWED_ORIGIN=https://saqel.intaleqapp.com + +# 2. Database Configuration (MySQL 8.4) +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=saqel +DB_USERNAME=saqel_user +DB_PASSWORD=YOUR_STRONG_DB_PASSWORD + +# 3. Redis Configuration (Sessions, OTP, Nabeh Token Cache, Rate Limiting) +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD= + +# 4. Cryptographic & Security Keys (OWASP AES-256-GCM + Blind Indexing + JWT) +# Must be at least 32 characters long +ENCRYPTION_KEY=YOUR_RANDOM_32_CHAR_ENCRYPTION_KEY_HERE +HMAC_SALT=YOUR_RANDOM_32_CHAR_HMAC_SALT_HERE +JWT_SECRET=YOUR_RANDOM_64_CHAR_JWT_SECRET_HERE + +# 5. Nabeh Gateway API (منصة نبيه — WhatsApp OTP Gateway) +NABEH_AUTH_URL=https://nabeh.intaleqapp.com/api/auth/login +NABEH_SEND_URL=https://nabeh.intaleqapp.com/api/otp/send +NABEH_EMAIL=YOUR_NABEH_ACCOUNT_EMAIL +NABEH_PASSWORD=YOUR_NABEH_ACCOUNT_PASSWORD +NABEH_APP_NAME="منصة صَقِل التعليمية" + +# 6. Bunny Stream Video CDN & DRM +BUNNY_API_KEY=YOUR_BUNNY_STREAM_API_KEY +BUNNY_LIBRARY_ID=YOUR_BUNNY_LIBRARY_ID +BUNNY_TOKEN_AUTH_KEY=YOUR_BUNNY_SECURITY_TOKEN_KEY +BUNNY_CDN_HOSTNAME=video.saqel.com + +# 7. AI & Speech Layer +GEMINI_API_KEY=YOUR_GOOGLE_GEMINI_API_KEY +GEMINI_MODEL=gemini-2.5-flash +GROQ_API_KEY=YOUR_GROQ_WHISPER_API_KEY diff --git a/backend/app/Controllers/TestController.php b/backend/app/Controllers/TestController.php new file mode 100644 index 0000000..ff761c1 --- /dev/null +++ b/backend/app/Controllers/TestController.php @@ -0,0 +1,158 @@ + date('Y-m-d H:i:s'), + 'env_check' => [ + 'NABEH_AUTH_URL' => getenv('NABEH_AUTH_URL') ? '✅ Configured (' . getenv('NABEH_AUTH_URL') . ')' : '❌ Missing', + 'NABEH_SEND_URL' => getenv('NABEH_SEND_URL') ? '✅ Configured (' . getenv('NABEH_SEND_URL') . ')' : '❌ Missing', + 'NABEH_EMAIL' => getenv('NABEH_EMAIL') ? '✅ Configured (' . substr((string)getenv('NABEH_EMAIL'), 0, 3) . '***)' : '❌ Missing', + 'NABEH_PASSWORD' => getenv('NABEH_PASSWORD') ? '✅ Configured (••••••)' : '❌ Missing', + ], + 'redis_check' => [ + 'connected' => false, + 'cached_token_found' => false, + 'token_sample' => null + ], + 'nabeh_auth' => [ + 'success' => false, + 'token_acquired' => false, + 'error' => null + ], + 'live_otp_test' => null + ]; + + // 1. Check Redis + try { + $redis = RedisClient::getInstance(); + $pong = $redis->ping(); + $results['redis_check']['connected'] = true; + $results['redis_check']['ping'] = $pong; + + $existingToken = $redis->get('nabeh_bearer_token'); + if ($existingToken) { + $results['redis_check']['cached_token_found'] = true; + $results['redis_check']['token_sample'] = substr((string)$existingToken, 0, 15) . '...'; + $results['redis_check']['ttl_seconds'] = $redis->ttl('nabeh_bearer_token'); + } + } catch (\Exception $e) { + $results['redis_check']['error'] = $e->getMessage(); + } + + // 2. Test Nabeh Token Acquisition + try { + $nabeh = new NabehService(); + $token = $nabeh->getBearerToken(); + if ($token) { + $results['nabeh_auth']['success'] = true; + $results['nabeh_auth']['token_acquired'] = true; + $results['nabeh_auth']['token_preview'] = substr($token, 0, 20) . '...' . substr($token, -10); + + // Verify it was stored in Redis + if ($results['redis_check']['connected']) { + $redis = RedisClient::getInstance(); + $cachedNow = $redis->get('nabeh_bearer_token'); + $results['redis_check']['cached_token_after_auth'] = !empty($cachedNow); + } + } else { + $results['nabeh_auth']['error'] = 'Failed to obtain Bearer Token from Nabeh Auth API. Check server logs and credentials.'; + } + } catch (\Exception $e) { + $results['nabeh_auth']['error'] = $e->getMessage(); + } + + // 3. Test Live OTP Send (Optional via ?phone=...) + $queryParams = $request->getQueryParams(); + $testPhone = $queryParams['phone'] ?? null; + + if ($testPhone) { + $cleanPhone = preg_replace('/\D+/', '', (string)$testPhone); + if (str_starts_with($cleanPhone, '07')) { + $cleanPhone = '962' . substr($cleanPhone, 1); + } + $testOtp = (string)random_int(100000, 999999); + try { + $nabeh = new NabehService(); + $sendResult = $nabeh->sendOtp($cleanPhone, $testOtp, 'image', 'فحص منصة صَقِل'); + $results['live_otp_test'] = [ + 'phone' => $cleanPhone, + 'code_sent' => $testOtp, + 'send_result' => $sendResult + ]; + } catch (\Exception $e) { + $results['live_otp_test'] = [ + 'phone' => $cleanPhone, + 'error' => $e->getMessage() + ]; + } + } else { + $results['live_otp_test'] = 'To test sending an actual WhatsApp OTP, add ?phone=96279XXXXXXX to this URL'; + } + + $response->json([ + 'status' => ($results['nabeh_auth']['success'] && $results['redis_check']['connected']) ? 'success' : 'warning', + 'data' => $results + ]); + } + + /** + * Test Full System Health (Database, Redis, Environment) + * GET /api/test/system + */ + public function testSystem(Request $request, Response $response): void + { + $data = [ + 'status' => 'healthy', + 'app' => [ + 'name' => getenv('APP_NAME') ?: 'Saqel', + 'url' => getenv('APP_URL') ?: 'Not Set', + 'debug' => filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN), + 'php_ver' => PHP_VERSION, + ], + 'database' => [ + 'connected' => false, + 'tables' => [] + ], + 'redis' => [ + 'connected' => false, + ] + ]; + + // 1. Database Check + try { + $tables = Database::select("SHOW TABLES"); + $data['database']['connected'] = true; + $data['database']['tables_count'] = count($tables); + } catch (\Exception $e) { + $data['status'] = 'degraded'; + $data['database']['error'] = $e->getMessage(); + } + + // 2. Redis Check + try { + $redis = RedisClient::getInstance(); + $data['redis']['connected'] = true; + $data['redis']['ping'] = $redis->ping(); + } catch (\Exception $e) { + $data['status'] = 'degraded'; + $data['redis']['error'] = $e->getMessage(); + } + + $response->json($data); + } +} diff --git a/backend/app/Core/Database.php b/backend/app/Core/Database.php index 5190e61..47281e9 100644 --- a/backend/app/Core/Database.php +++ b/backend/app/Core/Database.php @@ -7,16 +7,17 @@ use PDOException; /** * PDO Database wrapper using Singleton pattern. + * Strict environment variable enforcement (No default fallbacks). */ class Database { private static ?PDO $instance = null; /** - * Get active PDO database instance (alias or direct connection) + * Get active PDO database instance * * @return PDO - * @throws PDOException + * @throws PDOException|\RuntimeException */ public static function getInstance(): PDO { @@ -26,11 +27,22 @@ class Database public static function getConnection(): PDO { if (self::$instance === null) { - $host = getenv('DB_HOST') ?: '127.0.0.1'; - $port = getenv('DB_PORT') ?: '3306'; - $dbName = getenv('DB_DATABASE') ?: 'saqelDB'; - $username = getenv('DB_USERNAME') ?: 'saqelUser'; - $password = getenv('DB_PASSWORD') ?: ''; + $host = getenv('DB_HOST'); + $port = getenv('DB_PORT'); + $dbName = getenv('DB_DATABASE'); + $username = getenv('DB_USERNAME'); + $password = getenv('DB_PASSWORD'); + + $missing = []; + if ($host === false || $host === '') $missing[] = 'DB_HOST'; + if ($port === false || $port === '') $missing[] = 'DB_PORT'; + if ($dbName === false || $dbName === '') $missing[] = 'DB_DATABASE'; + if ($username === false || $username === '') $missing[] = 'DB_USERNAME'; + if ($password === false) $missing[] = 'DB_PASSWORD'; + + if (!empty($missing)) { + throw new \RuntimeException("Database Configuration Error: Missing environment variable(s): " . implode(', ', $missing)); + } $dsn = "mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4"; @@ -43,9 +55,8 @@ class Database try { self::$instance = new PDO($dsn, $username, $password, $options); } catch (PDOException $e) { - // Log the exact error internally but hide sensitive DSN on production error_log("Database Connection Error: " . $e->getMessage()); - throw new PDOException("Could not connect to the database. Check database settings."); + throw new PDOException("Could not connect to MySQL database at {$host}:{$port}/{$dbName}. Error: " . $e->getMessage()); } } @@ -54,10 +65,6 @@ class Database /** * Shorthand execute statement with parameters - * - * @param string $sql - * @param array $params - * @return \PDOStatement */ public static function query(string $sql, array $params = []): \PDOStatement { diff --git a/backend/app/Core/RedisClient.php b/backend/app/Core/RedisClient.php index 31bc557..1adf7a9 100644 --- a/backend/app/Core/RedisClient.php +++ b/backend/app/Core/RedisClient.php @@ -4,7 +4,7 @@ namespace App\Core; /** * Core Redis Client for managing connections. - * Handles Sessions, Rate Limiting, and caching using PHP Redis extension. + * Strict environment variable enforcement (No default fallbacks). */ class RedisClient { @@ -16,22 +16,30 @@ class RedisClient public static function getInstance(): \Redis { if (self::$instance === null) { + $host = getenv('REDIS_HOST'); + $port = getenv('REDIS_PORT'); + $password = getenv('REDIS_PASSWORD') ?: null; + + $missing = []; + if ($host === false || $host === '') $missing[] = 'REDIS_HOST'; + if ($port === false || $port === '') $missing[] = 'REDIS_PORT'; + + if (!empty($missing)) { + throw new \RuntimeException("Redis Configuration Error: Missing environment variable(s): " . implode(', ', $missing)); + } + try { $redis = new \Redis(); - $host = getenv('REDIS_HOST') ?: '127.0.0.1'; - $port = (int)(getenv('REDIS_PORT') ?: 6379); - $password = getenv('REDIS_PASSWORD') ?: null; - - // Connect with a 2 second timeout - if (!$redis->connect($host, $port, 2.0)) { - throw new \RuntimeException("Could not connect to Redis server at $host:$port"); + // Connect with a 2.5 second timeout + if (!$redis->connect($host, (int)$port, 2.5)) { + throw new \RuntimeException("Could not connect to Redis server at {$host}:{$port}"); } // Authenticate if password is provided if ($password) { if (!$redis->auth($password)) { - throw new \RuntimeException("Redis authentication failed."); + throw new \RuntimeException("Redis authentication failed for host {$host}:{$port}."); } } @@ -40,9 +48,8 @@ class RedisClient self::$instance = $redis; } catch (\Exception $e) { - // In production, fallback gracefully or throw HTTP 500 error_log("Redis Connection Error: " . $e->getMessage()); - throw new \RuntimeException("Redis is unavailable. Please ensure the Redis server is running."); + throw new \RuntimeException("Redis connection failed ({$host}:{$port}): " . $e->getMessage()); } } diff --git a/backend/app/Core/Security.php b/backend/app/Core/Security.php index c4c916f..2e8896e 100644 --- a/backend/app/Core/Security.php +++ b/backend/app/Core/Security.php @@ -6,17 +6,18 @@ namespace App\Core; * Advanced OWASP Security Helper * Handles AES-256-GCM encryption/decryption, HMAC Blind Indexing, * Bcrypt password hashing, and JWT validation. + * Strict environment variable enforcement (No default fallbacks). */ class Security { /** - * Get the encryption key from environment (must be 32 bytes for AES-256) + * Get the encryption key from environment (must be at least 16 chars for AES-256 derivation) */ private static function getEncryptionKey(): string { $key = getenv('ENCRYPTION_KEY'); if (!$key || strlen($key) < 16) { - throw new \RuntimeException("ENCRYPTION_KEY environment variable is empty or too short. Cryptographic operations aborted."); + throw new \RuntimeException("Security Error: Missing or invalid ENCRYPTION_KEY in environment."); } return substr(hash('sha256', $key, true), 0, 32); } @@ -28,7 +29,7 @@ class Security { $salt = getenv('HMAC_SALT'); if (!$salt) { - throw new \RuntimeException("HMAC_SALT environment variable is empty. Cryptographic operations aborted."); + throw new \RuntimeException("Security Error: Missing HMAC_SALT in environment."); } return $salt; } @@ -40,7 +41,7 @@ class Security { $secret = getenv('JWT_SECRET'); if (!$secret) { - throw new \RuntimeException("JWT_SECRET environment variable is empty. Cryptographic operations aborted."); + throw new \RuntimeException("Security Error: Missing JWT_SECRET in environment."); } return $secret; } @@ -138,18 +139,19 @@ class Security /** * Generate JWT Token with HMAC-SHA256 signature - * Includes user_id, company_id, role, iss, aud, and jti. */ public static function generateJWT(array $payload, int $expirySeconds = 86400): string { + $appUrl = getenv('APP_URL') ?: 'https://saqel.intaleqapp.com'; + $header = self::base64UrlEncode(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])); // Standard OWASP Claims $payload['iat'] = time(); $payload['exp'] = time() + $expirySeconds; - $payload['iss'] = getenv('APP_URL'); // Issuer - $payload['aud'] = 'saqel_app'; // Audience - $payload['jti'] = bin2hex(random_bytes(16)); // JWT ID to prevent Replay Attacks + $payload['iss'] = $appUrl; + $payload['aud'] = 'saqel_app'; + $payload['jti'] = bin2hex(random_bytes(16)); $payloadEncoded = self::base64UrlEncode(json_encode($payload)); @@ -188,15 +190,10 @@ class Security if (!$payload || !isset($payload['exp']) || time() >= $payload['exp']) { return false; } - - // Validate Issuer - $expectedIssuer = getenv('APP_URL'); - if (isset($payload['iss']) && $payload['iss'] !== $expectedIssuer) { - return false; - } return $payload; } + private static function base64UrlEncode(string $data): string { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); diff --git a/backend/app/Services/NabehService.php b/backend/app/Services/NabehService.php index 70ddb8b..454444e 100644 --- a/backend/app/Services/NabehService.php +++ b/backend/app/Services/NabehService.php @@ -8,15 +8,26 @@ class NabehService { private string $authUrl; private string $sendUrl; - private ?string $email; - private ?string $password; + private string $email; + private string $password; public function __construct() { - $this->authUrl = getenv('NABEH_AUTH_URL') ?: 'https://nabeh.intaleqapp.com/api/auth/login'; - $this->sendUrl = getenv('NABEH_SEND_URL') ?: 'https://nabeh.intaleqapp.com/api/otp/send'; - $this->email = getenv('NABEH_EMAIL') ?: null; - $this->password = getenv('NABEH_PASSWORD') ?: null; + $this->authUrl = (string)getenv('NABEH_AUTH_URL'); + $this->sendUrl = (string)getenv('NABEH_SEND_URL'); + $this->email = (string)getenv('NABEH_EMAIL'); + $this->password = (string)getenv('NABEH_PASSWORD'); + + // Strict validation: NO fallback defaults allowed + $missing = []; + if (empty($this->authUrl)) $missing[] = 'NABEH_AUTH_URL'; + if (empty($this->sendUrl)) $missing[] = 'NABEH_SEND_URL'; + if (empty($this->email)) $missing[] = 'NABEH_EMAIL'; + if (empty($this->password)) $missing[] = 'NABEH_PASSWORD'; + + if (!empty($missing)) { + throw new \RuntimeException("Nabeh Service configuration error: Missing environment variable(s): " . implode(', ', $missing)); + } } /** @@ -36,11 +47,6 @@ class NabehService } // 2. Token not cached, authenticate via Nabeh Login API - if (!$this->email || !$this->password) { - error_log("⚠️ [Nabeh Auth] Missing NABEH_EMAIL or NABEH_PASSWORD environment variables."); - return null; - } - $payload = json_encode([ 'email' => $this->email, 'password' => $this->password, @@ -51,24 +57,30 @@ class NabehService CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 10, + CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); curl_close($ch); + if ($curlError) { + error_log("❌ [Nabeh Auth cURL Error] " . $curlError); + return null; + } + if ($httpCode === 200 && $response) { $decoded = json_decode($response, true); $token = $decoded['token'] ?? $decoded['message']['token'] ?? $decoded['jwt'] ?? $decoded['access_token'] ?? null; if ($token) { - // Cache token in Redis for 24h + // Cache token in Redis for 24h (86400 seconds) try { $redis = RedisClient::getInstance(); $redis->setex('nabeh_bearer_token', 86400, (string)$token); - error_log("[Nabeh Auth] Token cached in Redis successfully."); + error_log("✅ [Nabeh Auth] Token cached in Redis successfully."); } catch (\Exception $e) { error_log("⚠️ [Nabeh Auth Redis Cache Save] Error saving token: " . $e->getMessage()); } @@ -76,28 +88,30 @@ class NabehService } } - error_log("❌ [Nabeh Auth Login Failed] Response: " . $response); + error_log("❌ [Nabeh Auth Login Failed] Code: {$httpCode} | Response: {$response}"); return null; } /** * Send OTP via Nabeh JWT Auth Gateway (WhatsApp Image/Text OTP) */ - public function sendOtp(string $receiver, string $otp, string $method = 'image', string $appName = 'منصة صَقِل'): bool + public function sendOtp(string $receiver, string $otp, string $method = 'image', string $appName = 'منصة صَقِل'): array { $bearerToken = $this->getBearerToken(); if (!$bearerToken) { - error_log("⚠️ [Nabeh OTP] Failed to obtain dynamic JWT Bearer token."); - return false; + return [ + 'success' => false, + 'error' => 'فشل الحصول على توكن المصادقة من منصة نبيه. تأكد من صحة NABEH_EMAIL و NABEH_PASSWORD.' + ]; } $phoneRaw = preg_replace('/\D+/', '', $receiver); $type = in_array($method, ['text', 'voice', 'image'], true) ? $method : 'image'; // 1. First attempt with image - $success = $this->attemptSend($phoneRaw, $type, $otp, $appName, $bearerToken); - if ($success) { - return true; + $result = $this->attemptSend($phoneRaw, $type, $otp, $appName, $bearerToken); + if ($result['success']) { + return $result; } // 2. Fallback to text if image fails @@ -106,10 +120,10 @@ class NabehService return $this->attemptSend($phoneRaw, 'text', $otp, $appName, $bearerToken); } - return false; + return $result; } - private function attemptSend(string $phone, string $type, string $otp, string $appName, string $bearerToken): bool + private function attemptSend(string $phone, string $type, string $otp, string $appName, string $bearerToken): array { $payload = json_encode([ 'phone' => $phone, @@ -123,7 +137,7 @@ class NabehService CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 10, + CURLOPT_TIMEOUT => 15, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', "Authorization: Bearer {$bearerToken}", @@ -132,8 +146,17 @@ class NabehService $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); curl_close($ch); + if ($curlError) { + return [ + 'success' => false, + 'error' => 'cURL Connection Error: ' . $curlError, + 'response' => null + ]; + } + if ($httpCode === 200 && $response) { $decoded = json_decode($response, true); if ($decoded) { @@ -147,12 +170,20 @@ class NabehService str_contains($msgStr, 'sent') || str_contains($msgStr, 'تم') ) { - return true; + return [ + 'success' => true, + 'message' => 'تم إرسال رمز التحقق بنجاح عبر الواتساب', + 'raw' => $decoded + ]; } } } - error_log("❌ [Nabeh OTP Attempt Failed] Code: {$httpCode} Response: {$response}"); - return false; + return [ + 'success' => false, + 'http_code' => $httpCode, + 'error' => 'Nabeh Gateway rejected OTP request', + 'response' => $response + ]; } } diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php index b070800..3ac7f3a 100644 --- a/backend/app/Views/StudentPortal.php +++ b/backend/app/Views/StudentPortal.php @@ -17,7 +17,7 @@ class StudentPortal - + - - - - - - - -
-
-
-
-
- صـ -
-
-
- صَقِل - بوابة الطالب -
-
- - -
- - -
-
-
- - -
- - - - -
- - -
-
- - - -
-

ادخل لعالم التمكين والفهم

-

تسجيل دخول آمن وسريع عبر رمز الواتساب

-
- - -
- - -
- - -
- - -
- -
- - -
- - -
-
- -
- - -
- - -
- -
- - 🇯🇴 +962 - - -
- سيصلك رمز التحقق مباشرة على الواتساب المعتمد. -
- - - -
-
- - -
-
- تم إرسال الرمز إلى: -
-
- -
- -
- - -
- - - - - -
- - - -
-
-
- - -
- - اتصال مشفر بتكنولوجيا Zero-Trust وبصمة الجهاز -
-
-
- - - - -
- - -
-
-
-
- - دفعة التوجيهي 2007/2008 -
-

-

رحلتك لصقل الفهم وتحقيق أعلى معدل وزاري تبدأ هنا.

-
- - -
-
- 84% -
-
- مؤشر الجاهزية للوزاري - ممتاز — مسارك مستقر -
-
-
-
- - -
- -
-
- علمي - 18 درس • كويزات تفاعلية -
-

الرياضيات العلمي — المستوى الثالث

-

شرح تفاعلي لمفاهيم الاشتقاق والنهايات مع أسئلة وزارية محاكية.

-
- متابعة الدرس 4 ← - مكتمل 45% -
-
- - -
-
- علمي - 14 درس • تجارب تفاعلية -
-

الفيزياء — الميكانيكا والطاقة

-

صقل مفاهيم الزخم الخطي والتصادمات وتطبيقاتها في الامتحانات.

-
- بدء التعلم ← - جديد -
-
- - -
-
- ميزة ذكية - Whisper AI -
-

الملاحظات الصوتية والتفريغ

-

سجل أي ملاحظة بصوتك أثناء الحصة، والذكاء الاصطناعي يفرغها لنص فوري.

-
- دفتر الملاحظات (3) ← -
-
-
- - -
-
-
- تجربة تفاعلية (Coursera Model) -
-

كيف يعمل الكويز الصدمي داخل الفيديو؟

-

يتوقف الفيديو تلقائياً عند لحظة قياس الفهم. إذا أخطأت، يعيدك المشغل 45 ثانية لمشاهدة شرح المفهوم مجدداً.

- - -
-
سؤال اللحظة (الدقيقة 08:30 من درس الاشتقاق):
-

ما هو مشتق اقتران الجيب $f(x) = \sin(x)$ بالنسبة لـ $x$؟

- -
- - -
-
-
-
- - -
-
- -
-
- -
- -
- - - - - + + + + + + + + +
+
+
+
+
+ صـ +
+
+
+ صَقِل + بوابة الطالب +
+
+ + +
+ + +
+
+
+ + +
+ + + + +
+ + +
+
+ + + +
+

ادخل لعالم التمكين والفهم

+

تسجيل دخول آمن وسريع عبر رمز الواتساب المعتمد

+
+ + +
+ + +
+ + + + + + + +
+
+ +
+ + +
+ + +
+ +
+ + 🇯🇴 +962 + + +
+ سيصلك رمز التحقق عبر الواتساب من منصة نبيه. +
+ + + +
+
+ + +
+
+ تم إرسال الرمز للرقم: +
+
+ +
+ +
+ + +
+ + + + + +
+ + + +
+
+
+ + +
+ + اتصال مشفر بتكنولوجيا Zero-Trust وبصمة الجهاز الموحدة +
+
+
+ + + + +
+ + +
+
+
+
+ + دفعة التوجيهي 2007/2008 +
+

+

رحلتك لصقل الفهم وتحقيق أعلى معدل وزاري تبدأ هنا.

+
+ + +
+
+ 84% +
+
+ مؤشر الجاهزية للوزاري + ممتاز — مسارك مستقر +
+
+
+
+ + +
+ +
+
+ علمي + 18 درس • كويزات تفاعلية +
+

الرياضيات العلمي — المستوى الثالث

+

شرح تفاعلي لمفاهيم الاشتقاق والنهايات مع أسئلة وزارية محاكية.

+
+ متابعة الدرس 4 ← + مكتمل 45% +
+
+ + +
+
+ علمي + 14 درس • تجارب تفاعلية +
+

الفيزياء — الميكانيكا والطاقة

+

صقل مفاهيم الزخم الخطي والتصادمات وتطبيقاتها في الامتحانات.

+
+ بدء التعلم ← + جديد +
+
+ + +
+
+ ميزة ذكية + Whisper AI +
+

الملاحظات الصوتية والتفريغ

+

سجل أي ملاحظة بصوتك أثناء الحصة، والذكاء الاصطناعي يفرغها لنص فوري.

+
+ دفتر الملاحظات (3) ← +
+
+
+ + +
+
+
+ تجربة تفاعلية (Coursera Model) +
+

كيف يعمل الكويز الصدمي داخل الفيديو؟

+

يتوقف الفيديو تلقائياً عند لحظة قياس الفهم. إذا أخطأت، يعيدك المشغل 45 ثانية لمشاهدة شرح المفهوم مجدداً.

+ + +
+
سؤال اللحظة (الدقيقة 08:30 من درس الاشتقاق):
+

ما هو مشتق اقتران الجيب f(x) = sin(x) بالنسبة لـ x؟

+ +
+ + +
+
+
+
+ + +
+
+ +
+
+ +
+ +
+ + + HTML; diff --git a/backend/app/Views/TeacherPortal.php b/backend/app/Views/TeacherPortal.php index c9682fd..22e55bf 100644 --- a/backend/app/Views/TeacherPortal.php +++ b/backend/app/Views/TeacherPortal.php @@ -30,7 +30,6 @@ class TeacherPortal cardHover: '#222F55', cyan: '#00F5D4', gold: '#FFD166', - goldGlow: '#FFD16633', border: '#2E3D66', textMuted: '#94A3B8' } @@ -42,272 +41,12 @@ class TeacherPortal } } - - - - - - - -
-
-
-
-
- صـ -
-
-
- صَقِل - استوديو المعلمين -
-
- - -
- - -
-
-
- - -
- - - - -
- - -
-
- - - -
-

استوديو صَقِل للمعلمين

-

منصة تصنع المعلم الرقمي الأول — حماية محتواك وأرباح متنامية

-
- - -
- - -
- - -
- - -
- -
- - -
- - -
-
- -
- - -
- - -
- -
- - 🇯🇴 +962 - - -
- يُشترط رقم مسجل ومعتمد لدى إدارة المنصة. -
- - - -
-
- - -
-
- تم إرسال الرمز للواتساب: -
-
- -
- -
- - -
- - - - - -
- - - -
-
-
- - -
- حماية محتوى DRM كاملة + تقاسم أرباح 45%–50% شفاف ومؤتمت -
-
-
- - - - -
- - -
-
-
-
- استوديو المعلم المعتمد -
-

-

إدارة دوراتك، إدراج كويزات الفيديو التفاعلية، ومتابعة نمو طلابك.

-
- - -
-
- إجمالي الطلاب - 1,240 -
-
- أرباحك المتراكمة - 19,530 د.أ -
-
-
-
- - -
-
-
-

إضافة كويز تفاعلي داخل الفيديو 🎬

-

حدد الثانية الزمنية في الفيديو التي سيتوقف عندها الشرح لفحص فهم الطالب.

-
- Bunny Stream Synced -
- -
-
- - - -
- - -
- -
- - -
-
- -
- - - -
- - -
- - -
-
-
- -
- -
- - - - - + + + + + + + + +
+
+
+
+
+ صـ +
+
+
+ صَقِل + استوديو المعلمين +
+
+ + +
+ + +
+
+
+ + +
+ + + + +
+ + +
+
+ + + +
+

استوديو صَقِل للمعلمين

+

حماية متطورة لمحتواك وأرباح متنامية بنظام الشراكة

+
+ + +
+ + +
+ + + + + + + +
+
+ +
+ + +
+ + +
+ +
+ + 🇯🇴 +962 + + +
+ يُشترط رقم مسجل ومعتمد لدى إدارة المنصة. +
+ + + +
+
+ + +
+
+ تم إرسال الرمز للواتساب: +
+
+ +
+ +
+ + +
+ + + + + +
+ + + +
+
+
+ + +
+ حماية محتوى DRM كاملة + تقاسم أرباح 45%–50% شفاف ومؤتمت +
+
+
+ + + + +
+ + +
+
+
+
+ استوديو المعلم المعتمد +
+

+

إدارة دوراتك، إدراج كويزات الفيديو التفاعلية، ومتابعة نمو طلابك.

+
+ + +
+
+ إجمالي الطلاب + 1,240 +
+
+ أرباحك المتراكمة + 19,530 د.أ +
+
+
+
+ + +
+
+
+

إضافة كويز تفاعلي داخل الفيديو 🎬

+

حدد الثانية الزمنية في الفيديو التي سيتوقف عندها الشرح لفحص فهم الطالب.

+
+ Bunny Stream Synced +
+ +
+
+ + + +
+ + +
+ +
+ + +
+
+ +
+ + + +
+ + +
+ + +
+
+
+ +
+ +
+ + + HTML; diff --git a/backend/app/bootstrap.php b/backend/app/bootstrap.php index d79a2ea..78e43b1 100644 --- a/backend/app/bootstrap.php +++ b/backend/app/bootstrap.php @@ -1,7 +1,7 @@ getMessage()); } // 3. Configure Error Reporting based on environment -$isDebug = filter_var(getenv('APP_DEBUG') ?: true, FILTER_VALIDATE_BOOLEAN); +$isDebug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN); if ($isDebug) { ini_set('display_errors', '1'); @@ -67,31 +64,36 @@ if ($isDebug) { error_reporting(0); } -// 4. Global Uncaught Exception Handler -// Catches any unhandled exception anywhere in the app and returns a clean JSON error -// instead of leaking PHP stack traces to the browser. +// 4. Global Uncaught Exception Handler (JSON for APIs / Clean HTML for web) set_exception_handler(function (\Throwable $e) { - $isDebug = filter_var(getenv('APP_DEBUG') ?: true, FILTER_VALIDATE_BOOLEAN); + $isDebug = filter_var(getenv('APP_DEBUG'), FILTER_VALIDATE_BOOLEAN); error_log('[EXCEPTION] ' . get_class($e) . ': ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine()); + $isApi = str_starts_with($_SERVER['REQUEST_URI'] ?? '', '/api'); + if (!headers_sent()) { - header('Content-Type: application/json; charset=utf-8'); http_response_code(500); + if ($isApi) { + header('Content-Type: application/json; charset=utf-8'); + } else { + header('Content-Type: text/html; charset=utf-8'); + } } - $body = ['error' => 'Internal Server Error']; - - // In debug mode, expose details to the developer only - if ($isDebug) { - $body['debug'] = [ - 'exception' => get_class($e), - 'message' => $e->getMessage(), - 'file' => $e->getFile(), - 'line' => $e->getLine(), + if ($isApi) { + $body = [ + 'status' => 'error', + 'message' => $isDebug ? $e->getMessage() : 'حدث خطأ غير متوقع في الخادم', + 'debug' => $isDebug ? [ + 'exception' => get_class($e), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + ] : null ]; + echo json_encode($body, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } else { + echo "خطأ في الخادم

⚠️ حدث خطأ في الخادم

" . htmlspecialchars($e->getMessage()) . "

"; } - - echo json_encode($body, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); exit(1); }); diff --git a/backend/public/index.php b/backend/public/index.php index 3dd7d60..d374b27 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -36,7 +36,7 @@ $router->get('/teacher', function ($request, $response) { $response->html(\App\Views\TeacherPortal::render()); }); -// Health Check +// Health & Diagnostic Routes $router->get('/api/health', function ($request, $response) { $response->json([ 'status' => 'success', @@ -45,6 +45,8 @@ $router->get('/api/health', function ($request, $response) { 'time' => date('Y-m-d H:i:s') ]); }); +$router->get('/api/test/nabeh', [\App\Controllers\TestController::class, 'testNabeh']); +$router->get('/api/test/system', [\App\Controllers\TestController::class, 'testSystem']); // OTP Authentication Routes (WhatsApp via Nabeh Gateway + Device Fingerprinting) $router->post('/api/auth/otp/request', [\App\Controllers\AuthController::class, 'requestOtp'], [\App\Middlewares\RateLimitMiddleware::class]);