feat: Add /student and /teacher portals with Alpine.js, WhatsApp OTP, and device fingerprinting

This commit is contained in:
Hamza-Ayed
2026-08-26 22:15:33 +03:00
parent a2f72a1c29
commit 388944cbff
5 changed files with 1453 additions and 146 deletions
+158
View File
@@ -0,0 +1,158 @@
<?php
namespace App\Services;
use App\Core\RedisClient;
class NabehService
{
private string $authUrl;
private string $sendUrl;
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;
}
/**
* Retrieve Nabeh JWT Bearer Token, caching it in Redis for 24 hours.
*/
public function getBearerToken(): ?string
{
// 1. Try fetching from Redis first
try {
$redis = RedisClient::getInstance();
$cachedToken = $redis->get('nabeh_bearer_token');
if ($cachedToken) {
return (string)$cachedToken;
}
} catch (\Exception $e) {
error_log("⚠️ [Nabeh Auth Redis] Error reading token: " . $e->getMessage());
}
// 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,
]);
$ch = curl_init($this->authUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
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
try {
$redis = RedisClient::getInstance();
$redis->setex('nabeh_bearer_token', 86400, (string)$token);
error_log("[Nabeh Auth] Token cached in Redis successfully.");
} catch (\Exception $e) {
error_log("⚠️ [Nabeh Auth Redis Cache Save] Error saving token: " . $e->getMessage());
}
return (string)$token;
}
}
error_log("❌ [Nabeh Auth Login Failed] 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
{
$bearerToken = $this->getBearerToken();
if (!$bearerToken) {
error_log("⚠️ [Nabeh OTP] Failed to obtain dynamic JWT Bearer token.");
return false;
}
$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;
}
// 2. Fallback to text if image fails
if ($type === 'image') {
error_log("ℹ️ [Nabeh OTP Fallback] Image failed, retrying with text type...");
return $this->attemptSend($phoneRaw, 'text', $otp, $appName, $bearerToken);
}
return false;
}
private function attemptSend(string $phone, string $type, string $otp, string $appName, string $bearerToken): bool
{
$payload = json_encode([
'phone' => $phone,
'type' => $type,
'code' => $otp,
'message' => "رمز التحقق الخاص بك لمنصة {$appName} هو: *{$otp}* \n الرجاء عدم مشاركته مع أي شخص لحماية حسابك.",
]);
$ch = curl_init($this->sendUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"Authorization: Bearer {$bearerToken}",
],
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200 && $response) {
$decoded = json_decode($response, true);
if ($decoded) {
$statusStr = strtolower((string)($decoded['status'] ?? ''));
$msgStr = strtolower((string)($decoded['message'] ?? ''));
if (
!empty($decoded['success']) ||
in_array($statusStr, ['success', 'ok', 'true', '200', 'sent', 'queued', '1'], true) ||
($decoded['status'] ?? false) === true ||
str_contains($msgStr, 'success') ||
str_contains($msgStr, 'sent') ||
str_contains($msgStr, 'تم')
) {
return true;
}
}
}
error_log("❌ [Nabeh OTP Attempt Failed] Code: {$httpCode} Response: {$response}");
return false;
}
}