289 lines
10 KiB
PHP
289 lines
10 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/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;
|
|
}
|
|
|
|
// TODO: Send SMS/WhatsApp via external provider
|
|
|
|
// 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),
|
|
'passenger' => $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
|
|
{
|
|
$request->validate(['phone' => 'required|string']);
|
|
|
|
$verified = DB::connection('primary')->table('phone_verification')
|
|
->where('phone_number', $request->input('phone'))
|
|
->where('is_verified', 1)
|
|
->exists();
|
|
|
|
return response()->json([
|
|
'status' => 'success',
|
|
'data' => ['verified' => $verified],
|
|
]);
|
|
}
|
|
}
|