refactor: update OTP system to support user-specific verification tables with legacy encryption and multi-role authentication.

This commit is contained in:
Hamza-Ayed
2026-04-23 17:03:38 +03:00
parent d64a423db9
commit 098aa9ad37
10 changed files with 649 additions and 189 deletions

View File

@@ -7,56 +7,96 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use App\Helpers\LegacyEncryption;
/**
* متحكم رموز التحقق (OTP Controller)
*
* الغرض من الملف:
* إدارة إرسال والتحقق من رموز الـ OTP (التي تصل عبر SMS أو البريد الإلكتروني) لضمان ملكية المستخدم للحساب.
*
* كيفية العمل:
* 1. يستقبل رقم الهاتف أو البريد الإلكتروني.
* 2. يولد رمزاً عشوائياً ويرسله عبر الخدمة المناسبة.
* 3. يخزن الرمز مؤقتاً للتحقق منه لاحقاً عند إدخاله من قبل المستخدم.
*/
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']);
$request->validate([
'phone' => 'required|string',
'user_type' => 'nullable|in:passenger,driver,admin',
]);
$phone = $request->input('phone');
$userType = $request->input('user_type', 'passenger');
// Rate limit: 3 OTP per phone per 5 minutes
$key = "otp_limit:{$phone}";
$key = "otp_limit_{$userType}:{$phone}";
if (Cache::get($key, 0) >= 3) {
return response()->json(['status' => 'failure', 'message' => 'Too many OTP requests'], 429);
return $this->failure('Too many OTP requests', 429);
}
Cache::increment($key);
Cache::put($key, Cache::get($key), 300);
// Generate 6-digit OTP
$otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
// Generate 5-digit OTP
$otp = (string) random_int(10000, 99999);
$expiration = now()->addMinutes(5);
// Store OTP
DB::connection('primary')->table('phone_verification')->updateOrInsert(
['phone_number' => $phone],
[
'token_code' => password_hash($otp, PASSWORD_BCRYPT),
'expiration_time' => $expiration,
'is_verified' => 0,
'created_at' => now(),
]
);
// 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);
// TODO: Send SMS via external provider
// For now, return success (SMS sending is provider-specific)
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;
return response()->json([
'status' => 'success',
'message' => 'OTP sent',
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);
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;
}
// TODO: Send SMS/WhatsApp via external provider
return $this->success([
'message' => 'OTP sent successfully',
'expires_at' => $expiration->toIso8601String(),
]);
}
@@ -66,33 +106,98 @@ class OtpController extends Controller
{
$request->validate([
'phone' => 'required|string',
'otp' => 'required|string|size:6',
'otp' => 'required|string',
'user_type' => 'nullable|in:passenger,driver,admin',
'device_number' => 'nullable|string', // Used for admin
]);
$phone = $request->input('phone');
$otp = $request->input('otp');
$userType = $request->input('user_type', 'passenger');
$deviceNumber = $request->input('device_number', '');
$record = DB::connection('primary')->table('phone_verification')
->where('phone_number', $phone)
->where('is_verified', 0)
->where('expiration_time', '>', now())
->first();
switch ($userType) {
case 'driver':
$table = 'token_verification_driver';
$encPhone = $this->encryption->encrypt($phone);
$encOtp = $this->encryption->encrypt($otp);
if (!$record) {
return response()->json(['status' => 'failure', 'message' => 'OTP expired or not found'], 400);
$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;
}
// Verify OTP hash
if (!password_verify($otp, $record->token_code)) {
return response()->json(['status' => 'failure', 'message' => 'Invalid OTP'], 400);
}
// Mark as verified
DB::connection('primary')->table('phone_verification')
->where('phone_number', $phone)
->update(['is_verified' => 1]);
return response()->json(['status' => 'success', 'message' => 'Phone verified']);
return $this->success([
'message' => 'Phone verified successfully'
]);
}
/** POST /v2/otp/email/send */
@@ -113,9 +218,7 @@ class OtpController extends Controller
]
);
// TODO: Send email with token link
return response()->json(['status' => 'success', 'message' => 'Verification email sent']);
return $this->success(['message' => 'Verification email sent']);
}
/** POST /v2/otp/email/verify */
@@ -128,18 +231,17 @@ class OtpController extends Controller
$record = DB::connection('primary')->table('email_verifications')
->where('email', $request->input('email'))
->where('verified', 0)
->first();
if (!$record || !password_verify($request->input('token'), $record->token)) {
return response()->json(['status' => 'failure', 'message' => 'Invalid verification'], 400);
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 response()->json(['status' => 'success', 'message' => 'Email verified']);
return $this->success(['message' => 'Email verified']);
}
/** GET /v2/otp/check-phone?phone=XXX */