feat(auth): complete registration anti-spam and quota hardening
This commit is contained in:
66
app/Services/Auth/DisposableEmailService.php
Normal file
66
app/Services/Auth/DisposableEmailService.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
class DisposableEmailService
|
||||
{
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return (bool) config('registration.disposable_domains_enabled', true);
|
||||
}
|
||||
|
||||
public function isDisposableEmail(string $email): bool
|
||||
{
|
||||
if (! $this->isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$domain = $this->extractDomain($email);
|
||||
if ($domain === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$blocked = (array) config('disposable_email_domains.domains', []);
|
||||
foreach ($blocked as $entry) {
|
||||
$pattern = strtolower(trim((string) $entry));
|
||||
if ($pattern === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->matchesPattern($domain, $pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function extractDomain(string $email): ?string
|
||||
{
|
||||
$normalized = strtolower(trim($email));
|
||||
if ($normalized === '' || ! str_contains($normalized, '@')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parts = explode('@', $normalized);
|
||||
$domain = trim((string) end($parts));
|
||||
|
||||
return $domain !== '' ? $domain : null;
|
||||
}
|
||||
|
||||
private function matchesPattern(string $domain, string $pattern): bool
|
||||
{
|
||||
if ($pattern === $domain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (! str_contains($pattern, '*')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$quoted = preg_quote($pattern, '#');
|
||||
$regex = '#^' . str_replace('\\*', '.*', $quoted) . '$#i';
|
||||
|
||||
return (bool) preg_match($regex, $domain);
|
||||
}
|
||||
}
|
||||
37
app/Services/Auth/RegistrationEmailQuotaService.php
Normal file
37
app/Services/Auth/RegistrationEmailQuotaService.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\SystemEmailQuota;
|
||||
|
||||
class RegistrationEmailQuotaService
|
||||
{
|
||||
public function isExceeded(): bool
|
||||
{
|
||||
$quota = $this->getCurrentPeriodQuota();
|
||||
|
||||
return $quota->sent_count >= $quota->limit_count;
|
||||
}
|
||||
|
||||
public function incrementSentCount(): void
|
||||
{
|
||||
$quota = $this->getCurrentPeriodQuota();
|
||||
$quota->sent_count = (int) $quota->sent_count + 1;
|
||||
$quota->updated_at = now();
|
||||
$quota->save();
|
||||
}
|
||||
|
||||
private function getCurrentPeriodQuota(): SystemEmailQuota
|
||||
{
|
||||
$period = now()->format('Y-m');
|
||||
|
||||
return SystemEmailQuota::query()->firstOrCreate(
|
||||
['period' => $period],
|
||||
[
|
||||
'sent_count' => 0,
|
||||
'limit_count' => max(1, (int) config('registration.monthly_email_limit', 10000)),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
67
app/Services/Auth/RegistrationVerificationTokenService.php
Normal file
67
app/Services/Auth/RegistrationVerificationTokenService.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RegistrationVerificationTokenService
|
||||
{
|
||||
public function createForUser(int $userId): string
|
||||
{
|
||||
DB::table('user_verification_tokens')->where('user_id', $userId)->delete();
|
||||
|
||||
$rawToken = Str::random(64);
|
||||
$tokenHash = $this->hashToken($rawToken);
|
||||
|
||||
// Support environments where the migration hasn't renamed the column yet
|
||||
$column = \Illuminate\Support\Facades\Schema::hasColumn('user_verification_tokens', 'token_hash') ? 'token_hash' : 'token';
|
||||
|
||||
DB::table('user_verification_tokens')->insert([
|
||||
'user_id' => $userId,
|
||||
$column => $tokenHash,
|
||||
'expires_at' => now()->addHours($this->ttlHours()),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
return $rawToken;
|
||||
}
|
||||
|
||||
public function findValidRecord(string $rawToken): ?object
|
||||
{
|
||||
$tokenHash = $this->hashToken($rawToken);
|
||||
|
||||
$column = \Illuminate\Support\Facades\Schema::hasColumn('user_verification_tokens', 'token_hash') ? 'token_hash' : 'token';
|
||||
|
||||
$record = DB::table('user_verification_tokens')
|
||||
->where($column, $tokenHash)
|
||||
->first();
|
||||
|
||||
if (! $record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! hash_equals((string) ($record->{$column} ?? ''), $tokenHash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (now()->greaterThan($record->expires_at)) {
|
||||
DB::table('user_verification_tokens')->where('id', $record->id)->delete();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
private function ttlHours(): int
|
||||
{
|
||||
return max(1, (int) config('registration.verify_token_ttl_hours', 24));
|
||||
}
|
||||
|
||||
private function hashToken(string $rawToken): string
|
||||
{
|
||||
return hash('sha256', $rawToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user