email); $lastIssuedAt = DB::table(self::TABLE)->where('email', $email)->max('created_at'); if ($lastIssuedAt && now()->subSeconds(self::throttleSeconds())->lt($lastIssuedAt)) { return null; } $body = ''; for ($i = 0; $i < self::LENGTH - 1; $i++) { $body .= self::ALPHABET[random_int(0, strlen(self::ALPHABET) - 1)]; } $token = $body . self::checkCharacter($body); DB::transaction(function () use ($email, $token) { DB::table(self::TABLE)->where('email', $email)->delete(); DB::table(self::TABLE)->insert([ 'email' => $email, 'token' => self::hash($token), 'created_at' => now(), ]); }); return $token; } public static function isWellFormed(string $token): bool { if (strlen($token) !== self::LENGTH || strspn($token, self::ALPHABET) !== self::LENGTH) { return false; } return self::checkCharacter(substr($token, 0, -1)) === $token[self::LENGTH - 1]; } public static function findCustomer(string $token): ?Customer { if (!self::isWellFormed($token)) { return null; } $row = DB::table(self::TABLE) ->where('token', self::hash($token)) ->where('created_at', '>=', now()->subMinutes(self::expireMinutes())) ->first(); if (!$row) { return null; } return Customer::whereRaw('LOWER(email) = ?', [$row->email])->first(); } public static function revokeFor(Customer $customer): void { DB::table(self::TABLE)->where('email', strtolower($customer->email))->delete(); } private static function checkCharacter(string $body): string { $base = strlen(self::ALPHABET); $sum = 0; $double = true; for ($i = strlen($body) - 1; $i >= 0; $i--) { $value = strpos(self::ALPHABET, $body[$i]); if ($double) { $value *= 2; $value = intdiv($value, $base) + $value % $base; } $sum += $value; $double = !$double; } return self::ALPHABET[($base - $sum % $base) % $base]; } private static function hash(string $token): string { return hash('sha256', $token); } private static function expireMinutes(): int { return (int) config('auth.passwords.users.expire', 60); } private static function throttleSeconds(): int { return (int) config('auth.passwords.users.throttle', 60); } }