220 lines
7.4 KiB
C#
220 lines
7.4 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Npgsql;
|
|
using SPTarkov.DI;
|
|
using SPTarkov.DI.Annotations;
|
|
|
|
namespace PersonalAuthMod;
|
|
|
|
[Injectable]
|
|
public class DatabaseManager
|
|
{
|
|
private readonly AuthConfig _config;
|
|
private readonly string _connectionString;
|
|
|
|
public DatabaseManager()
|
|
{
|
|
_config = AuthConfig.Load();
|
|
_connectionString = $"Host={_config.DbUrl};Port={_config.DbPort};Username={_config.DbUser};Password={_config.DbPassword};Database={_config.DbName}";
|
|
Console.WriteLine($"[PersonalAuthMod] DB Manager connecting to: Host={_config.DbUrl}, Port={_config.DbPort}, User={_config.DbUser}, DB={_config.DbName}");
|
|
}
|
|
|
|
public void Initialize()
|
|
{
|
|
try
|
|
{
|
|
using var conn = new NpgsqlConnection(_connectionString);
|
|
conn.Open();
|
|
|
|
// Create Users Table
|
|
using (var cmd = new NpgsqlCommand(@"
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
|
password_hash TEXT NOT NULL,
|
|
salt TEXT NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);", conn))
|
|
{
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
// Create Sessions Table
|
|
using (var cmd = new NpgsqlCommand(@"
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
session_id VARCHAR(100) PRIMARY KEY,
|
|
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
);", conn))
|
|
{
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log error (using Console for now as we don't have logger injected here yet, or we can use DI)
|
|
Console.WriteLine($"[PersonalAuthMod] Database Initialization Failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public bool RegisterUser(string username, string password)
|
|
{
|
|
try
|
|
{
|
|
using var conn = new NpgsqlConnection(_connectionString);
|
|
conn.Open();
|
|
|
|
// Check if user exists
|
|
using (var checkCmd = new NpgsqlCommand("SELECT COUNT(*) FROM users WHERE username = @u", conn))
|
|
{
|
|
checkCmd.Parameters.AddWithValue("u", username);
|
|
var count = (long)checkCmd.ExecuteScalar();
|
|
if (count > 0) return false;
|
|
}
|
|
|
|
// Hash Password
|
|
var salt = GenerateSalt();
|
|
var hash = HashPassword(password, salt);
|
|
|
|
// Insert User
|
|
using (var cmd = new NpgsqlCommand("INSERT INTO users (username, password_hash, salt) VALUES (@u, @p, @s)", conn))
|
|
{
|
|
cmd.Parameters.AddWithValue("u", username);
|
|
cmd.Parameters.AddWithValue("p", hash);
|
|
cmd.Parameters.AddWithValue("s", salt);
|
|
cmd.ExecuteNonQuery();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[PersonalAuthMod] RegisterUser Failed: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public string? LoginUser(string username, string password)
|
|
{
|
|
try
|
|
{
|
|
using var conn = new NpgsqlConnection(_connectionString);
|
|
conn.Open();
|
|
|
|
int userId;
|
|
string storedHash, storedSalt;
|
|
|
|
using (var cmd = new NpgsqlCommand("SELECT id, password_hash, salt FROM users WHERE username = @u", conn))
|
|
{
|
|
cmd.Parameters.AddWithValue("u", username);
|
|
using var reader = cmd.ExecuteReader();
|
|
if (!reader.Read()) return null; // User not found
|
|
|
|
userId = reader.GetInt32(0);
|
|
storedHash = reader.GetString(1);
|
|
storedSalt = reader.GetString(2);
|
|
}
|
|
|
|
var hash = HashPassword(password, storedSalt);
|
|
if (hash != storedHash) return null; // Wrong password
|
|
|
|
// Generate Session (Must be 24-character hex for MongoId compatibility)
|
|
var sessionBytes = new byte[12];
|
|
using (var rng = RandomNumberGenerator.Create())
|
|
{
|
|
rng.GetBytes(sessionBytes);
|
|
}
|
|
var sessionId = Convert.ToHexString(sessionBytes).ToLower();
|
|
|
|
// Invalidate old sessions for this user? Requirement: "block existing login sessions".
|
|
using (var delCmd = new NpgsqlCommand("DELETE FROM sessions WHERE user_id = @uid", conn))
|
|
{
|
|
delCmd.Parameters.AddWithValue("uid", userId);
|
|
delCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
using (var insertCmd = new NpgsqlCommand("INSERT INTO sessions (session_id, user_id) VALUES (@sid, @uid)", conn))
|
|
{
|
|
insertCmd.Parameters.AddWithValue("sid", sessionId);
|
|
insertCmd.Parameters.AddWithValue("uid", userId);
|
|
insertCmd.ExecuteNonQuery();
|
|
}
|
|
|
|
return sessionId;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[PersonalAuthMod] LoginUser Failed: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public bool ValidateSession(string sessionId)
|
|
{
|
|
if (string.IsNullOrEmpty(sessionId)) return false;
|
|
|
|
try
|
|
{
|
|
using var conn = new NpgsqlConnection(_connectionString);
|
|
conn.Open();
|
|
|
|
using (var cmd = new NpgsqlCommand("SELECT COUNT(*) FROM sessions WHERE session_id = @sid", conn))
|
|
{
|
|
cmd.Parameters.AddWithValue("sid", sessionId);
|
|
var count = (long)cmd.ExecuteScalar();
|
|
return count > 0;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[PersonalAuthMod] ValidateSession Failed: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public string? GetUsernameBySession(string sessionId)
|
|
{
|
|
try
|
|
{
|
|
using var conn = new NpgsqlConnection(_connectionString);
|
|
conn.Open();
|
|
|
|
using (var cmd = new NpgsqlCommand(@"
|
|
SELECT u.username
|
|
FROM sessions s
|
|
JOIN users u ON s.user_id = u.id
|
|
WHERE s.session_id = @sid", conn))
|
|
{
|
|
cmd.Parameters.AddWithValue("sid", sessionId);
|
|
return cmd.ExecuteScalar() as string;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[PersonalAuthMod] GetUsernameBySession Failed: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private string GenerateSalt()
|
|
{
|
|
var bytes = new byte[16];
|
|
using (var rng = RandomNumberGenerator.Create())
|
|
{
|
|
rng.GetBytes(bytes);
|
|
}
|
|
return Convert.ToBase64String(bytes);
|
|
}
|
|
|
|
private string HashPassword(string password, string salt)
|
|
{
|
|
// Simple SHA256 with salt. Pepper is mentioned in requirements but simplified here to salt.
|
|
// If pepper is strictly required, we can add a hardcoded string constant.
|
|
var pepper = "spt-server-pepper";
|
|
using var sha256 = SHA256.Create();
|
|
var combined = password + salt + pepper;
|
|
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined));
|
|
return Convert.ToBase64String(bytes);
|
|
}
|
|
}
|