diff --git a/Core/Core.csproj b/Core/Core.csproj index d44e594..0461ea9 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -19,8 +19,4 @@ - - - - diff --git a/Core/Entities/Users/Class1.cs b/Core/Entities/Users/Class1.cs new file mode 100644 index 0000000..1ddd5cf --- /dev/null +++ b/Core/Entities/Users/Class1.cs @@ -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; } + } +} diff --git a/Core/Entities/Users/PasswordHasher.cs b/Core/Entities/Users/PasswordHasher.cs new file mode 100644 index 0000000..b6ff815 --- /dev/null +++ b/Core/Entities/Users/PasswordHasher.cs @@ -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); + } + } + } +} diff --git a/Database/Database.csproj b/Database/Database.csproj index 35e3a11..d737a20 100644 --- a/Database/Database.csproj +++ b/Database/Database.csproj @@ -3,7 +3,6 @@ net8.0 enable - enable diff --git a/Database/Identity/DbSetup.cs b/Database/Identity/DbSetup.cs new file mode 100644 index 0000000..a8ab1b0 --- /dev/null +++ b/Database/Identity/DbSetup.cs @@ -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 på 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; + } + } + } +} diff --git a/Database/Identity/UserService.cs b/Database/Identity/UserService.cs new file mode 100644 index 0000000..3c2b5e9 --- /dev/null +++ b/Database/Identity/UserService.cs @@ -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(@$" + 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(@$" + 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; + } + } + } +} diff --git a/TestPostgresLISTEN/Program.cs b/TestPostgresLISTEN/Program.cs index 71dc835..f78d218 100644 --- a/TestPostgresLISTEN/Program.cs +++ b/TestPostgresLISTEN/Program.cs @@ -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}"); + } + } } \ No newline at end of file