where('destination', $destination) ->where('purpose', $purpose) ->latest('id') ->first(); if ($latest && $latest->created_at->diffInSeconds(now()) < $cooldown && ! $latest->isConsumed()) { $retryAfter = $cooldown - (int) $latest->created_at->diffInSeconds(now()); return OtpIssueResult::throttled(max(1, $retryAfter)); } $code = $this->generateCode(); $ttl = (int) config('services.otp.ttl_minutes', 10); $otp = OtpCode::create([ 'user_id' => $user?->id, 'channel' => $channel, 'destination' => $destination, 'purpose' => $purpose, 'code_hash' => Hash::make($code), 'attempts' => 0, 'expires_at' => now()->addMinutes($ttl), ]); $message = $this->renderMessage($purpose, $code, $ttl); $sendResult = $channel === OtpCode::CHANNEL_SMS ? $this->sms->send($destination, $message) : null; return OtpIssueResult::issued($otp, $code, $sendResult); } public function verify(string $destination, string $purpose, string $code): OtpVerifyResult { $otp = OtpCode::query() ->where('destination', $destination) ->where('purpose', $purpose) ->whereNull('consumed_at') ->latest('id') ->first(); if (! $otp) { return OtpVerifyResult::notFound(); } if ($otp->isExpired()) { return OtpVerifyResult::expired(); } if ($otp->isExhausted()) { return OtpVerifyResult::exhausted(); } $otp->increment('attempts'); if (! Hash::check($code, $otp->code_hash)) { return OtpVerifyResult::invalid($otp->fresh()); } $otp->forceFill(['consumed_at' => now()])->save(); return OtpVerifyResult::ok($otp); } private function generateCode(): string { $length = max(4, (int) config('services.otp.length', 6)); $max = (10 ** $length) - 1; return str_pad((string) random_int(0, $max), $length, '0', STR_PAD_LEFT); } private function renderMessage(string $purpose, string $code, int $ttlMinutes): string { $reason = match ($purpose) { OtpCode::PURPOSE_REGISTER => 'verify your Verde registration', OtpCode::PURPOSE_LOGIN => 'log in to Verde', OtpCode::PURPOSE_PASSWORD_RESET => 'reset your Verde password', OtpCode::PURPOSE_PHONE_VERIFY => 'verify your Verde phone number', default => 'continue with Verde', }; return "Your Verde code is {$code} (valid {$ttlMinutes} min). Use to {$reason}. Do not share."; } }