108 lines
2.7 KiB
PHP
Executable file
108 lines
2.7 KiB
PHP
Executable file
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\DTOs\MagicTokenResult;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
|
|
class MagicLoginToken extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
/**
|
|
* The attributes that are mass assignable.
|
|
*
|
|
* @var list<string>
|
|
*/
|
|
protected $fillable = [
|
|
'email_hash',
|
|
'token_hash',
|
|
'code_hash',
|
|
'expires_at',
|
|
'ip_address',
|
|
'user_agent',
|
|
'used_at',
|
|
];
|
|
|
|
/**
|
|
* Get the attributes that should be cast.
|
|
*
|
|
* @return array<string, string>
|
|
*/
|
|
protected function casts(): array
|
|
{
|
|
return [
|
|
'expires_at' => 'datetime',
|
|
'used_at' => 'datetime',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate a new magic login token for the given email.
|
|
* Plain token and code are returned in the DTO but never persisted.
|
|
*/
|
|
public static function generate(string $email, ?string $ip = null, ?string $ua = null): MagicTokenResult
|
|
{
|
|
$plainToken = Str::random(64);
|
|
$plainCode = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
|
|
|
$model = self::create([
|
|
'email_hash' => User::hashEmail($email),
|
|
'token_hash' => hash('sha256', $plainToken),
|
|
'code_hash' => Hash::make($plainCode),
|
|
'expires_at' => now()->addMinutes(15),
|
|
'ip_address' => $ip,
|
|
'user_agent' => $ua,
|
|
]);
|
|
|
|
return new MagicTokenResult($model, $plainToken, $plainCode);
|
|
}
|
|
|
|
/**
|
|
* Check if the token is valid (not expired and not used).
|
|
*/
|
|
public function isValid(): bool
|
|
{
|
|
return $this->expires_at->isFuture() && $this->used_at === null;
|
|
}
|
|
|
|
/**
|
|
* Mark the token as used.
|
|
*/
|
|
public function markAsUsed(): void
|
|
{
|
|
$this->update(['used_at' => now()]);
|
|
}
|
|
|
|
/**
|
|
* Verify the provided code matches the hashed code.
|
|
*/
|
|
public function verifyCode(string $code): bool
|
|
{
|
|
return Hash::check($code, $this->code_hash);
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include valid tokens.
|
|
*/
|
|
public function scopeValid(Builder $query): void
|
|
{
|
|
$query->where('expires_at', '>', now())
|
|
->whereNull('used_at');
|
|
}
|
|
|
|
/**
|
|
* Scope a query to only include expired or used tokens.
|
|
*/
|
|
public function scopeExpiredOrUsed(Builder $query): void
|
|
{
|
|
$query->where(function (Builder $q) {
|
|
$q->where('expires_at', '<=', now())
|
|
->orWhereNotNull('used_at');
|
|
});
|
|
}
|
|
}
|