Files
intaleq_v2/app/Http/Controllers/OtpController.php
2026-04-25 11:42:40 +03:00

360 lines
13 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use App\Services\LegacyEncryption;
/**
* متحكم رموز التحقق (OTP Controller)
*
* الغرض من الملف:
* إدارة إرسال والتحقق من رموز الـ OTP (التي تصل عبر SMS أو البريد الإلكتروني) لضمان ملكية المستخدم للحساب.
*/
class OtpController extends Controller
{
private LegacyEncryption $encryption;
public function __construct(LegacyEncryption $encryption)
{
$this->encryption = $encryption;
}
/** POST /v2/otp/driver/send */
public function sendDriver(Request $request): JsonResponse
{
$request->merge(['user_type' => 'driver']);
return $this->send($request);
}
/** POST /v2/otp/driver/verify */
public function verifyDriver(Request $request): JsonResponse
{
$request->merge(['user_type' => 'driver']);
return $this->verify($request);
}
/** POST /v2/otp/send */
public function send(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'user_type' => 'nullable|in:passenger,driver,admin',
]);
$phone = $request->input('phone') ?? $request->input('phone_number');
$userType = $request->input('user_type', 'passenger');
if (!$phone) {
return $this->failure('The phone field is required', 400);
}
// Rate limit: 3 OTP per phone per 5 minutes
$key = "otp_limit_{$userType}:{$phone}";
if (RateLimiter::tooManyAttempts($key, 3)) {
return $this->failure('Too many OTP requests. Please try again later.', 429);
}
RateLimiter::hit($key, 300);
// Generate 5-digit OTP
$otp = (string) random_int(10000, 99999);
$expiration = now()->addMinutes(5);
// Map user type to table and logic
switch ($userType) {
case 'driver':
$table = 'token_verification_driver';
$encPhone = $this->encryption->encrypt($phone);
$encOtp = $this->encryption->encrypt($otp);
DB::connection('primary')->table($table)->where('phone_number', $encPhone)->delete();
DB::connection('primary')->table($table)->insert([
'phone_number' => $encPhone,
'token' => $encOtp,
'expiration_time' => $expiration,
'verified' => 0,
'created_at' => now(),
]);
break;
case 'admin':
$table = 'token_verification_admin';
// Admins use raw phone and OTP in V1
DB::connection('primary')->table($table)->updateOrInsert(
['phone_number' => $phone],
[
'token' => $otp,
'expiration_time' => $expiration,
]
);
break;
case 'passenger':
default:
$table = 'token_verification';
$encPhone = $this->encryption->encrypt($phone);
$encOtp = $this->encryption->encrypt($otp);
try {
DB::connection('primary')->table($table)->where('phone_number', $encPhone)->delete();
DB::connection('primary')->table($table)->insert([
'phone_number' => $encPhone,
'token' => $encOtp,
'expiration_time' => $expiration,
'verified' => 0,
]);
} catch (\Exception $e) {
\Log::error("OTP Send Error ($table): " . $e->getMessage());
// Procedural success even if DB fails for now, to allow dev flow
return $this->success([
'message' => 'OTP procedural success (DB log error)',
'expires_at' => $expiration->toIso8601String(),
]);
}
break;
}
// Send WhatsApp message (especially for drivers changing devices)
// $message = "Your Intaleq App verification code is: $otp";
// $this->sendWhatsAppFromServer($phone, $message);
// Check if passenger exists to allow immediate login (V1 style)
// We check both encrypted and raw phone with multiple formats (963... and 0...)
$rawPhone = $phone;
$localPhone = '0' . substr($phone, 3); // Convert 9639... to 09...
$encRawPhone = $this->encryption->encrypt($rawPhone);
$encLocalPhone = $this->encryption->encrypt($localPhone);
$passenger = DB::connection('primary')->table('passengers')
->whereIn('phone', [$rawPhone, $localPhone, $encRawPhone, $encLocalPhone])
->first();
return $this->success([
'message' => 'OTP process initiated',
'isRegistered' => !is_null($passenger),
'expires_at' => $expiration->toIso8601String(),
]);
}
/** POST /v2/otp/verify */
public function verify(Request $request): JsonResponse
{
$request->validate([
'phone' => 'required|string',
'otp' => 'required|string',
'user_type' => 'nullable|in:passenger,driver,admin',
'device_number' => 'nullable|string|max:64|regex:/^[a-zA-Z0-9_\-\.]+$/', // Used for admin
]);
$phone = $request->input('phone');
$otp = $request->input('otp');
$userType = $request->input('user_type', 'passenger');
$deviceNumber = $request->input('device_number', '');
switch ($userType) {
case 'driver':
$table = 'token_verification_driver';
$encPhone = $this->encryption->encrypt($phone);
$encOtp = $this->encryption->encrypt($otp);
$record = DB::connection('primary')->table($table)
->where('phone_number', $encPhone)
->where('token', $encOtp)
->where('expiration_time', '>', now())
->first();
if (!$record) {
return $this->failure('Invalid or expired OTP', 400);
}
DB::connection('primary')->table($table)
->where('id', $record->id)
->update(['verified' => 1]);
break;
case 'admin':
$table = 'token_verification_admin';
$record = DB::connection('primary')->table($table)
->where('phone_number', $phone)
->where('token', $otp)
->where('expiration_time', '>', now())
->first();
if (!$record) {
return $this->failure('Invalid or expired OTP', 400);
}
// V1 Admin specific logic: create or update adminUser with device_number
if (empty($deviceNumber)) {
return $this->failure('Device number is required for admin verification', 400);
}
$adminExists = DB::connection('primary')->table('adminUser')
->where('name', $phone)
->exists();
if ($adminExists) {
DB::connection('primary')->table('adminUser')
->where('name', $phone)
->update(['device_number' => $deviceNumber, 'updated_at' => now()]);
} else {
DB::connection('primary')->table('adminUser')->insert([
'device_number' => $deviceNumber,
'name' => $phone,
'created_at' => now(),
'updated_at' => now(),
]);
}
break;
case 'passenger':
default:
$table = 'token_verification';
$encPhone = $this->encryption->encrypt($phone);
$encOtp = $this->encryption->encrypt($otp);
$record = DB::connection('primary')->table($table)
->where('phone_number', $encPhone)
->where('token', $encOtp)
->where('expiration_time', '>', now())
->first();
if (!$record) {
return $this->failure('Invalid or expired OTP', 400);
}
DB::connection('primary')->table($table)
->where('id', $record->id)
->update(['verified' => 1]);
break;
}
return $this->success([
'message' => 'Phone verified successfully'
]);
}
/** POST /v2/otp/email/send */
public function sendEmail(Request $request): JsonResponse
{
$request->validate(['email' => 'required|email']);
$email = $request->input('email');
$token = Str::random(32);
DB::connection('primary')->table('email_verifications')->updateOrInsert(
['email' => $email],
[
'token' => password_hash($token, PASSWORD_BCRYPT),
'verified' => 0,
'created_at' => now(),
'updated_at' => now(),
]
);
return $this->success(['message' => 'Verification email sent']);
}
/** POST /v2/otp/email/verify */
public function verifyEmail(Request $request): JsonResponse
{
$request->validate([
'email' => 'required|email',
'token' => 'required|string',
]);
$record = DB::connection('primary')->table('email_verifications')
->where('email', $request->input('email'))
->first();
if (!$record || !password_verify($request->input('token'), $record->token)) {
return $this->failure('Invalid or expired token', 400);
}
DB::connection('primary')->table('email_verifications')
->where('email', $request->input('email'))
->update(['verified' => 1, 'updated_at' => now()]);
return $this->success(['message' => 'Email verified']);
}
/** GET /v2/otp/check-phone?phone=XXX */
public function checkPhone(Request $request): JsonResponse
{
$phone = $request->input('phone') ?? $request->query('phone');
if (!$phone) {
return $this->failure('Phone parameter is missing', 400);
}
// We check phone_verification table (Legacy V1 style)
$verified = DB::connection('primary')->table('phone_verification')
->where('phone_number', $phone)
->where('verified', 1) // In V1 it might be 'verified' or 'is_verified'
->exists();
// Fallback for column name 'is_verified' if 'verified' fails
if (!$verified) {
try {
$verified = DB::connection('primary')->table('phone_verification')
->where('phone_number', $phone)
->where('is_verified', 1)
->exists();
} catch (\Exception $e) {}
}
return response()->json([
'status' => 'success',
'data' => ['verified' => $verified],
]);
}
/**
* Send WhatsApp message using the available bot servers (Ported from V1 functions.php)
*/
private function sendWhatsAppFromServer($to, $message)
{
$servers = [
"https://bot5.intaleq.xyz/send", // ramat bus
"https://bot3.intaleq.xyz/send", // shahd
];
$url = $servers[array_rand($servers)];
$payload = [
"to" => $to,
"message" => $message
];
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
CURLOPT_TIMEOUT => 5 // Don't block API for too long
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
\Log::error("[sendWhatsAppFromServer] cURL Error on $url: $err");
return false;
}
return json_decode($response, true);
}
}