test borked
This commit is contained in:
parent
49b528a66b
commit
2418edccfd
29 changed files with 2036 additions and 121 deletions
138
app/Models/MagicLoginToken.php
Executable file
138
app/Models/MagicLoginToken.php
Executable file
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class MagicLoginToken extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'email_hash',
|
||||
'token_hash',
|
||||
'plain_token',
|
||||
'code_hash',
|
||||
'plain_code',
|
||||
'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.
|
||||
*/
|
||||
public static function generate(string $email, ?string $ip = null, ?string $ua = null): self
|
||||
{
|
||||
$emailHash = User::hashEmail($email);
|
||||
$wordToken = self::generateWordToken();
|
||||
$code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||
|
||||
return self::create([
|
||||
'email_hash' => $emailHash,
|
||||
'token_hash' => Hash::make($wordToken),
|
||||
'plain_token' => $wordToken,
|
||||
'code_hash' => Hash::make($code),
|
||||
'plain_code' => $code,
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
'ip_address' => $ip,
|
||||
'user_agent' => $ua,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique 4-word token from the word list.
|
||||
*/
|
||||
public static function generateWordToken(): string
|
||||
{
|
||||
$words = explode("\n", trim(Storage::get('words.txt')));
|
||||
|
||||
do {
|
||||
$selectedWords = [];
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$selectedWords[] = $words[array_rand($words)];
|
||||
}
|
||||
$token = implode('-', $selectedWords);
|
||||
|
||||
// Check for uniqueness in database
|
||||
$exists = self::where('plain_token', $token)->exists();
|
||||
} while ($exists);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 token matches the plain token.
|
||||
*/
|
||||
public function verifyToken(string $token): bool
|
||||
{
|
||||
return $this->plain_token === $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ namespace App\Models;
|
|||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
|
|
@ -19,8 +20,7 @@ class User extends Authenticatable
|
|||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'email_hash',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -29,7 +29,7 @@ class User extends Authenticatable
|
|||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'email_hash',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
|
|
@ -42,7 +42,32 @@ class User extends Authenticatable
|
|||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash an email address using HMAC-SHA256.
|
||||
*/
|
||||
public static function hashEmail(string $email): string
|
||||
{
|
||||
return hash_hmac('sha256', strtolower(trim($email)), config('app.key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a user by their email address.
|
||||
*/
|
||||
public static function findByEmail(string $email): ?self
|
||||
{
|
||||
$emailHash = self::hashEmail($email);
|
||||
|
||||
return self::where('email_hash', $emailHash)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the magic login tokens for the user.
|
||||
*/
|
||||
public function magicLoginTokens(): HasMany
|
||||
{
|
||||
return $this->hasMany(MagicLoginToken::class, 'email_hash', 'email_hash');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue