دليل ربط Udeyou OTP بلغة Laravel ================================ الرابط الأساسي https://udeyou.com/api/v1 التوثيق https://udeyou.com/docs الصيغة JSON في الطلب والرد (Content-Type: application/json) المصادقة Authorization: Bearer أو X-API-Key: هذه النسخة العربية، والنسخة الإنجليزية هي المرجع الأساسي. الكود هو نفسه المعروض في https://udeyou.com/docs ومُختبر على الواجهة. 1) قبل البدء 1) أنشئ حسابًا: https://udeyou.com/register تبدأ الحسابات الجديدة تجربة مجانية لمدة شهرين: 2000 رسالة في الشهر الأول ومثلها في الشهر الثاني. 2) أنشئ مفتاح API من لوحة التحكم: https://udeyou.com/dashboard#keys يظهر المفتاح مرة واحدة فقط، فاحفظه في متغير البيئة UDEYOU_API_KEY على خادمك. 3) أنواع المفاتيح udy_test_... لا يرسل بريدًا ولا يخصم رصيدًا، ويعمل حتى برصيد صفر. يعود الرمز في الرد ضمن الحقل code مع "test_mode": true. للتطوير والاختبار الآلي. udy_live_... يرسل البريد فعليًا ويخصم رصيدًا واحدًا لكل رمز، ويُعاد الرصيد تلقائيًا إذا تعذّر الإرسال. كل مفتاح يرى رموز نوعه فقط. 4) أبقِ المفتاح على خادمك، ولا تضعه في JavaScript المتصفح أو تطبيق الجوال أو مستودع الكود. 2) المتطلبات والإعداد Laravel 10 أو أحدث (PHP 8.1 أو أحدث) عبر عميل HTTP المدمج. 1) أضف المفتاح إلى .env (لا ترفعه إلى المستودع أبدًا): UDEYOU_API_KEY=udy_test_... اختياري: UDEYOU_BASE_URL (الافتراضي https://udeyou.com/api/v1) 2) أضف المدخل udeyou إلى config/services.php. 3) أنشئ app/Services/Udeyou.php و app/Http/Controllers/OtpController.php. 4) أضف المسارات إلى routes/web.php وأنشئ resources/views/otp.blade.php. 5) شغّل php artisan serve ثم افتح /otp في المتصفح. 3) كيف يعمل المثال - الدوال Udeyou::send() و verify() و balance() تعيد كائن Illuminate\Http\Client\Response، فتستخدم ->failed() و ->status() و ->json('data.otp_id') أو ->json('error.code'). - المتحكم يحفظ otp_id في جلسة الخادم (لا في حقل مخفي)، ويعرض رسالة واضحة لكل error.code، ويحفظ data.to من رد التحقق بريدًا موثّقًا. - throttle:5,1 و throttle:10,1 تضيف حدودك الخاصة لكل مستخدم فوق حدود المنصة. - مع المفتاح التجريبي تعيد Udeyou::send() الرمز في data.code، وهذا مفيد في اختبارات Feature. 4) الملفات ----- BEGIN config/services.php ----- // config/services.php: add this entry to the returned array 'udeyou' => [ 'key' => env('UDEYOU_API_KEY'), // .env: UDEYOU_API_KEY=udy_test_... 'url' => env('UDEYOU_BASE_URL', 'https://udeyou.com/api/v1'), ], ----- END config/services.php ----- ----- BEGIN app/Services/Udeyou.php ----- http()->post('/otp/send', ['to' => $to] + $options + ['lang' => 'ar']); } /** POST /otp/verify. data: verified, otp_id, to, verified_at. */ public function verify(string $otpId, string $code): Response { return $this->http()->post('/otp/verify', ['otp_id' => $otpId, 'code' => $code]); } /** GET /balance. data: company, mode, credits. */ public function balance(): Response { return $this->http()->get('/balance'); } private function http(): PendingRequest { return Http::baseUrl(config('services.udeyou.url')) ->withToken(config('services.udeyou.key')) ->acceptJson() ->timeout(20); } } ----- END app/Services/Udeyou.php ----- ----- BEGIN app/Http/Controllers/OtpController.php ----- validate(['email' => ['required', 'email', 'max:190']]); try { $res = $this->udeyou->send($data['email']); } catch (ConnectionException) { return back()->withInput()->withErrors(['email' => 'The service is unavailable. Try again later.']); } if ($res->failed()) { return back()->withInput()->withErrors(['email' => $this->message($res)]); } // Keep otp_id in the server session, never in a hidden form field. $request->session()->put('otp_id', $res->json('data.otp_id')); return back()->with('status', 'We sent a code to your email.'); } // POST /otp/verify (code) public function verify(Request $request): RedirectResponse { $data = $request->validate(['code' => ['required', 'string', 'regex:/^[0-9A-Za-z]{4,10}$/']]); $otpId = $request->session()->get('otp_id'); if (! $otpId) { return back()->withErrors(['code' => 'Request a code first.']); } try { $res = $this->udeyou->verify($otpId, $data['code']); } catch (ConnectionException) { return back()->withErrors(['code' => 'The service is unavailable. Try again later.']); } if ($res->failed()) { return back()->withErrors(['code' => $this->message($res)]); } $request->session()->forget('otp_id'); // data.to is the verified address: sign the user in or mark the email as verified here. $request->session()->put('verified_email', $res->json('data.to')); return redirect()->intended('/'); } private function message(Response $res): string { return match ($res->json('error.code')) { 'invalid_code' => 'Incorrect code. Attempts left: '.$res->json('error.attempts_remaining'), 'resend_too_soon', 'recipient_limit', 'rate_limited' => 'Please wait '.$res->json('error.retry_after').' seconds and try again.', 'otp_expired', 'otp_already_used', 'otp_not_found', 'too_many_attempts' => 'This code is no longer valid. Request a new one.', 'validation_error' => 'Check the email address and try again.', // 401, 402, 403 and 5xx: log $res->json('error') and show a generic message. default => 'The service is unavailable. Try again later.', }; } } ----- END app/Http/Controllers/OtpController.php ----- ----- BEGIN routes/web.php ----- // routes/web.php (Route is already imported at the top of this file) use App\Http\Controllers\OtpController; Route::view('/otp', 'otp'); Route::post('/otp/send', [OtpController::class, 'send'])->middleware('throttle:5,1'); Route::post('/otp/verify', [OtpController::class, 'verify'])->middleware('throttle:10,1'); ----- END routes/web.php ----- ----- BEGIN resources/views/otp.blade.php ----- {{-- resources/views/otp.blade.php --}} @if (session('status'))

