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);
}
}
}