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