Auth: convert auth views and verification email to Nova layout
This commit is contained in:
96
app/Console/Commands/EnforceUsernamePolicy.php
Normal file
96
app/Console/Commands/EnforceUsernamePolicy.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Support\UsernamePolicy;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class EnforceUsernamePolicy extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:enforce-usernames {--dry-run : Report only, no writes}';
|
||||
|
||||
protected $description = 'Normalize and enforce username policy on existing users, with collision resolution and redirect logging.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$logPath = storage_path('logs/username_migration.log');
|
||||
@file_put_contents($logPath, '['.now()."] enforce-usernames dry_run=".($dryRun ? '1' : '0')."\n", FILE_APPEND);
|
||||
|
||||
$used = User::query()->whereNotNull('username')->pluck('id', 'username')->mapWithKeys(fn ($id, $username) => [strtolower((string) $username) => (int) $id])->all();
|
||||
|
||||
$updated = 0;
|
||||
|
||||
User::query()->orderBy('id')->chunkById(500, function ($users) use (&$used, &$updated, $dryRun, $logPath): void {
|
||||
foreach ($users as $user) {
|
||||
$current = strtolower(trim((string) ($user->username ?? '')));
|
||||
$base = UsernamePolicy::sanitizeLegacy($current !== '' ? $current : ('user'.$user->id));
|
||||
|
||||
if (UsernamePolicy::isReserved($base) || UsernamePolicy::similarReserved($base) !== null) {
|
||||
$base = 'user'.$user->id;
|
||||
}
|
||||
|
||||
$candidate = substr($base, 0, UsernamePolicy::max());
|
||||
$suffix = 1;
|
||||
while ((isset($used[$candidate]) && (int) $used[$candidate] !== (int) $user->id) || UsernamePolicy::isReserved($candidate) || UsernamePolicy::similarReserved($candidate) !== null) {
|
||||
$suffixStr = (string) $suffix;
|
||||
$prefixLen = max(1, UsernamePolicy::max() - strlen($suffixStr));
|
||||
$candidate = substr($base, 0, $prefixLen) . $suffixStr;
|
||||
$suffix++;
|
||||
}
|
||||
|
||||
$needsUpdate = $candidate !== $current;
|
||||
if (! $needsUpdate) {
|
||||
$used[$candidate] = (int) $user->id;
|
||||
continue;
|
||||
}
|
||||
|
||||
@file_put_contents($logPath, sprintf("[%s] user_id=%d old=%s new=%s\n", now()->toDateTimeString(), (int) $user->id, $current, $candidate), FILE_APPEND);
|
||||
|
||||
if (! $dryRun) {
|
||||
DB::transaction(function () use ($user, $current, $candidate): void {
|
||||
if ($current !== '' && Schema::hasTable('username_history')) {
|
||||
DB::table('username_history')->insert([
|
||||
'user_id' => (int) $user->id,
|
||||
'old_username' => $current,
|
||||
'changed_at' => now(),
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($current !== '' && Schema::hasTable('username_redirects')) {
|
||||
DB::table('username_redirects')->updateOrInsert(
|
||||
['old_username' => $current],
|
||||
[
|
||||
'new_username' => $candidate,
|
||||
'user_id' => (int) $user->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
DB::table('users')->where('id', (int) $user->id)->update([
|
||||
'username' => $candidate,
|
||||
'username_changed_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
$used[$candidate] = (int) $user->id;
|
||||
$updated++;
|
||||
}
|
||||
});
|
||||
|
||||
$this->info("Username policy enforcement complete. Updated: {$updated}" . ($dryRun ? ' (dry run)' : ''));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user