Adds PasswordHasher + DbSetup

This commit is contained in:
Janus C. H. Knudsen 2025-01-21 23:26:05 +01:00
parent 4ec4beef21
commit db09261768
7 changed files with 269 additions and 45 deletions

View file

@ -19,8 +19,4 @@
<PackageReference Include="npgsql" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Entities\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Entities.Users
{
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string PasswordHash { get; set; }
public string SecurityStamp { get; set; }
public bool EmailConfirmed { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? LastLoginDate { get; set; }
}
public class Tenant
{
public int Id { get; set; }
public string ConnectionString { get; set; }
public DateTime CreatedDate { get; set; }
public int CreatedBy { get; set; }
public bool IsActive { get; set; }
}
public class UserTenant
{
public int UserId { get; set; }
public int TenantId { get; set; }
public DateTime CreatedDate { get; set; }
}
}

View file

@ -0,0 +1,47 @@
namespace Core.Entities.Users
{
public static class PasswordHasher
{
private const int _saltSize = 16; // 128 bit
private const int _keySize = 32; // 256 bit
private const int _iterations = 100000;
public static string HashPassword(string password)
{
using (var algorithm = new System.Security.Cryptography.Rfc2898DeriveBytes(
password,
_saltSize,
_iterations,
System.Security.Cryptography.HashAlgorithmName.SHA256))
{
var key = Convert.ToBase64String(algorithm.GetBytes(_keySize));
var salt = Convert.ToBase64String(algorithm.Salt);
return $"{_iterations}.{salt}.{key}";
}
}
public static bool VerifyPassword(string hash, string password)
{
var parts = hash.Split('.', 3);
if (parts.Length != 3)
{
return false;
}
var iterations = Convert.ToInt32(parts[0]);
var salt = Convert.FromBase64String(parts[1]);
var key = Convert.FromBase64String(parts[2]);
using (var algorithm = new System.Security.Cryptography.Rfc2898DeriveBytes(
password,
salt,
iterations,
System.Security.Cryptography.HashAlgorithmName.SHA256))
{
var keyToCheck = algorithm.GetBytes(_keySize);
return keyToCheck.SequenceEqual(key);
}
}
}
}

View file

@ -3,7 +3,6 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>

View file

