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 (Cache::get($key, 0) >= 3) { return $this->failure('Too many OTP requests', 429); } Cache::increment($key); Cache::put($key, Cache::get($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) $passenger = DB::connection('primary')->table('passengers') ->where('phone', $phone) ->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', // 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], ]); } }