{{ session('status') }}

@endif
@csrf @error('email')

{{ $message }}

@enderror
@if (session('otp_id'))
@csrf @error('code')

{{ $message }}

@enderror
@endif ----- END resources/views/otp.blade.php ----- 5) مرجع الواجهة POST /otp/send -> 201 Created to نص إلزامي بريد المستلم، حتى 190 حرفًا. sender_name نص اختياري اسم المرسل في صندوق الوارد، من 2 إلى 60 حرفًا. الافتراضي اسم حسابك. lang نص اختياري ar (الافتراضي) أو en: لغة الرسالة الافتراضية والتذييل. code_length رقم اختياري من 4 إلى 10 خانات. الافتراضي 6. expires_in رقم اختياري الصلاحية بالثواني من 60 إلى 3600. الافتراضي 300. code نص اختياري رمز تحدده بنفسك من 4 إلى 10 أحرف أو أرقام، وإلا تولّد Udeyou رمزًا رقميًا. template نص اختياري نص عادي حتى 1000 حرف ويجب أن يحتوي {{code}}، ويمكن استخدام {{sender_name}} و {{minutes}}. subject نص اختياري حتى 120 حرفًا ويقبل المتغيرات نفسها. reply_to نص اختياري بريد يستقبل ردود العميل. أرسل code_length و expires_in كأرقام في JSON، فالقيمة النصية "6" تُرفض بالخطأ 422. إرسال رمز جديد إلى البريد نفسه يُلغي الرموز السابقة. وتنتهي كل رسالة بسطر يوضح أنها أُرسلت نيابةً عن sender_name عبر Udeyou OTP. data: otp_id و to و status ("sent") و mode ("live" أو "test") و expires_in و expires_at (ISO 8601 بتوقيت UTC) و credits_remaining، ومع المفتاح التجريبي test_mode و code (تعامل مع الرمز كنص فقد يبدأ بصفر). POST /otp/verify -> 200 OK code نص إلزامي الرمز الذي أدخله العميل (4 إلى 10 أحرف أو أرقام). otp_id نص موصى به المعرّف المعاد من /otp/send. to نص بديل بريد المستلم بدل otp_id، ويُتحقق من آخر رمز أُرسل إليه. data: verified (true) و otp_id و to و verified_at. اعتمد data.to بريدًا موثّقًا. الرمز يُستخدم مرة واحدة، وينتهي بعد expires_in ثانية، ويُقفل بعد 5 محاولات خاطئة. GET /balance -> 200 OK data: company و mode و credits. GET /health -> 200 OK (دون مفتاح) {"success": true, "status": "ok"} النجاح: {"success": true, "data": {...}} الفشل: {"success": false, "error": {"code": "...", "message": "...", ...}} 6) أكواد الأخطاء (اعتمد على error.code، أما message فنص توضيحي بالإنجليزية) 400 invalid_json جسم الطلب ليس كائن JSON. 400 invalid_code الرمز خاطئ، و attempts_remaining = المحاولات المتبقية. 401 invalid_api_key المفتاح مفقود أو بصيغة خاطئة أو غير معروف أو ملغى. 402 insufficient_credits مفتاح حي والرصيد صفر. 403 account_suspended الحساب موقوف. 404 otp_not_found لا يوجد رمز مطابق لـ otp_id أو to ضمن نوع المفتاح. 404 not_found المسار غير موجود. 405 method_not_allowed طريقة HTTP غير مدعومة لهذا المسار. 409 otp_already_used الرمز استُخدم مسبقًا. 410 otp_expired انتهت الصلاحية، أو أُرسل رمز أحدث إلى البريد نفسه. 422 validation_error حقول غير صالحة (التفاصيل في error.fields عند /otp/send). 429 rate_limited أكثر من 60 طلب إرسال في الدقيقة (retry_after). 429 resend_too_soon أقل من 60 ثانية منذ آخر رمز لهذا البريد (retry_after). 429 recipient_limit 5 رموز لهذا البريد خلال الساعة الأخيرة (retry_after). 429 too_many_attempts استُنفدت المحاولات (5)، اطلب رمزًا جديدًا. 500 server_error خطأ غير متوقع في الخادم. 502 delivery_failed تعذّر تسليم البريد، وأُعيد الرصيد. 7) الحدود (لكل حساب، ويتشاركها المفتاح الحي والتجريبي) - طلبات الإرسال: 60 في الدقيقة (rate_limited و retry_after 60). - الفاصل بين رمزين للبريد نفسه: 60 ثانية (resend_too_soon و retry_after 60). - الرموز للبريد نفسه: 5 في الساعة (recipient_limit و retry_after 3600). - المحاولات الخاطئة: 5 لكل رمز (too_many_attempts، ثم اطلب رمزًا جديدًا). - صلاحية الرمز: 300 ثانية افتراضيًا، ومن 60 إلى 3600 عبر expires_in (otp_expired). طلبات الإرسال وحدها تدخل في حد الدقيقة. وقيمة retry_after هي طول النافذة بالثواني وليست الوقت المتبقي: انتظرها ثم أعد المحاولة. 8) أفضل الممارسات - احفظ المفتاح في متغير بيئة على الخادم فقط. - المفتاح التجريبي للتطوير والاختبار الآلي، والحي للإنتاج فقط. - احفظ otp_id في جلسة المستخدم على الخادم وتحقق به. - اعتمد data.to من رد التحقق، لا البريد المرسل من التطبيق. - تعامل مع الرمز كنص، فقد يبدأ بصفر. - اعرض attempts_remaining، وعطّل زر إعادة الإرسال 60 ثانية بعد كل إرسال. - اضبط مهلة لكل طلب، ولا تعرض تفاصيل أخطاء الشبكة للمستخدم. - ألغِ أي مفتاح يُشتبه في تسرّبه من لوحة التحكم ثم أنشئ مفتاحًا جديدًا. - لا تسجّل الرموز أو المفاتيح في السجلات. الدعم: https://udeyou.com/contact (أرفق otp_id وكود الخطأ إن وُجد)