2FAuth/app/Models/TwoFAccount.php

668 lines
21 KiB
PHP
Raw Normal View History

2019-05-20 07:37:41 +02:00
<?php
2021-12-02 13:15:53 +01:00
namespace App\Models;
2019-05-20 07:37:41 +02:00
use App\Events\TwoFAccountDeleted;
use App\Exceptions\InvalidOtpParameterException;
2022-11-22 15:15:52 +01:00
use App\Exceptions\InvalidSecretException;
use App\Exceptions\UndecipherableException;
2022-11-22 15:15:52 +01:00
use App\Exceptions\UnsupportedOtpTypeException;
2024-10-18 14:28:45 +02:00
use App\Facades\Icons;
2022-11-22 15:15:52 +01:00
use App\Helpers\Helpers;
use App\Models\Dto\HotpDto;
use App\Models\Dto\TotpDto;
2024-10-18 14:28:45 +02:00
use App\Models\Traits\CanEncryptField;
2024-09-26 23:17:00 +02:00
use Database\Factories\TwoFAccountFactory;
2022-11-22 15:15:52 +01:00
use Illuminate\Database\Eloquent\Factories\HasFactory;
2019-05-20 07:37:41 +02:00
use Illuminate\Database\Eloquent\Model;
2024-10-18 14:28:45 +02:00
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
2021-10-15 23:46:21 +02:00
use Illuminate\Support\Facades\Log;
2022-11-22 15:15:52 +01:00
use Illuminate\Validation\ValidationException;
use OTPHP\Factory;
use OTPHP\HOTP;
use OTPHP\TOTP;
use ParagonIE\ConstantTime\Base32;
2022-11-22 15:15:52 +01:00
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;
use SteamTotp\SteamTotp;
2019-05-20 07:37:41 +02:00
2023-03-02 15:24:57 +01:00
/**
* App\Models\TwoFAccount
*
* @property int $id
* @property string|null $service
* @property string $legacy_uri
* @property string $account
* @property string|null $icon
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property int|null $order_column
* @property int|null $group_id
* @property string $otp_type
* @property string $secret
* @property string $algorithm
* @property int $digits
* @property int|null $period
* @property int|null $counter
* @property int|null $user_id
* @property-read \App\Models\User|null $user
2024-09-26 23:17:00 +02:00
*
2024-07-05 16:34:25 +02:00
* @method static \Database\Factories\TwoFAccountFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount ordered(string $direction = 'asc')
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount query()
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereAccount($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereAlgorithm($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereCounter($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereDigits($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereGroupId($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereIcon($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereLegacyUri($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereOrderColumn($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereOtpType($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount wherePeriod($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereSecret($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereService($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount whereUserId($value)
2024-11-09 10:18:45 +01:00
*
2024-06-26 13:53:23 +02:00
* @mixin \Eloquent
2024-11-09 10:18:45 +01:00
*
2024-10-18 14:28:45 +02:00
* @property-read \App\Models\Icon|null $iconResource
2024-11-09 10:18:45 +01:00
*
2024-10-18 14:28:45 +02:00
* @method static \Illuminate\Database\Eloquent\Builder|TwoFAccount orphans()
2023-03-02 15:24:57 +01:00
*/
2020-03-25 21:58:05 +01:00
class TwoFAccount extends Model implements Sortable
2019-05-20 07:37:41 +02:00
{
2024-09-26 23:17:00 +02:00
/**
* @use HasFactory<TwoFAccountFactory>
*/
2024-11-09 10:18:45 +01:00
use CanEncryptField, HasFactory, SortableTrait;
2020-03-25 21:58:05 +01:00
2022-11-22 15:15:52 +01:00
const TOTP = 'totp';
const HOTP = 'hotp';
const STEAM_TOTP = 'steamtotp';
2022-11-22 15:15:52 +01:00
const SHA1 = 'sha1';
const MD5 = 'md5';
const SHA256 = 'sha256';
const SHA512 = 'sha512';
const DEFAULT_PERIOD = 30;
2022-11-22 15:15:52 +01:00
const DEFAULT_COUNTER = 0;
2022-11-22 15:15:52 +01:00
const DEFAULT_DIGITS = 6;
2022-11-22 15:15:52 +01:00
const DEFAULT_ALGORITHM = self::SHA1;
2022-10-10 13:44:12 +02:00
const DUPLICATE_ID = -1;
2022-11-22 15:15:52 +01:00
2022-10-10 13:44:12 +02:00
const FAKE_ID = -2;
/**
* List of OTP types supported by 2FAuth
*/
private array $generatorClassMap = [
'OTPHP\TOTP' => self::TOTP,
'OTPHP\HOTP' => self::HOTP,
];
2020-03-25 21:58:05 +01:00
2020-01-10 13:43:36 +01:00
/**
* model's array form.
*
2024-04-20 19:03:44 +02:00
* @var array<int, string>
2020-01-10 13:43:36 +01:00
*/
protected $fillable = [];
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'twofaccounts';
2020-01-08 17:03:41 +01:00
2020-01-24 12:56:38 +01:00
/**
* The accessors to append to the model's array form.
*
2024-04-20 19:03:44 +02:00
* @var array<int, string>
2020-01-24 12:56:38 +01:00
*/
public $appends = [];
2022-11-22 15:15:52 +01:00
/**
* The model's attributes.
2022-11-22 15:15:52 +01:00
*
* @var array
*/
protected $attributes = [
2022-11-22 15:15:52 +01:00
'digits' => 6,
'algorithm' => self::SHA1,
];
2020-01-24 12:56:38 +01:00
2020-11-06 15:51:52 +01:00
/**
* The attributes that should be hidden for arrays.
*
* @var array<int, string>
2020-11-06 15:51:52 +01:00
*/
protected $hidden = [];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'user_id' => 'integer',
];
2020-11-06 15:51:52 +01:00
/**
* The event map for the model.
*
* @var array
*/
protected $dispatchesEvents = [
'deleted' => TwoFAccountDeleted::class,
];
/**
* Override The "booting" method of the model
*
* @return void
*/
protected static function boot()
{
parent::boot();
static::saving(function (TwoFAccount $twofaccount) {
2022-12-13 12:07:29 +01:00
if (! $twofaccount->legacy_uri) {
2022-11-22 15:15:52 +01:00
$twofaccount->legacy_uri = $twofaccount->getURI();
}
2022-12-13 12:07:29 +01:00
if ($twofaccount->otp_type == TwoFAccount::TOTP && ! $twofaccount->period) {
2022-11-22 15:15:52 +01:00
$twofaccount->period = TwoFAccount::DEFAULT_PERIOD;
}
2022-12-13 12:07:29 +01:00
if ($twofaccount->otp_type == TwoFAccount::HOTP && ! $twofaccount->counter) {
2022-11-22 15:15:52 +01:00
$twofaccount->counter = TwoFAccount::DEFAULT_COUNTER;
}
});
static::created(function (object $model) {
Log::info(sprintf('TwoFAccount ID #%d created for user ID #%s', $model->id, $model->user_id));
});
static::updated(function (object $model) {
Log::info(sprintf('TwoFAccount ID #%d updated by user ID #%s', $model->id, $model->user_id));
});
static::deleted(function (object $model) {
Log::info(sprintf('TwoFAccount ID #%d deleted ', $model->id));
});
}
2020-03-25 21:58:05 +01:00
/**
* Settings for @spatie/eloquent-sortable package
2020-03-25 21:58:05 +01:00
*
* @var array
*/
public $sortable = [
2022-11-22 15:15:52 +01:00
'order_column_name' => 'order_column',
2020-03-25 21:58:05 +01:00
'sort_when_creating' => true,
];
/**
* The OTP generator.
* Instanciated as null to keep the model light
*
* @var \OTPHP\OTPInterface|null
*/
protected $generator = null;
/**
* Get the user that owns the twofaccount.
*
2024-11-23 14:29:31 +01:00
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\App\Models\User, $this>
*/
public function user()
{
return $this->belongsTo(\App\Models\User::class);
}
2024-10-18 14:28:45 +02:00
/**
* Get the relation between the icon resource and the model.
2024-11-09 10:18:45 +01:00
*
2024-11-23 14:29:31 +01:00
* @return HasOne<\App\Models\Icon, $this>
2024-10-18 14:28:45 +02:00
*/
2024-11-09 10:18:45 +01:00
public function iconResource() : HasOne
2024-10-18 14:28:45 +02:00
{
return $this->hasOne(Icon::class, 'name', 'icon');
}
/**
* Scope a query to only include orphan (userless) accounts.
*
* @param \Illuminate\Database\Eloquent\Builder<User> $query
* @return \Illuminate\Database\Eloquent\Builder<User>
*/
public function scopeOrphans($query)
{
return $query->where('user_id', null);
}
2020-01-24 12:56:38 +01:00
/**
* Get legacy_uri attribute
*
* @param string $value
* @return string
*/
public function getLegacyUriAttribute($value)
2020-01-24 12:56:38 +01:00
{
return $this->decryptOrReturn($value);
2020-01-24 12:56:38 +01:00
}
2022-11-22 15:15:52 +01:00
/**
* Set legacy_uri attribute
*
* @param string $value
* @return void
*/
public function setLegacyUriAttribute($value)
{
2020-11-18 01:13:00 +01:00
// Encrypt if needed
$this->attributes['legacy_uri'] = $this->encryptOrReturn($value);
}
/**
2020-11-18 01:13:00 +01:00
* Get account attribute
*
* @param string $value
* @return string
*/
public function getAccountAttribute($value)
{
return $this->decryptOrReturn($value);
}
2022-11-22 15:15:52 +01:00
/**
* Set account attribute
*
2022-11-22 15:15:52 +01:00
* @param string $value
* @return void
*/
public function setAccountAttribute($value)
{
2020-11-18 01:13:00 +01:00
// Encrypt when needed
$this->attributes['account'] = $this->encryptOrReturn($value);
}
/**
* Get service attribute
*
* @param string $value
* @return string
*/
public function getServiceAttribute($value)
{
return $this->decryptOrReturn($value);
}
/**
* Set service attribute
*
* @param string $value
* @return void
*/
public function setServiceAttribute($value)
{
// Encrypt when needed
$this->attributes['service'] = $value ? $this->encryptOrReturn($value) : $value;
}
/**
* Get secret attribute
*
* @param string $value
* @return string
*/
public function getSecretAttribute($value)
{
return $this->decryptOrReturn($value);
}
2022-11-22 15:15:52 +01:00
/**
* Set secret attribute
*
2022-11-22 15:15:52 +01:00
* @param string $value
* @return void
*/
public function setSecretAttribute($value)
{
// Encrypt when needed
$this->attributes['secret'] = $this->encryptOrReturn(Helpers::PadToBase32Format($value));
}
/**
* Set digits attribute
*
2022-11-22 15:15:52 +01:00
* @param string $value
* @return void
*/
public function setDigitsAttribute($value)
{
2022-12-13 12:07:29 +01:00
$this->attributes['digits'] = ! $value ? 6 : $value;
}
/**
* Set algorithm attribute
*
2022-11-22 15:15:52 +01:00
* @param string $value
* @return void
*/
public function setAlgorithmAttribute($value)
{
2022-12-13 12:07:29 +01:00
$this->attributes['algorithm'] = ! $value ? self::SHA1 : strtolower($value);
}
/**
* Set period attribute
*
2022-11-22 15:15:52 +01:00
* @param string $value
* @return void
*/
public function setPeriodAttribute($value)
{
2022-12-13 12:07:29 +01:00
$this->attributes['period'] = ! $value && $this->otp_type === self::TOTP ? self::DEFAULT_PERIOD : $value;
}
/**
* Set counter attribute
*
2022-11-22 15:15:52 +01:00
* @param string $value
* @return void
*/
public function setCounterAttribute($value)
{
$this->attributes['counter'] = blank($value) && $this->otp_type === self::HOTP ? self::DEFAULT_COUNTER : $value;
}
/**
* Returns a One-Time Password with its parameters
2022-11-22 15:15:52 +01:00
*
* @return TotpDto|HotpDto
*
* @throws InvalidSecretException The secret is not a valid base32 encoded string
* @throws UndecipherableException The secret cannot be deciphered
* @throws UnsupportedOtpTypeException The defined OTP type is not supported
* @throws InvalidOtpParameterException One OTP parameter is invalid
*/
public function getOTP(?int $time = null)
{
2022-11-22 15:15:52 +01:00
Log::info(sprintf('OTP requested for TwoFAccount (%s)', $this->id ? 'id:' . $this->id : 'preview'));
// Early exit if the model has an undecipherable secret
if (strtolower($this->secret) === __('errors.indecipherable')) {
Log::error('Secret cannot be deciphered, OTP generation aborted');
2024-09-26 23:17:00 +02:00
throw new UndecipherableException;
}
$this->initGenerator();
2022-11-22 15:15:52 +01:00
try {
if ($this->otp_type === self::HOTP) {
2024-09-26 23:17:00 +02:00
$OtpDto = new HotpDto;
2022-11-22 15:15:52 +01:00
$OtpDto->otp_type = $this->otp_type;
$counter = $this->generator->getParameter('counter');
$OtpDto->password = $this->generator->at($counter);
$OtpDto->counter = $this->counter = $counter + 1;
// The updated HOTP counter must be saved to db for persisted account only
if ($this->id) {
$this->save();
}
2022-11-22 15:15:52 +01:00
} else {
2024-09-26 23:17:00 +02:00
$OtpDto = new TotpDto;
2022-11-22 15:15:52 +01:00
$OtpDto->otp_type = $this->otp_type;
$OtpDto->generated_at = $time ?: time();
2022-11-22 15:15:52 +01:00
$OtpDto->password = $this->otp_type === self::TOTP
2022-12-09 10:55:11 +01:00
? $this->generator->at($OtpDto->generated_at)
: SteamTotp::getAuthCode(base64_encode(Base32::decodeUpper($this->secret)));
2022-11-22 15:15:52 +01:00
$OtpDto->period = $this->period;
}
2022-11-22 15:15:52 +01:00
Log::info(sprintf('New OTP generated for TwoFAccount (%s)', $this->id ? 'id:' . $this->id : 'preview'));
2022-11-22 15:15:52 +01:00
return $OtpDto;
2023-03-26 17:13:32 +02:00
} catch (\Exception|\Throwable $ex) {
Log::error('An error occured, OTP generation aborted');
// Currently a secret issue is the only possible exception thrown by OTPHP for this stack
// so it is Ok to send the corresponding 2FAuth exception.
// If the generator package change it could be necessary to throw a more generic exception.
throw new InvalidSecretException($ex->getMessage());
}
}
/**
* Fill the model using an array of OTP parameters.
* Missing parameters will be set with default values
2022-11-22 15:15:52 +01:00
*
* @return $this
*/
2022-07-26 22:35:04 +02:00
public function fillWithOtpParameters(array $parameters, bool $skipIconFetching = false)
{
2022-11-22 15:15:52 +01:00
$this->otp_type = strtolower(Arr::get($parameters, 'otp_type'));
$this->account = Arr::get($parameters, 'account');
$this->service = Arr::get($parameters, 'service');
$this->icon = Arr::get($parameters, 'icon');
$this->secret = Arr::get($parameters, 'secret');
$this->algorithm = strtolower(Arr::get($parameters, 'algorithm', self::SHA1));
$this->digits = Arr::get($parameters, 'digits', self::DEFAULT_DIGITS);
$this->period = Arr::get($parameters, 'period', $this->otp_type == self::TOTP ? self::DEFAULT_PERIOD : null);
$this->counter = Arr::get($parameters, 'counter', $this->otp_type == self::HOTP ? self::DEFAULT_COUNTER : null);
$this->initGenerator();
// The generator could have been initialized without a secret, in that case it generates one on the fly.
// The secret attribute has thus to be updated
$this->secret = $this->secret ?: $this->generator->getSecret();
2022-11-22 15:15:52 +01:00
if ($this->otp_type === self::STEAM_TOTP || strtolower($this->service) === 'steam') {
$this->enforceAsSteam();
}
if (! $this->icon && ! $skipIconFetching && Auth::user()?->preferences['getOfficialIcons']) {
2024-10-18 14:28:45 +02:00
$this->icon = Icons::buildFromOfficialLogo($this->service);
2022-11-22 15:15:52 +01:00
}
2022-07-26 22:35:04 +02:00
Log::info(sprintf('TwoFAccount filled with OTP parameters'));
return $this;
}
/**
* Fill the model by parsing an otpauth URI
2022-11-22 15:15:52 +01:00
*
* @return $this
*/
2022-07-26 22:35:04 +02:00
public function fillWithURI(string $uri, bool $isSteamTotp = false, bool $skipIconFetching = false)
{
// First we instanciate the OTP generator
try {
$this->generator = Factory::loadFromProvisioningUri($isSteamTotp ? str_replace('otpauth://steam', 'otpauth://totp', $uri) : $uri);
2023-03-26 23:04:06 +02:00
} catch (\Exception|\Throwable $ex) {
// Hack for Microsoft corporate 2FAs for whom the Issuer query parameter != the Issuer aside the account
$parsed_uri = \OTPHP\Url::fromString($uri);
$pathChunks = explode(':', rawurldecode(mb_substr($parsed_uri->getPath(), 1)));
$service = $pathChunks[0];
$issuer = data_get($parsed_uri->getQuery(), 'issuer', $service);
if (count($pathChunks) == 2 && strtolower($issuer) == 'microsoft' && strcasecmp($issuer, $service) != 0) {
$newUri = str_replace($pathChunks[0] . ':', '', rawurldecode($uri));
try {
$this->generator = Factory::loadFromProvisioningUri($newUri);
$this->generator->setLabel($service . '_' . $this->generator->getLabel());
} catch (\Exception|\Throwable $ex) {
throw ValidationException::withMessages([
'uri' => __('validation.custom.uri.regex', ['attribute' => 'uri']),
]);
}
} else {
throw ValidationException::withMessages([
'uri' => __('validation.custom.uri.regex', ['attribute' => 'uri']),
]);
}
}
// As loadFromProvisioningUri() accept URI without label (nor account nor service) we check
// that the account is set
2022-12-13 12:07:29 +01:00
if (! $this->generator->getLabel()) {
Log::error('URI passed to fillWithURI() must contain a label');
throw ValidationException::withMessages([
2022-11-22 15:15:52 +01:00
'label' => __('validation.custom.label.required'),
]);
}
2022-11-22 15:15:52 +01:00
$this->otp_type = $this->getGeneratorOtpType();
$this->account = $this->generator->getLabel();
$this->secret = $this->generator->getSecret();
$this->service = $this->generator->getIssuer();
$this->algorithm = $this->generator->getDigest();
$this->digits = $this->generator->getDigits();
$this->period = $this->generator->hasParameter('period') ? $this->generator->getParameter('period') : null;
$this->counter = $this->generator->hasParameter('counter') ? $this->generator->getParameter('counter') : null;
$this->legacy_uri = $uri;
if ($isSteamTotp || strtolower($this->service) === 'steam') {
$this->enforceAsSteam();
}
2022-07-19 17:27:23 +02:00
if ($this->generator->hasParameter('image')) {
2024-10-18 14:28:45 +02:00
$this->icon = Icons::buildFromRemoteImage($this->generator->getParameter('image'));
2022-07-19 17:27:23 +02:00
}
2023-03-10 22:59:46 +01:00
2024-10-18 14:28:45 +02:00
$uuu = Auth::user()?->preferences;
if (! $this->icon && ! $skipIconFetching && Auth::user()?->preferences['getOfficialIcons']) {
2024-10-18 14:28:45 +02:00
$this->icon = Icons::buildFromOfficialLogo($this->service);
2022-11-22 15:15:52 +01:00
}
Log::info(sprintf('TwoFAccount filled with an URI'));
return $this;
}
2022-12-09 10:52:17 +01:00
/**
* Compare 2 TwoFAccounts
*/
2022-12-13 12:07:29 +01:00
public function equals(self $other) : bool
2022-12-09 10:52:17 +01:00
{
return $this->service === $other->service &&
$this->account === $other->account &&
$this->icon === $other->icon &&
$this->otp_type === $other->otp_type &&
$this->secret === $other->secret &&
$this->digits === $other->digits &&
$this->algorithm === $other->algorithm &&
$this->period === $other->period &&
$this->counter === $other->counter;
}
/**
* Sets model attributes to STEAM values
*/
2022-12-13 12:07:29 +01:00
private function enforceAsSteam() : void
{
$this->otp_type = self::STEAM_TOTP;
$this->service = 'Steam';
$this->digits = 5;
$this->algorithm = self::SHA1;
$this->period = 30;
2022-11-22 15:15:52 +01:00
2022-07-14 18:05:19 +02:00
Log::info(sprintf('TwoFAccount configured as Steam account'));
}
/**
* Returns the OTP type of the instanciated OTP generator
2022-11-22 15:15:52 +01:00
*
* @return mixed
*/
private function getGeneratorOtpType()
{
2022-07-13 11:02:09 +02:00
return Arr::get($this->generatorClassMap, get_class($this->generator));
}
/**
* Returns an otpauth URI built with model attribute values
*/
2022-12-13 12:07:29 +01:00
public function getURI() : string
{
$this->initGenerator();
return $this->generator->getProvisioningUri();
}
/**
* Instanciates the OTP generator with model attribute values
2022-11-22 15:15:52 +01:00
*
* @throws UnsupportedOtpTypeException The defined OTP type is not supported
* @throws InvalidOtpParameterException One OTP parameter is invalid
*/
2022-12-13 12:07:29 +01:00
private function initGenerator() : void
{
try {
switch ($this->otp_type) {
case self::TOTP:
$this->generator = TOTP::create(
$this->secret,
$this->period ?: self::DEFAULT_PERIOD,
$this->algorithm ?: self::DEFAULT_ALGORITHM,
$this->digits ?: self::DEFAULT_DIGITS
);
break;
case self::STEAM_TOTP:
$this->generator = TOTP::create($this->secret, 30, self::SHA1, 5);
break;
case self::HOTP:
$this->generator = HOTP::create(
$this->secret,
$this->counter ?: self::DEFAULT_COUNTER,
$this->algorithm ?: self::DEFAULT_ALGORITHM,
$this->digits ?: self::DEFAULT_DIGITS
);
break;
2022-11-22 15:15:52 +01:00
default:
2024-09-26 23:17:00 +02:00
throw new UnsupportedOtpTypeException;
}
2022-11-22 15:15:52 +01:00
if ($this->service) {
$this->generator->setIssuer($this->service);
}
if ($this->account) {
$this->generator->setLabel($this->account);
}
} catch (UnsupportedOtpTypeException $exception) {
Log::error(sprintf('%s is not an OTP type supported by the current generator', $this->otp_type));
throw $exception;
2023-03-26 17:13:32 +02:00
} catch (\Exception|\Throwable $exception) {
throw new InvalidOtpParameterException($exception->getMessage());
}
}
2023-02-25 21:12:10 +01:00
/**
2024-11-23 14:29:31 +01:00
* @return \Illuminate\Database\Eloquent\Builder<static>
2023-02-25 21:12:10 +01:00
*/
public function buildSortQuery()
{
return static::query()->where('user_id', $this->user_id);
}
2022-11-22 15:15:52 +01:00
}