validate([ 'pin' => ['required','string','regex:/^\d{4,8}$/'], 'current_pin' => ['sometimes','nullable','string','regex:/^\d{4,8}$/'], ]); // If PIN already set, require current_pin and verify if (!empty($user->vault_pin_hash)) { if (empty($data['current_pin']) || !Hash::check((string) $data['current_pin'], $user->vault_pin_hash)) { return response()->json(['message' => 'Current PIN invalid'], 400); } } $user->vault_pin_hash = Hash::make((string) $data['pin']); $user->vault_pin_set_at = now(); $user->vault_pin_attempts = 0; $user->vault_pin_locked_until = null; $user->save(); return response()->json([ 'success' => true, 'message' => 'Vault PIN saved.', ], 200); } /** * POST /api/wallet/vault/pin/verify — local implementation */ public function verify(Request $request) { $user = Auth::user(); abort_unless($user, 403); $data = $request->validate([ 'pin' => ['required','string','regex:/^\d{4,8}$/'], ]); // Locked? if (!empty($user->vault_pin_locked_until) && now()->lessThan($user->vault_pin_locked_until)) { return response()->json([ 'success' => false, 'message' => 'Vault PIN locked. Try again later.', 'locked_until' => optional($user->vault_pin_locked_until)->toIso8601String(), ], 423); } if (empty($user->vault_pin_hash)) { return response()->json(['success' => false, 'message' => 'Vault PIN not set'], 400); } if (!Hash::check((string) $data['pin'], $user->vault_pin_hash)) { $attempts = (int) ($user->vault_pin_attempts ?? 0) + 1; $user->vault_pin_attempts = $attempts; if ($attempts >= 5) { $user->vault_pin_locked_until = now()->addMinutes(15); $user->vault_pin_attempts = 0; } $user->save(); return response()->json(['success' => false, 'message' => 'Invalid PIN'], 423); } // OK if (!empty($user->vault_pin_attempts)) { $user->vault_pin_attempts = 0; $user->save(); } return response()->json([ 'success' => true, 'message' => 'Verified', ], 200); } }