refactor: update OTP system to support user-specific verification tables with legacy encryption and multi-role authentication.
This commit is contained in:
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user