*/ use HasFactory; /** * A code is only good for this long — kept short since it's delivered * over email/SMS and re-requesting a fresh one is cheap (throttled by * the api-otp rate limiter). */ private const CODE_LIFETIME_MINUTES = 10; /** * Wrong-code guesses allowed before the code is locked out and a fresh * one must be requested. */ private const MAX_ATTEMPTS = 5; /** * @var list */ protected $fillable = [ 'identifier', 'type', 'code', 'attempts', 'verified_at', 'verification_token', 'consumed_at', 'expires_at', ]; /** * @return array */ protected function casts(): array { return [ 'verified_at' => 'datetime', 'consumed_at' => 'datetime', 'expires_at' => 'datetime', ]; } /** * Generates a fresh code for the identifier and delivers it over * email or SMS, replacing any previous pending verification for the * same identifier (resend just supersedes the old code). */ public static function issueFor(string $identifier, string $type): self { $code = (string) random_int(100000, 999999); $verification = self::query()->updateOrCreate( ['identifier' => $identifier], [ 'type' => $type, 'code' => Hash::make($code), 'attempts' => 0, 'verified_at' => null, 'verification_token' => null, 'consumed_at' => null, 'expires_at' => now()->addMinutes(self::CODE_LIFETIME_MINUTES), ], ); $verification->deliver($code); return $verification; } /** * Checks the given code against this pending verification. On success, * marks it verified and issues the one-time token step 3 (registration) * will need to complete the flow. */ public function attemptVerify(string $code): bool { if ($this->isExpired() || $this->attempts >= self::MAX_ATTEMPTS || ! Hash::check($code, $this->code)) { $this->increment('attempts'); return false; } $this->forceFill([ 'verified_at' => now(), 'verification_token' => Str::random(64), ])->save(); return true; } public function isExpired(): bool { return $this->expires_at->isPast(); } public function isVerified(): bool { return $this->verified_at !== null; } public function isConsumed(): bool { return $this->consumed_at !== null; } private function deliver(string $code): void { if ($this->type === 'email') { Mail::to($this->identifier)->send(new RegistrationCodeMail($code)); return; } $appName = config('app.name'); app(SmsService::class)->send( $this->identifier, "{$appName}: Your verification code is {$code}. It expires in ".self::CODE_LIFETIME_MINUTES.' minutes.', ); } }