feat: Initial commit for Saqel Platform architecture, backend, Docker, and documentation
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Exception;
|
||||
|
||||
class NabehOtpService
|
||||
{
|
||||
protected string $authUrl;
|
||||
protected string $sendUrl;
|
||||
protected ?string $email;
|
||||
protected ?string $password;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->authUrl = config('services.nabeh.auth_url', 'https://nabeh.intaleqapp.com/api/auth/login');
|
||||
$this->sendUrl = config('services.nabeh.send_url', 'https://nabeh.intaleqapp.com/api/otp/send');
|
||||
$this->email = config('services.nabeh.email', env('NABEH_EMAIL'));
|
||||
$this->password = config('services.nabeh.password', env('NABEH_PASSWORD'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve Nabeh JWT Bearer Token, caching it in Redis for 24 hours.
|
||||
*/
|
||||
public function getBearerToken(): ?string
|
||||
{
|
||||
// 1. Try fetching from Redis first
|
||||
try {
|
||||
$cachedToken = Redis::get('nabeh_bearer_token');
|
||||
if ($cachedToken) {
|
||||
return (string)$cachedToken;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::warning("⚠️ [Nabeh Auth Redis] Error reading token: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// 2. Token not cached, authenticate via Nabeh Login API
|
||||
if (!$this->email || !$this->password) {
|
||||
Log::error("❌ [Nabeh Auth] Missing NABEH_EMAIL or NABEH_PASSWORD environment variables.");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(10)->post($this->authUrl, [
|
||||
'email' => $this->email,
|
||||
'password' => $this->password,
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$decoded = $response->json();
|
||||
$token = $decoded['token'] ?? $decoded['message']['token'] ?? $decoded['jwt'] ?? $decoded['access_token'] ?? null;
|
||||
|
||||
if ($token) {
|
||||
// 3. Cache token in Redis for 24h
|
||||
try {
|
||||
Redis::setex('nabeh_bearer_token', 86400, (string)$token);
|
||||
Log::info("[Nabeh Auth] Token cached in Redis successfully.");
|
||||
} catch (Exception $e) {
|
||||
Log::warning("⚠️ [Nabeh Auth Redis Cache Save] Error saving token: " . $e->getMessage());
|
||||
}
|
||||
return (string)$token;
|
||||
}
|
||||
Log::error("❌ [Nabeh Auth Login Failed] Response without token: " . $response->body());
|
||||
} else {
|
||||
Log::error("❌ [Nabeh Auth Login Failed] Status: " . $response->status() . " | Body: " . $response->body());
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("❌ [Nabeh Auth Exception] " . $e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send OTP via Nabeh JWT Auth Gateway (WhatsApp Image/Text OTP)
|
||||
*
|
||||
* @param string $receiver Recipient phone number
|
||||
* @param string $otp 4-6 digit verification code
|
||||
* @param string $method image | text | voice
|
||||
* @param string $appName Application name to appear in the message
|
||||
* @return bool
|
||||
*/
|
||||
public function sendOtp(string $receiver, string $otp, string $method = 'image', string $appName = 'منصة صَقِل'): bool
|
||||
{
|
||||
$bearerToken = $this->getBearerToken();
|
||||
if (!$bearerToken) {
|
||||
Log::error("⚠️ [Nabeh OTP] Failed to obtain dynamic JWT Bearer token.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Strip symbols from phone
|
||||
$phoneRaw = preg_replace('/\D+/', '', $receiver);
|
||||
|
||||
// Type mapping (Image OTP card is default for Nabeh)
|
||||
$type = in_array($method, ['text', 'voice', 'image'], true) ? $method : 'image';
|
||||
|
||||
// First attempt with the chosen type
|
||||
$success = $this->attemptSend($phoneRaw, $type, $otp, $appName, $bearerToken);
|
||||
if ($success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: if image failed, retry with text
|
||||
if ($type === 'image') {
|
||||
Log::info("ℹ️ [Nabeh OTP Fallback] Image failed, retrying with text type...");
|
||||
return $this->attemptSend($phoneRaw, 'text', $otp, $appName, $bearerToken);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper: single OTP send attempt to Nabeh
|
||||
*/
|
||||
protected function attemptSend(string $phone, string $type, string $otp, string $appName, string $bearerToken): bool
|
||||
{
|
||||
$payload = [
|
||||
'phone' => $phone,
|
||||
'type' => $type,
|
||||
'code' => $otp,
|
||||
'message' => "رمز التحقق الخاص بك لمنصة {$appName} هو: *{code}* \n الرجاء عدم مشاركته مع أي شخص لحماية حسابك.",
|
||||
];
|
||||
|
||||
try {
|
||||
$response = Http::withToken($bearerToken)
|
||||
->timeout(10)
|
||||
->post($this->sendUrl, $payload);
|
||||
|
||||
Log::info("ℹ️ [Nabeh OTP Response type={$type}] " . $response->body());
|
||||
|
||||
if ($response->successful()) {
|
||||
$decoded = $response->json();
|
||||
if ($decoded) {
|
||||
$statusStr = strtolower((string)($decoded['status'] ?? ''));
|
||||
$msgStr = strtolower((string)($decoded['message'] ?? ''));
|
||||
$errStr = strtolower((string)($decoded['error'] ?? ''));
|
||||
|
||||
if (
|
||||
!empty($decoded['success']) ||
|
||||
in_array($statusStr, ['success', 'ok', 'true', '200', 'sent', 'queued', '1'], true) ||
|
||||
($decoded['status'] ?? false) === true ||
|
||||
($decoded['code'] ?? 0) === 200 ||
|
||||
!empty($decoded['message_id']) ||
|
||||
!empty($decoded['id']) ||
|
||||
!empty($decoded['token']) ||
|
||||
str_contains($msgStr, 'success') ||
|
||||
str_contains($msgStr, 'sent') ||
|
||||
str_contains($msgStr, 'تم') ||
|
||||
str_contains($errStr, 'via gateway')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Log::warning("⚠️ [Nabeh OTP Response not matching success] " . $response->body());
|
||||
} else {
|
||||
Log::error("❌ [Nabeh OTP Request Failed] Status: " . $response->status() . " | Body: " . $response->body());
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error("❌ [Nabeh OTP Exception] " . $e->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user