chore: commit current workspace changes
This commit is contained in:
@@ -10,7 +10,7 @@ use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Builds all sitemap documents and writes them as static .xml files to the
|
||||
* public disk (default: public/sitemap.xml and public/sitemaps/{name}.xml).
|
||||
* public disk (default: public/sitemaps/sitemap.xml and public/sitemaps/{name}.xml).
|
||||
*
|
||||
* Nginx can then serve those files directly (try_files $uri @php) without
|
||||
* hitting PHP at all. The SitemapController falls back to these same files
|
||||
@@ -25,11 +25,11 @@ final class GenerateSitemapsCommand extends Command
|
||||
{--only=* : Limit to specific sitemap families (comma or space separated)}
|
||||
{--disk= : Override the target filesystem disk (default: sitemaps.static_publish.disk)}';
|
||||
|
||||
protected $description = 'Build all sitemaps and write them as static .xml files to public/.';
|
||||
protected $description = 'Build all sitemaps and write them as static .xml files to the configured public sitemap disk.';
|
||||
|
||||
public function handle(SitemapBuildService $build): int
|
||||
{
|
||||
$totalS tart = microtime(true);
|
||||
$totalStart = microtime(true);
|
||||
$families = $this->selectedFamilies($build);
|
||||
|
||||
if ($families === []) {
|
||||
@@ -50,10 +50,10 @@ final class GenerateSitemapsCommand extends Command
|
||||
// ── Root sitemap index ────────────────────────────────────────────
|
||||
$t = microtime(true);
|
||||
$index = $build->buildIndex(force: true, persist: false, families: $families);
|
||||
$disk->put('sitemap.xml', $index['content']);
|
||||
$disk->put('sitemaps/sitemap.xml', $index['content']);
|
||||
$written++;
|
||||
$this->line(sprintf(
|
||||
' <info>✔</info> sitemap.xml %d entries <comment>%.3fs</comment>',
|
||||
' <info>✔</info> sitemaps/sitemap.xml %d entries <comment>%.3fs</comment>',
|
||||
$index['url_count'],
|
||||
microtime(true) - $t,
|
||||
));
|
||||
|
||||
34
app/Console/Commands/RefreshLeaderboardsCommand.php
Normal file
34
app/Console/Commands/RefreshLeaderboardsCommand.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\LeaderboardService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RefreshLeaderboardsCommand extends Command
|
||||
{
|
||||
protected $signature = 'leaderboards:refresh';
|
||||
|
||||
protected $description = 'Refresh all leaderboard rows and clear leaderboard caches.';
|
||||
|
||||
public function __construct(private readonly LeaderboardService $leaderboards)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('Refreshing leaderboards …');
|
||||
|
||||
$results = $this->leaderboards->refreshAll();
|
||||
$updated = collect($results)
|
||||
->flatten(1)
|
||||
->sum(fn (int $count): int => $count);
|
||||
|
||||
$this->info("Done. Updated: {$updated} leaderboard row(s).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
162
app/Console/Commands/SendUserVerificationEmailCommand.php
Normal file
162
app/Console/Commands/SendUserVerificationEmailCommand.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\SendVerificationEmailJob;
|
||||
use App\Mail\RegistrationVerificationMail;
|
||||
use App\Models\EmailSendEvent;
|
||||
use App\Models\User;
|
||||
use App\Services\Auth\RegistrationEmailQuotaService;
|
||||
use App\Services\Auth\RegistrationVerificationTokenService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
|
||||
class SendUserVerificationEmailCommand extends Command
|
||||
{
|
||||
protected $signature = 'user:send-verification-email
|
||||
{userId : The user ID that should receive the verification email}
|
||||
{--now : Send immediately instead of queueing the existing verification job}
|
||||
{--force : Allow sending even if the user is already verified}';
|
||||
|
||||
protected $description = 'Send the registration verification email to a specific user ID.';
|
||||
|
||||
public function __construct(
|
||||
private readonly RegistrationVerificationTokenService $tokenService,
|
||||
private readonly RegistrationEmailQuotaService $quotaService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$userId = (int) $this->argument('userId');
|
||||
|
||||
if ($userId < 1) {
|
||||
$this->error('The user ID must be a positive integer.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user = User::query()->find($userId);
|
||||
|
||||
if (! $user) {
|
||||
$this->error("User {$userId} was not found.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$email = strtolower(trim((string) $user->email));
|
||||
|
||||
if ($email === '') {
|
||||
$this->error("User {$userId} does not have an email address.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($user->email_verified_at !== null && ! $this->option('force')) {
|
||||
$this->error("User {$userId} already has a verified email address. Use --force to send anyway.");
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$token = $this->tokenService->createForUser($userId);
|
||||
|
||||
$event = EmailSendEvent::query()->create([
|
||||
'type' => 'verify_email',
|
||||
'email' => $email,
|
||||
'ip' => null,
|
||||
'user_id' => $userId,
|
||||
'status' => $this->option('now') ? 'pending' : 'queued',
|
||||
'reason' => null,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
if ($this->option('now')) {
|
||||
return $this->sendNow($user, $event, $token);
|
||||
}
|
||||
|
||||
SendVerificationEmailJob::dispatch(
|
||||
emailEventId: (int) $event->id,
|
||||
email: $email,
|
||||
token: $token,
|
||||
userId: $userId,
|
||||
ip: null,
|
||||
);
|
||||
|
||||
$this->markVerificationEmailSent($user);
|
||||
|
||||
$this->info("Queued verification email for user {$userId} <{$email}>.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function sendNow(User $user, EmailSendEvent $event, string $token): int
|
||||
{
|
||||
if (! $this->acquireGlobalSendSlot()) {
|
||||
$this->updateEvent($event, 'blocked', 'rate_limited');
|
||||
$this->error('The global verification email rate limit is currently exhausted. Try again in a minute.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if ($this->quotaService->isExceeded()) {
|
||||
$this->updateEvent($event, 'blocked', 'quota');
|
||||
$this->error('The monthly registration email quota is exceeded.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
Mail::to($user->email)->send(new RegistrationVerificationMail($token));
|
||||
} catch (\Throwable $exception) {
|
||||
$this->updateEvent($event, 'failed', 'send_error');
|
||||
$this->error('Failed to send the verification email: ' . $exception->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->quotaService->incrementSentCount();
|
||||
$this->updateEvent($event, 'sent', null);
|
||||
$this->markVerificationEmailSent($user);
|
||||
|
||||
$email = strtolower(trim((string) $user->email));
|
||||
$this->info("Sent verification email to user {$user->id} <{$email}>.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function acquireGlobalSendSlot(): bool
|
||||
{
|
||||
$key = 'registration:verification-email:global';
|
||||
$maxPerMinute = max(1, (int) config('registration.email_global_send_per_minute', 30));
|
||||
|
||||
return RateLimiter::attempt($key, $maxPerMinute, static fn () => true, 60);
|
||||
}
|
||||
|
||||
private function updateEvent(EmailSendEvent $event, string $status, ?string $reason): void
|
||||
{
|
||||
EmailSendEvent::query()
|
||||
->whereKey($event->getKey())
|
||||
->update([
|
||||
'status' => $status,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
private function markVerificationEmailSent(User $user): void
|
||||
{
|
||||
$now = now();
|
||||
|
||||
$windowStartedAt = $user->verification_send_window_started_at;
|
||||
if (! $windowStartedAt || $windowStartedAt->lt($now->copy()->subDay())) {
|
||||
$user->verification_send_window_started_at = $now;
|
||||
$user->verification_send_count_24h = 1;
|
||||
} else {
|
||||
$user->verification_send_count_24h = ((int) $user->verification_send_count_24h) + 1;
|
||||
}
|
||||
|
||||
$user->last_verification_sent_at = $now;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user