test borked

This commit is contained in:
Dan Baker 2026-02-22 17:49:23 +00:00
parent 49b528a66b
commit 2418edccfd
29 changed files with 2036 additions and 121 deletions

138
app/Models/MagicLoginToken.php Executable file
View 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');
});
}
}