Current state
This commit is contained in:
203
app/Console/Commands/ImportLegacyUsers.php
Normal file
203
app/Console/Commands/ImportLegacyUsers.php
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ImportLegacyUsers extends Command
|
||||
{
|
||||
protected $signature = 'skinbase:import-legacy-users {--chunk=200 : Chunk size for processing} {--force-reset-all : Force reset passwords for all imported users}';
|
||||
protected $description = 'Import legacy users into the new auth schema per legacy_users_migration spec';
|
||||
|
||||
protected array $usedUsernames = [];
|
||||
protected array $usedEmails = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->usedUsernames = User::pluck('username', 'username')->filter()->all();
|
||||
$this->usedEmails = User::pluck('email', 'email')->filter()->all();
|
||||
|
||||
$chunk = (int) $this->option('chunk');
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
|
||||
if (! DB::connection('legacy')->getPdo()) {
|
||||
$this->error('Legacy DB connection "legacy" is not configured or reachable.');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
DB::connection('legacy')
|
||||
->table('users')
|
||||
->chunkById($chunk, function ($rows) use (&$imported, &$skipped) {
|
||||
$ids = $rows->pluck('user_id')->all();
|
||||
$stats = DB::connection('legacy')
|
||||
->table('users_statistics')
|
||||
->whereIn('user_id', $ids)
|
||||
->get()
|
||||
->keyBy('user_id');
|
||||
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
$this->importRow($row, $stats[$row->user_id] ?? null);
|
||||
$imported++;
|
||||
} catch (\Throwable $e) {
|
||||
$skipped++;
|
||||
$this->warn("Skip user_id {$row->user_id}: {$e->getMessage()}");
|
||||
}
|
||||
}
|
||||
}, 'user_id');
|
||||
|
||||
$this->info("Imported: {$imported}, Skipped: {$skipped}");
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
protected function importRow($row, $statRow = null): void
|
||||
{
|
||||
$legacyId = (int) $row->user_id;
|
||||
$baseUsername = $this->sanitizeUsername($row->uname ?: ('user'.$legacyId));
|
||||
$username = $this->uniqueUsername($baseUsername);
|
||||
|
||||
$email = $this->prepareEmail($row->email ?? null, $username);
|
||||
|
||||
$legacyPassword = $row->password2 ?: $row->password ?: null;
|
||||
|
||||
// Optionally force-reset every imported user's password to a secure random value.
|
||||
if ($this->option('force-reset-all')) {
|
||||
$this->warn("Force-reset-all enabled: generating secure password for user_id {$row->user_id}.");
|
||||
$passwordHash = Hash::make(Str::random(64));
|
||||
} else {
|
||||
// Force-reset known weak default passwords (e.g. "abc123").
|
||||
if ($legacyPassword !== null && trim($legacyPassword) === 'abc123') {
|
||||
$this->warn("Weak password 'abc123' detected for user_id {$row->user_id}; forcing reset.");
|
||||
$passwordHash = Hash::make(Str::random(64));
|
||||
} else {
|
||||
$passwordHash = Hash::make($legacyPassword ?: Str::random(32));
|
||||
}
|
||||
}
|
||||
|
||||
$uploads = $this->sanitizeStatValue($statRow->uploads ?? 0);
|
||||
$downloads = $this->sanitizeStatValue($statRow->downloads ?? 0);
|
||||
$pageviews = $this->sanitizeStatValue($statRow->pageviews ?? 0);
|
||||
$awards = $this->sanitizeStatValue($statRow->awards ?? 0);
|
||||
|
||||
DB::transaction(function () use ($legacyId, $username, $email, $passwordHash, $row, $uploads, $downloads, $pageviews, $awards) {
|
||||
$now = now();
|
||||
|
||||
DB::table('users')->insert([
|
||||
'id' => $legacyId,
|
||||
'username' => $username,
|
||||
'name' => $row->real_name ?: $username,
|
||||
'email' => $email,
|
||||
'password' => $passwordHash,
|
||||
'is_active' => (int) ($row->active ?? 1) === 1,
|
||||
'needs_password_reset' => true,
|
||||
'role' => 'user',
|
||||
'legacy_password_algo' => null,
|
||||
'last_visit_at' => $row->LastVisit ?: null,
|
||||
'created_at' => $row->joinDate ?: $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
DB::table('user_profiles')->insert([
|
||||
'user_id' => $legacyId,
|
||||
'bio' => $row->about_me ?: $row->description ?: null,
|
||||
'avatar' => $row->picture ?: null,
|
||||
'cover_image' => $row->cover_art ?: null,
|
||||
'country' => $row->country ?: null,
|
||||
'country_code' => $row->country_code ? substr($row->country_code, 0, 2) : null,
|
||||
'language' => $row->lang ?: null,
|
||||
'birthdate' => $row->birth ?: null,
|
||||
'gender' => $row->gender ?: 'X',
|
||||
'website' => $row->web ?: null,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
if (!empty($row->web)) {
|
||||
DB::table('user_social_links')->insert([
|
||||
'user_id' => $legacyId,
|
||||
'platform' => 'website',
|
||||
'url' => $row->web,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
DB::table('user_statistics')->insert([
|
||||
'user_id' => $legacyId,
|
||||
'uploads' => $uploads,
|
||||
'downloads' => $downloads,
|
||||
'pageviews' => $pageviews,
|
||||
'awards' => $awards,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure statistic values are safe for unsigned DB columns.
|
||||
*/
|
||||
protected function sanitizeStatValue($value): int
|
||||
{
|
||||
$n = is_numeric($value) ? (int) $value : 0;
|
||||
if ($n < 0) {
|
||||
return 0;
|
||||
}
|
||||
return $n;
|
||||
}
|
||||
|
||||
protected function sanitizeUsername(string $username): string
|
||||
{
|
||||
$username = strtolower(trim($username));
|
||||
$username = preg_replace('/[^a-z0-9._-]/', '-', $username) ?: 'user';
|
||||
return trim($username, '.-') ?: 'user';
|
||||
}
|
||||
|
||||
protected function uniqueUsername(string $base): string
|
||||
{
|
||||
$name = $base;
|
||||
$i = 1;
|
||||
while (isset($this->usedUsernames[$name]) || DB::table('users')->where('username', $name)->exists()) {
|
||||
$name = $base . '-' . $i;
|
||||
$i++;
|
||||
}
|
||||
$this->usedUsernames[$name] = $name;
|
||||
return $name;
|
||||
}
|
||||
|
||||
protected function prepareEmail(?string $legacyEmail, string $username): string
|
||||
{
|
||||
$legacyEmail = $legacyEmail ? strtolower(trim($legacyEmail)) : null;
|
||||
$baseLocal = $this->sanitizeEmailLocal($username);
|
||||
$domain = 'users.skinbase.org';
|
||||
|
||||
$email = $legacyEmail ?: ($baseLocal . '@' . $domain);
|
||||
$email = $this->uniqueEmail($email, $baseLocal, $domain);
|
||||
return $email;
|
||||
}
|
||||
|
||||
protected function uniqueEmail(string $email, string $baseLocal, string $domain): string
|
||||
{
|
||||
$i = 1;
|
||||
$local = explode('@', $email)[0];
|
||||
$current = $email;
|
||||
while (isset($this->usedEmails[$current]) || DB::table('users')->where('email', $current)->exists()) {
|
||||
$current = $local . $i . '@' . $domain;
|
||||
$i++;
|
||||
}
|
||||
$this->usedEmails[$current] = $current;
|
||||
return $current;
|
||||
}
|
||||
|
||||
protected function sanitizeEmailLocal(string $value): string
|
||||
{
|
||||
$local = strtolower(trim($value));
|
||||
$local = preg_replace('/[^a-z0-9._-]/', '-', $local) ?: 'user';
|
||||
return trim($local, '.-') ?: 'user';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user