@ -0,0 +1,79 @@
using Insight.Database;
using System.Data;
namespace Database.Identity
{
public class DbSetup
{
private readonly IDbConnection _db;
public DbSetup(IDbConnection db)
{
_db = db;
}
public void CreateDatabase()
{
var schema = "dev";
if (_db.State != ConnectionState.Open)
_db.Open();
using var transaction = _db.BeginTransaction();
try
{
// Create tables
_db.Execute(@$"
CREATE TABLE IF NOT EXISTS {schema}.users (
id SERIAL PRIMARY KEY,
email VARCHAR(256) NOT NULL UNIQUE,
password_hash VARCHAR(256) NOT NULL,
security_stamp VARCHAR(36) NOT NULL,
email_confirmed BOOLEAN NOT NULL DEFAULT FALSE,
created_date TIMESTAMP NOT NULL,
last_login_date TIMESTAMP NULL
);
CREATE TABLE IF NOT EXISTS {schema}.tenants (
id SERIAL PRIMARY KEY,
connection_string VARCHAR(500) NOT NULL,
created_date TIMESTAMP NOT NULL,
created_by INTEGER REFERENCES users(id),
is_active BOOLEAN DEFAULT true
);
CREATE TABLE IF NOT EXISTS {schema}.user_tenants (
user_id INTEGER REFERENCES users(id),
tenant_id INTEGER REFERENCES tenants(id),
created_date TIMESTAMP NOT NULL,
PRIMARY KEY (user_id, tenant_id)
);
-- Enable RLS både tenants og user_tenants
ALTER TABLE {schema}.tenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE {schema}.user_tenants ENABLE ROW LEVEL SECURITY;
-- RLS policy for tenants
DROP POLICY IF EXISTS tenant_access ON {schema}.tenants;
CREATE POLICY tenant_access ON {schema}.tenants
USING (id IN (
SELECT tenant_id
FROM {schema}.user_tenants
WHERE user_id = current_setting('app.user_id', TRUE)::INTEGER
));
-- RLS policy for user_tenants
DROP POLICY IF EXISTS user_tenant_access ON {schema}.user_tenants;
CREATE POLICY user_tenant_access ON {schema}.user_tenants
USING (user_id = current_setting('app.user_id', TRUE)::INTEGER);");
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}

View file

@ -0,0 +1,76 @@
using Core.Entities.Users;
using Insight.Database;
using System.Data;
namespace Database.Identity
{
public class UserService
{
private readonly IDbConnection _db;
public UserService(IDbConnection db)
{
_db = db;
}
public async Task CreateUserWithTenant(string email, string password, string tenantConnectionString)
{
var schema = "dev";
if (_db.State != ConnectionState.Open)
_db.Open();
using var transaction = _db.BeginTransaction();
try
{
// Create user
var user = new User
{
Email = email,
PasswordHash = PasswordHasher.HashPassword(password),
SecurityStamp = Guid.NewGuid().ToString(),
EmailConfirmed = false,
CreatedDate = DateTime.UtcNow
};
var userId = await _db.ExecuteScalarAsync<int>(@$"
INSERT INTO {schema}.users (email, password_hash, security_stamp, email_confirmed, created_date)
VALUES (@Email, @PasswordHash, @SecurityStamp, @EmailConfirmed, @CreatedDate)
RETURNING id", user);
// Create tenant
var tenant = new Tenant
{
ConnectionString = tenantConnectionString,
CreatedDate = DateTime.UtcNow,
CreatedBy = userId,
IsActive = true
};
var tenantId = await _db.ExecuteScalarAsync<int>(@$"
INSERT INTO {schema}.tenants (connection_string, created_date, created_by, is_active)
VALUES (@ConnectionString, @CreatedDate, @CreatedBy, @IsActive)
RETURNING id", tenant);
// Link user to tenant
var userTenant = new UserTenant
{
UserId = userId,
TenantId = tenantId,
CreatedDate = DateTime.UtcNow
};
await _db.ExecuteAsync(@$"
INSERT INTO {schema}.user_tenants (user_id, tenant_id, created_date)
VALUES (@UserId, @TenantId, @CreatedDate)", userTenant);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
}

View file

@ -1,51 +1,43 @@
using Npgsql;
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
// Tilpas connection string til dine behov
var connectionString = "Host=192.168.1.57;Database=ptdb01;Username=postgres;Password=3911";
static async Task Main(string[] args)
{
var connectionString = "Host=192.168.1.57;Database=ptdb01;Username=postgres;Password=3911";
try
{
await using NpgsqlConnection conn = new NpgsqlConnection(connectionString);
await conn.OpenAsync();
try
{
await using NpgsqlConnection conn = new NpgsqlConnection(connectionString);
await conn.OpenAsync();
Console.WriteLine("Forbundet til databasen. Lytter efter notifikationer...");
Console.WriteLine("Forbundet til databasen. Lytter efter notifikationer...");
// Opsæt notification handling
conn.Notification += (o, e) =>
{
Console.WriteLine($"Notifikation modtaget:");
Console.WriteLine($" PID: {e.PID}");
Console.WriteLine($" Kanal: {e.Channel}");
Console.WriteLine($" Payload: {e.Payload}");
Console.WriteLine("------------------------");
};
conn.Notification += (o, e) =>
{
Console.WriteLine($"Notifikation modtaget:");
Console.WriteLine($" PID: {e.PID}");
Console.WriteLine($" Kanal: {e.Channel}");
Console.WriteLine($" Payload: {e.Payload}");
Console.WriteLine("------------------------");
};
// Start lytning
await using (var cmd = new NpgsqlCommand("LISTEN config_changes;", conn))
{
await cmd.ExecuteNonQueryAsync();
}
await using (var cmd = new NpgsqlCommand("LISTEN config_changes;", conn))
{
await cmd.ExecuteNonQueryAsync();
}
// Hold programmet kørende og lyt efter notifikationer
Console.WriteLine("Tryk på en tast for at stoppe...");
Console.WriteLine("Tryk på en tast for at stoppe...");
// Mens vi venter på input, skal vi huske at wait for notifikationer
while (!Console.KeyAvailable)
{
// Wait for notification for 1 second, then continue loop
await conn.WaitAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"Der opstod en fejl: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
while (!Console.KeyAvailable)
{
await conn.WaitAsync();
}
}
catch (Exception ex)
{
Console.WriteLine($"Der opstod en fejl: {ex.Message}");
Console.WriteLine($"Stack trace: {ex.StackTrace}");
}
}
}