using framework_library; using System; using System.Collections; using System.Data; using System.Data.SqlClient; namespace framework_business { public class PasswordMigrationResult { public int Migrated { get; set; } public int Skipped { get; set; } public int Failed { get; set; } } /// /// BCrypt password storage in pal_UserPassword (central EVOL) with dual-read legacy fallback. /// Hash R/W uses connLive (same catalog as UpdateCentralUser). User lookups use tenant views. /// public static class PasswordService { private const int DefaultWorkFactor = 12; private const int DefaultMaxLoginAttempts = 5; private static readonly encryption LegacyEncrypt = new encryption("p@l3tt3"); public static int GetWorkFactor() { string env = Environment.GetEnvironmentVariable("BCRYPT_WORK_FACTOR"); if (int.TryParse(env, out int factor) && factor >= 4 && factor <= 31) return factor; return DefaultWorkFactor; } public static string Hash(string plain) { if (string.IsNullOrEmpty(plain)) throw new ArgumentException("Password cannot be empty.", nameof(plain)); return global::BCrypt.Net.BCrypt.HashPassword(plain, GetWorkFactor()); } public static bool Verify(string plain, string hash) { if (string.IsNullOrEmpty(plain) || string.IsNullOrEmpty(hash)) return false; return global::BCrypt.Net.BCrypt.Verify(plain, hash); } public static string GetPasswordHash(int userId) { using (SqlConnection connection = OpenCentralConnection()) using (SqlCommand command = new SqlCommand( "SELECT passwordHash FROM dbo.pal_UserPassword WHERE userId = @userId", connection)) { command.Parameters.Add("@userId", SqlDbType.Int).Value = userId; object result = command.ExecuteScalar(); return result == null || result == DBNull.Value ? string.Empty : result.ToString(); } } public static void DeletePassword(int userId) { if (userId <= 0) return; using (SqlConnection connection = OpenCentralConnection()) using (SqlCommand command = new SqlCommand( "DELETE FROM dbo.pal_UserPassword WHERE userId = @userId", connection)) { command.Parameters.Add("@userId", SqlDbType.Int).Value = userId; command.ExecuteNonQuery(); } } public static void SetPassword(int userId, string plain) { SetPassword(userId, plain, enforcePolicy: true, emailForPolicy: null); } public static void SetPassword(int userId, string plain, bool enforcePolicy, string emailForPolicy = null) { if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId), "userId must be positive."); if (enforcePolicy) PasswordPolicy.Enforce(plain, emailForPolicy); string hash = Hash(plain); using (SqlConnection connection = OpenCentralConnection()) { const string mergeSql = "MERGE dbo.pal_UserPassword AS target " + "USING (SELECT @userId AS userId) AS source " + "ON target.userId = source.userId " + "WHEN MATCHED THEN " + " UPDATE SET passwordHash = @hash, hashAlgorithm = 'BCrypt', dateUpdated = GETDATE() " + "WHEN NOT MATCHED THEN " + " INSERT (userId, passwordHash, hashAlgorithm, dateCreated, dateUpdated) " + " VALUES (@userId, @hash, 'BCrypt', GETDATE(), GETDATE());"; using (SqlCommand merge = new SqlCommand(mergeSql, connection)) { merge.Parameters.Add("@userId", SqlDbType.Int).Value = userId; merge.Parameters.Add("@hash", SqlDbType.NVarChar, 255).Value = hash; merge.ExecuteNonQuery(); } using (SqlCommand clearLegacy = new SqlCommand( "UPDATE dbo.pal_User SET password = '' WHERE recId = @userId", connection)) { clearLegacy.Parameters.Add("@userId", SqlDbType.Int).Value = userId; clearLegacy.ExecuteNonQuery(); } } } public static ArrayList ValidateLogin(string email, string plainPassword) { return ValidateLogin(email, plainPassword, customerCode: null); } public static ArrayList ValidateLogin(string email, string plainPassword, string customerCode) { ArrayList matches = new ArrayList(); if (string.IsNullOrWhiteSpace(email) || string.IsNullOrEmpty(plainPassword)) return matches; string trimmedEmail = email.Trim(); ArrayList candidates = LoadActiveUsersByEmail(trimmedEmail); string legacyCipher = LegacyEncrypt.encryptValue(plainPassword); foreach (oUser user in candidates) { if (user == null || IsLoginLocked(user)) continue; if (!string.IsNullOrEmpty(customerCode) && !MatchesLoginTenant(user, customerCode)) continue; if (VerifyUserPassword(user.recId, plainPassword, user.password, legacyCipher)) matches.Add(user); } return matches; } public static int GetMaxLoginAttempts() { string configured = Environment.GetEnvironmentVariable("LOGIN_MAX_ATTEMPTS"); if (string.IsNullOrEmpty(configured)) configured = ConfigResolver.GetAppSetting("LOGIN_MAX_ATTEMPTS"); if (int.TryParse(configured, out int attempts) && attempts >= 0) return attempts; return DefaultMaxLoginAttempts; } public static bool IsLoginLocked(oUser user) { int maxAttempts = GetMaxLoginAttempts(); return maxAttempts > 0 && user != null && user.verifyCodeWrongAttempts >= maxAttempts; } public static void RecordFailedLoginAttempt(string email, string customerCode) { int maxAttempts = GetMaxLoginAttempts(); if (maxAttempts <= 0 || string.IsNullOrWhiteSpace(email)) return; foreach (oUser user in LoadActiveUsersByEmail(email.Trim())) { if (!MatchesLoginTenant(user, customerCode) || IsLoginLocked(user)) continue; user.verifyCodeWrongAttempts++; handler.UpdateCentralUser(user); return; } } public static void ResetLoginAttempts(oUser user) { if (user == null || user.recId <= 0 || user.verifyCodeWrongAttempts == 0) return; user.verifyCodeWrongAttempts = 0; } public static bool IsLoginAllowed(oUser user) { return user != null && user.recId > 0 && user.isActive && !user.isDeactivated; } public static oUser LoadUserById(int userId) { if (userId <= 0) return null; DataTable table = new DataTable(); using (SqlConnection connection = OpenTenantConnection()) using (SqlCommand command = new SqlCommand( "SELECT * FROM pal_vUserShared WHERE recId = @userId", connection)) { command.Parameters.Add("@userId", SqlDbType.Int).Value = userId; using (SqlDataAdapter adapter = new SqlDataAdapter(command)) { adapter.Fill(table); } } if (table.Rows.Count == 0) return null; ArrayList users = utils.ConvertDataTableToList(table, typeof(oUser)); return users.Count > 0 ? (oUser)users[0] : null; } public static bool SetUserDeactivated(int userId, bool deactivated) { if (userId <= 0) return false; using (SqlConnection connection = OpenCentralConnection()) using (SqlCommand command = new SqlCommand( deactivated ? "UPDATE dbo.pal_User SET isDeactivated = 1, dateDeactivated = GETDATE(), dateUpdated = GETDATE() WHERE recId = @userId" : "UPDATE dbo.pal_User SET isDeactivated = 0, verifyCodeWrongAttempts = 0, isReset = 0, resetCode = '', dateUpdated = GETDATE() WHERE recId = @userId", connection)) { command.Parameters.Add("@userId", SqlDbType.Int).Value = userId; return command.ExecuteNonQuery() > 0; } } public static bool ConfirmRegistrationUser(int userId) { oUser user = LoadUserById(userId); if (user == null || user.isDeactivated || user.isActive) return false; using (SqlConnection connection = OpenCentralConnection()) using (SqlCommand command = new SqlCommand( "UPDATE dbo.pal_User SET isActive = 1, dateUpdated = GETDATE() " + "WHERE recId = @userId AND isActive = 0 AND isDeactivated = 0", connection)) { command.Parameters.Add("@userId", SqlDbType.Int).Value = userId; return command.ExecuteNonQuery() > 0; } } private static bool MatchesLoginTenant(oUser user, string customerCode) { if (user.customerCode == customerCode) return true; return user.customerCode == "EVOL-1" && user.userType > pNums.UserType.PowerUser.GetHashCode() && user.userType != (int)pNums.UserType.CustomUser; } public static PasswordMigrationResult MigrateShou1LegacyPasswords(bool dryRun) { PasswordMigrationResult result = new PasswordMigrationResult(); encryption decrypt = new encryption("p@l3tt3"); ArrayList rows = new ArrayList(); using (SqlConnection connection = OpenTenantConnection()) using (SqlCommand command = new SqlCommand( "SELECT u.recId, u.password, p.passwordHash " + "FROM pal_vUserShared u " + "LEFT JOIN pal_vUserPasswordShared p ON p.userId = u.recId " + "WHERE u.customerCode = 'SHOU-1' AND u.password <> ''", connection)) using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { rows.Add(new object[] { reader.GetInt32(0), reader.IsDBNull(1) ? string.Empty : reader.GetString(1), reader.IsDBNull(2) ? string.Empty : reader.GetString(2) }); } } foreach (object[] row in rows) { int userId = (int)row[0]; string legacyCipher = (string)row[1]; string existingHash = (string)row[2]; if (string.IsNullOrEmpty(legacyCipher)) { result.Skipped++; continue; } if (!string.IsNullOrEmpty(existingHash)) { result.Skipped++; continue; } string plain; try { plain = decrypt.decryptData(legacyCipher); } catch { result.Failed++; Console.WriteLine("Failed decrypt: userId=" + userId); continue; } if (string.IsNullOrEmpty(plain)) { result.Failed++; Console.WriteLine("Empty decrypt: userId=" + userId); continue; } if (dryRun) { Console.WriteLine("Would migrate: userId=" + userId); result.Migrated++; continue; } SetPassword(userId, plain, enforcePolicy: false); result.Migrated++; } return result; } private static bool VerifyUserPassword(int userId, string plainPassword, string legacyStored, string legacyCipher) { string hash = GetPasswordHash(userId); if (!string.IsNullOrEmpty(hash)) return Verify(plainPassword, hash); if (string.IsNullOrEmpty(legacyStored)) return false; return string.Equals(legacyCipher, legacyStored, StringComparison.Ordinal); } private static ArrayList LoadActiveUsersByEmail(string email) { DataTable table = new DataTable(); using (SqlConnection connection = OpenTenantConnection()) using (SqlCommand command = new SqlCommand( "SELECT * FROM pal_vUserShared WHERE isActive = 1 AND isDeactivated = 0 AND email = @email", connection)) { command.Parameters.Add("@email", SqlDbType.NVarChar, 255).Value = email; using (SqlDataAdapter adapter = new SqlDataAdapter(command)) { adapter.Fill(table); } } return utils.ConvertDataTableToList(table, typeof(oUser)); } /// /// Tenant DB — for pal_vUserShared / pal_vUserPasswordShared views. /// private static SqlConnection OpenTenantConnection() { return ConfigResolver.OpenConnection("conn"); } /// /// Central auth DB (connLive) — same catalog as handler.UpdateCentralUser / SaveCentralUser. /// private static SqlConnection OpenCentralConnection() { return ConfigResolver.OpenConnection(handler.CentralConnectionKey); } } }