diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index 4171d44a..3a04fea2 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -35,7 +35,9 @@ server_scripts { 'server/bridge/**/*.lua', 'server/modules/npwd.lua', - 'server/modules/createJob.lua' + 'server/modules/createJob.lua', + 'server/migration/**/main.lua', + 'server/migration/main.lua', } client_scripts { diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index dc4a8fe6..1577708c 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -818,8 +818,9 @@ function Core.IsPlayerAdmin(playerSrc) end -- Generates a unique 9-digit SSN in dashed format (XXX-XX-XXXX). +---@param skipUniqueCheck boolean? ---@return string -function Core.generateSSN() +function Core.generateSSN(skipUniqueCheck) local reservedSSNs = { ["078-05-1120"] = true, ["219-09-9999"] = true, @@ -852,6 +853,10 @@ function Core.generateSSN() goto continue end + if skipUniqueCheck then + return candidate + end + local exists = MySQL.scalar.await("SELECT 1 FROM `users` WHERE `ssn` = ? LIMIT 1", { candidate }) if not exists then diff --git a/[core]/es_extended/server/migration/main.lua b/[core]/es_extended/server/migration/main.lua new file mode 100644 index 00000000..378894fd --- /dev/null +++ b/[core]/es_extended/server/migration/main.lua @@ -0,0 +1,46 @@ +RegisterCommand("resetmigrations", function(src) + if src > 0 then + print("^1[ERROR]^7 This command can only be run from the server console.") + return + end + + for version, _ in pairs(Core.Migrations or {}) do + DeleteResourceKvp(("esx_migration:%s"):format(version)) + end + print("^2[SUCCESS]^7 Reset all migrations. This will re-run all migrations on the next server start.") +end) + +local migrationsRan = 0 +local restartRequired = false + +for esxVersion, migrations in pairs(Core.Migrations or {}) do + ---@cast esxVersion string + ---@cast migrations table + + if ESX.Table.SizeOf(migrations) > 0 then + print(("^4[INFO]^7 Running migrations for ESX version %s"):format(esxVersion)) + + for migrationName, migration in pairs(migrations) do + local success, result = pcall(migration) + if not success then + local err = result --[[@as string]] + error(("^1[ERROR]^7 Failed migration ^4['%s.%s']^7: %s"):format(esxVersion, migrationName, err)) + end + + local migrationRequiresRestart = result --[[@as boolean]] + + if not restartRequired and migrationRequiresRestart then + restartRequired = true + end + end + + SetResourceKvpInt(("esx_migration:%s"):format(esxVersion), 1) + print(("^2[SUCCESS]^7 Successfully completed migrations for ESX version %s"):format(esxVersion)) + migrationsRan += 1 + end +end + +while restartRequired do + print(("^4[INFO]^7 Ran migrations for %d ESX version(s). ^1Server restart required!^7"):format(migrationsRan)) + Wait(500) +end diff --git a/[core]/es_extended/server/migration/v1.13.3/ssn/main.lua b/[core]/es_extended/server/migration/v1.13.3/ssn/main.lua new file mode 100644 index 00000000..83b36e9b --- /dev/null +++ b/[core]/es_extended/server/migration/v1.13.3/ssn/main.lua @@ -0,0 +1,70 @@ +local esxVersion = "v1.13.3" + +Core.Migrations = Core.Migrations or {} +Core.Migrations[esxVersion] = Core.Migrations[esxVersion] or {} + +if GetResourceKvpInt(("esx_migration:%s"):format(esxVersion)) == 1 then + return +end + +---@return boolean restartRequired +Core.Migrations[esxVersion].ssn = function() + print("^4[esx_migration:v.1.13.3:ssn]^7 Adding SSN column to users table.") + local col = MySQL.scalar.await([[ + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'users' + AND COLUMN_NAME = 'ssn' +]]) + + local idx = MySQL.scalar.await([[ + SELECT COUNT(*) + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'users' + AND INDEX_NAME = 'unique_ssn' +]]) + + if col == 0 and idx == 0 then + MySQL.update.await([[ + ALTER TABLE `users` + ADD COLUMN `ssn` VARCHAR(11) NULL DEFAULT NULL AFTER `identifier`, + ADD UNIQUE KEY `unique_ssn` (`ssn`) + ]]) + elseif col == 0 then + MySQL.update.await("ALTER TABLE `users` ADD COLUMN `ssn` VARCHAR(11) NULL DEFAULT NULL AFTER `identifier`") + elseif idx == 0 then + MySQL.update.await("ALTER TABLE `users` ADD UNIQUE KEY `unique_ssn` (`ssn`)") + end + + + local Result = MySQL.query.await("SELECT `identifier` FROM `users` WHERE `ssn` IS NULL") + if #Result == 0 then + print("^4[esx_migration:v.1.13.3:ssn]^7 No users found without SSN, migration not needed.") + return false + end + + print("^4[esx_migration:v.1.13.3:ssn]^7 Generating SSN for existing users.") + local GeneratedSSNs = {} + local Parameters = {} + for i = 1, #Result do + local ssn + repeat + ssn = Core.generateSSN(true) + until not GeneratedSSNs[ssn] + + GeneratedSSNs[ssn] = true + Parameters[i] = { ssn, Result[i].identifier } + end + + print("^4[esx_migration:v.1.13.3:ssn]^7 Updating users with generated SSN. This may take a minute...") + MySQL.prepare.await("UPDATE `users` SET `ssn` = ? WHERE `identifier` = ?", Parameters) + + print("^4[esx_migration:v.1.13.3:ssn]^7 Removing SSN default value.") + MySQL.update.await("ALTER TABLE `users` MODIFY `ssn` VARCHAR(11) NOT NULL") + + print(("^4[esx_migration:v.1.13.3:ssn]^7 Successfully migrated %d users."):format(#Parameters)) + + return true +end