validate(['phone' => 'required|string']); $phone = $request->input('phone'); // Rate limit: 3 OTP per phone per 5 minutes $key = "otp_limit:{$phone}"; if (Cache::get($key, 0) >= 3) { return response()->json(['status' => 'failure', 'message' => '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); $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(), ] ); // TODO: Send SMS via external provider // For now, return success (SMS sending is provider-specific) return response()->json([ 'status' => 'success', 'message' => 'OTP sent', 'expires_at' => $expiration->toIso8601String(), ]); } /** POST /v2/otp/verify */ public function verify(Request $request): JsonResponse { $request->validate([ 'phone' => 'required|string', 'otp' => 'required|string|size:6', ]); $phone = $request->input('phone'); $otp = $request->input('otp'); $record = DB::connection('primary')->table('phone_verification') ->where('phone_number', $phone) ->where('is_verified', 0) ->where('expiration_time', '>', now()) ->first(); if (!$record) { return response()->json(['status' => 'failure', 'message' => 'OTP expired or not found'], 400); } // 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']); } /** 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(), ] ); // TODO: Send email with token link return response()->json(['status' => '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')) ->where('verified', 0) ->first(); if (!$record || !password_verify($request->input('token'), $record->token)) { return response()->json(['status' => 'failure', 'message' => 'Invalid verification'], 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']); } /** 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], ]); } }