Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz

This commit is contained in:
2026-03-22 09:13:39 +01:00
parent e8b5edf5d2
commit 2608be7420
80 changed files with 3991 additions and 723 deletions

View File

@@ -11,7 +11,7 @@ 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} {--dry-run : Preview which users would be skipped/deleted without making changes}';
protected $signature = 'skinbase:import-legacy-users {--chunk=200 : Chunk size for processing} {--force-reset-all : Force reset passwords for all imported users} {--restore-temp-usernames : Restore legacy usernames for existing users still using tmpu12345-style placeholders} {--dry-run : Preview which users would be skipped/deleted without making changes}';
protected $description = 'Import legacy users into the new auth schema per legacy_users_migration spec';
protected string $migrationLogPath;
@@ -20,7 +20,7 @@ class ImportLegacyUsers extends Command
public function handle(): int
{
$this->migrationLogPath = storage_path('logs/username_migration.log');
$this->migrationLogPath = (string) storage_path('logs/username_migration.log');
@file_put_contents($this->migrationLogPath, '['.now()."] Starting legacy username policy migration\n", FILE_APPEND);
// Build the set of legacy user IDs that have any meaningful activity.
@@ -134,8 +134,14 @@ class ImportLegacyUsers extends Command
{
$legacyId = (int) $row->user_id;
// Use legacy username as-is (sanitized only, no numeric suffixing — was unique in old DB).
$username = $this->sanitizeUsername((string) ($row->uname ?: ('user' . $legacyId)));
// Use legacy username as-is by default. Placeholder tmp usernames can be
// restored explicitly with --restore-temp-usernames using safe uniqueness rules.
$existingUser = DB::table('users')
->select(['id', 'username'])
->where('id', $legacyId)
->first();
$username = $this->resolveImportUsername($row, $legacyId, $existingUser?->username ?? null);
$normalizedLegacy = UsernamePolicy::normalize((string) ($row->uname ?? ''));
if ($normalizedLegacy !== $username) {
@@ -173,7 +179,12 @@ class ImportLegacyUsers extends Command
DB::transaction(function () use ($legacyId, $username, $email, $passwordHash, $row, $uploads, $downloads, $pageviews, $awards) {
$now = now();
$alreadyExists = DB::table('users')->where('id', $legacyId)->exists();
$existingUser = DB::table('users')
->select(['id', 'username'])
->where('id', $legacyId)
->first();
$alreadyExists = $existingUser !== null;
$previousUsername = (string) ($existingUser?->username ?? '');
// All fields synced from legacy on every run
$sharedFields = [
@@ -212,7 +223,7 @@ class ImportLegacyUsers extends Command
'country_code' => $row->country_code ? substr($row->country_code, 0, 2) : null,
'language' => $row->lang ?: null,
'birthdate' => $row->birth ?: null,
'gender' => $row->gender ?: 'X',
'gender' => $this->normalizeLegacyGender($row->gender ?? null),
'website' => $row->web ?: null,
'updated_at' => $now,
]
@@ -232,7 +243,7 @@ class ImportLegacyUsers extends Command
);
if (Schema::hasTable('username_redirects')) {
$old = UsernamePolicy::normalize((string) ($row->uname ?? ''));
$old = $this->usernameRedirectKey((string) ($row->uname ?? ''));
if ($old !== '' && $old !== $username) {
DB::table('username_redirects')->updateOrInsert(
['old_username' => $old],
@@ -244,10 +255,50 @@ class ImportLegacyUsers extends Command
]
);
}
if ($this->shouldRestoreTemporaryUsername($previousUsername) && $previousUsername !== $username) {
DB::table('username_redirects')->updateOrInsert(
['old_username' => $this->usernameRedirectKey($previousUsername)],
[
'new_username' => $username,
'user_id' => $legacyId,
'created_at' => $now,
'updated_at' => $now,
]
);
}
}
});
}
protected function resolveImportUsername(object $row, int $legacyId, ?string $existingUsername = null): string
{
$legacyUsername = $this->sanitizeUsername((string) ($row->uname ?: ('user' . $legacyId)));
if (! $this->option('restore-temp-usernames')) {
return $legacyUsername;
}
if ($existingUsername === null || $existingUsername === '') {
return $legacyUsername;
}
if (! $this->shouldRestoreTemporaryUsername($existingUsername)) {
return $existingUsername;
}
return UsernamePolicy::uniqueCandidate((string) ($row->uname ?: ('user' . $legacyId)), $legacyId);
}
protected function shouldRestoreTemporaryUsername(?string $username): bool
{
if (! is_string($username) || trim($username) === '') {
return false;
}
return preg_match('/^tmpu\d+$/i', trim($username)) === 1;
}
/**
* Ensure statistic values are safe for unsigned DB columns.
*/
@@ -265,6 +316,24 @@ class ImportLegacyUsers extends Command
return UsernamePolicy::sanitizeLegacy($username);
}
protected function usernameRedirectKey(?string $username): string
{
$value = $this->sanitizeUsername((string) ($username ?? ''));
return $value === 'user' && trim((string) ($username ?? '')) === '' ? '' : $value;
}
protected function normalizeLegacyGender(mixed $value): ?string
{
$normalized = strtoupper(trim((string) ($value ?? '')));
return match ($normalized) {
'M', 'MALE', 'MAN', 'BOY' => 'M',
'F', 'FEMALE', 'WOMAN', 'GIRL' => 'F',
default => null,
};
}
protected function sanitizeEmailLocal(string $value): string
{
$local = strtolower(trim($value));