diff --git a/Core/Entities/Users/PasswordHasher.cs b/Core/Entities/Users/PasswordHasher.cs
deleted file mode 100644
index 6d1e8aa..0000000
--- a/Core/Entities/Users/PasswordHasher.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-namespace PlanTempus.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/Core/ISecureTokenizer.cs b/Core/ISecureTokenizer.cs
new file mode 100644
index 0000000..c29a610
--- /dev/null
+++ b/Core/ISecureTokenizer.cs
@@ -0,0 +1,8 @@
+namespace PlanTempus.Core
+{
+ public interface ISecureTokenizer
+ {
+ string TokenizeText(string word);
+ bool VerifyToken(string hash, string word);
+ }
+}
\ No newline at end of file
diff --git a/Core/PlanTempus.Core.csproj b/Core/PlanTempus.Core.csproj
index 96a7344..7fc76f2 100644
--- a/Core/PlanTempus.Core.csproj
+++ b/Core/PlanTempus.Core.csproj
@@ -10,10 +10,12 @@
+
+
diff --git a/Core/SecureTokenizer.cs b/Core/SecureTokenizer.cs
new file mode 100644
index 0000000..bffe593
--- /dev/null
+++ b/Core/SecureTokenizer.cs
@@ -0,0 +1,48 @@
+namespace PlanTempus.Core
+{
+
+ public class SecureTokenizer : ISecureTokenizer
+ {
+ private const int _saltSize = 16; // 128 bit
+ private const int _keySize = 32; // 256 bit
+ private const int _iterations = 100000;
+
+ public string TokenizeText(string word)
+ {
+ using (var algorithm = new System.Security.Cryptography.Rfc2898DeriveBytes(
+ word,
+ _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 bool VerifyToken(string hash, string word)
+ {
+ 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(
+ word,
+ salt,
+ iterations,
+ System.Security.Cryptography.HashAlgorithmName.SHA256))
+ {
+ var keyToCheck = algorithm.GetBytes(_keySize);
+ return keyToCheck.SequenceEqual(key);
+ }
+ }
+ }
+}
diff --git a/Database/Core/UserService.cs b/Database/Core/UserService.cs
index 0cbdcb4..da8e721 100644
--- a/Database/Core/UserService.cs
+++ b/Database/Core/UserService.cs
@@ -1,10 +1,11 @@
using Insight.Database;
+using PlanTempus.Core;
using PlanTempus.Core.Entities.Users;
using System.Data;
namespace PlanTempus.Database.Core
{
- public class UserService
+ public class UserService
{
public record UserCreateCommand(string CorrelationId, string Email, string Password);
@@ -21,7 +22,7 @@ namespace PlanTempus.Database.Core
var user = new User
{
Email = command.Email,
- PasswordHash = PasswordHasher.HashPassword(command.Password),
+ PasswordHash = new SecureTokenizer().TokenizeText(command.Password),
SecurityStamp = Guid.NewGuid().ToString(),
EmailConfirmed = false,
CreatedDate = DateTime.UtcNow
diff --git a/PlanTempus.Components/Class1.cs b/PlanTempus.Components/Class1.cs
deleted file mode 100644
index d850929..0000000
--- a/PlanTempus.Components/Class1.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace PlanTempus.Components
-{
- public class Class1
- {
-
- }
-}
diff --git a/PlanTempus.Components/PlanTempus.Components.csproj b/PlanTempus.Components/PlanTempus.Components.csproj
index d0bccb2..d925cd4 100644
--- a/PlanTempus.Components/PlanTempus.Components.csproj
+++ b/PlanTempus.Components/PlanTempus.Components.csproj
@@ -10,11 +10,15 @@
-
+
+
+
+
+
diff --git a/PlanTempus.Components/Users/Create/CreateUserCommand.cs b/PlanTempus.Components/Users/Create/CreateUserCommand.cs
new file mode 100644
index 0000000..f07a6d5
--- /dev/null
+++ b/PlanTempus.Components/Users/Create/CreateUserCommand.cs
@@ -0,0 +1,9 @@
+namespace PlanTempus.Components.Users.Create
+{
+ public class CreateUserCommand
+ {
+ public string Email { get; set; }
+ public string Password { get; set; }
+ public bool IsActive { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/PlanTempus.Components/Users/Create/CreateUserController.cs b/PlanTempus.Components/Users/Create/CreateUserController.cs
new file mode 100644
index 0000000..c4cd2f5
--- /dev/null
+++ b/PlanTempus.Components/Users/Create/CreateUserController.cs
@@ -0,0 +1,33 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace PlanTempus.Components.Users.Create
+{
+ [ApiController]
+ [Route("api/users")]
+ public class CreateUserController(CreateUserHandler handler, CreateUserValidator validator) : ControllerBase
+ {
+ [HttpPost]
+ public async Task> Create([FromBody] CreateUserCommand command)
+ {
+ try
+ {
+ var validationResult = await validator.ValidateAsync(command);
+ if (!validationResult.IsValid)
+ {
+ return BadRequest(validationResult.Errors);
+ }
+
+ var result = await handler.Handle(command);
+ return CreatedAtAction(
+ nameof(CreateUserCommand),
+ "GetUser",
+ new { id = result.Id },
+ result);
+ }
+ catch (Exception ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PlanTempus.Components/Users/Create/CreateUserHandler.cs b/PlanTempus.Components/Users/Create/CreateUserHandler.cs
new file mode 100644
index 0000000..259053d
--- /dev/null
+++ b/PlanTempus.Components/Users/Create/CreateUserHandler.cs
@@ -0,0 +1,64 @@
+using Insight.Database;
+using PlanTempus.Core;
+using PlanTempus.Core.Sql;
+
+namespace PlanTempus.Components.Users.Create
+{
+ public class CreateUserHandler(IDatabaseOperations databaseOperations, ISecureTokenizer secureTokenizer)
+ {
+ private readonly ISecureTokenizer _secureTokenizer;
+
+ public async Task Handle(CreateUserCommand command)
+ {
+ using var db = databaseOperations.CreateScope(nameof(CreateUserHandler));
+ try
+ {
+ var sql = @"
+ INSERT INTO system.users(email, password_hash, security_stamp, email_confirmed,
+ access_failed_count, lockout_enabled, lockout_end,
+ is_active, created_at, last_login_at)
+ VALUES(@Email, @PasswordHash, @SecurityStamp, @EmailConfirmed,
+ @AccessFailedCount, @LockoutEnabled, @LockoutEnd,
+ @IsActive, @CreatedAt, @LastLoginAt)
+ RETURNING id, created_at";
+
+ var result = await db.Connection.QuerySqlAsync(sql, new
+ {
+ Email = command.Email,
+ PasswordHash = secureTokenizer.TokenizeText(command.Password),
+ SecurityStamp = Guid.NewGuid().ToString("N"),
+ EmailConfirmed = false,
+ AccessFailedCount = 0,
+ LockoutEnabled = true,
+ LockoutEnd = (DateTime?)null,
+ IsActive = command.IsActive,
+ CreatedAt = DateTime.UtcNow,
+ LastLoginAt = (DateTime?)null
+ });
+
+ var createdUser = result.First();
+
+ db.Success();
+
+ return new CreateUserResponse
+ {
+ Id = createdUser.Id,
+ Email = command.Email,
+ IsActive = command.IsActive,
+ CreatedAt = createdUser.CreatedAt
+ };
+ }
+ catch (Exception ex)
+ {
+ db.Error(ex);
+ throw;
+ }
+ }
+
+ private class UserCreationResult
+ {
+ public long Id { get; set; }
+ public DateTime CreatedAt { get; set; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PlanTempus.Components/Users/Create/CreateUserResponse.cs b/PlanTempus.Components/Users/Create/CreateUserResponse.cs
new file mode 100644
index 0000000..defdbd4
--- /dev/null
+++ b/PlanTempus.Components/Users/Create/CreateUserResponse.cs
@@ -0,0 +1,11 @@
+
+namespace PlanTempus.Components.Users.Create
+{
+ public class CreateUserResponse
+ {
+ public long Id { get; set; }
+ public string Email { get; set; }
+ public bool IsActive { get; set; }
+ public DateTime CreatedAt { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/PlanTempus.Components/Users/Create/CreateUserValidator.cs b/PlanTempus.Components/Users/Create/CreateUserValidator.cs
new file mode 100644
index 0000000..6007115
--- /dev/null
+++ b/PlanTempus.Components/Users/Create/CreateUserValidator.cs
@@ -0,0 +1,23 @@
+using FluentValidation;
+
+namespace PlanTempus.Components.Users.Create
+{
+ public class CreateUserValidator : AbstractValidator
+ {
+ public CreateUserValidator()
+ {
+ RuleFor(x => x.Email)
+ .NotEmpty().WithMessage("Email skal angives.")
+ .EmailAddress().WithMessage("Ugyldig emailadresse.")
+ .MaximumLength(256).WithMessage("Email må højst være 256 tegn.");
+
+ RuleFor(x => x.Password)
+ .NotEmpty().WithMessage("Password skal angives.")
+ .MinimumLength(8).WithMessage("Password skal være mindst 8 tegn.")
+ .Matches("[A-Z]").WithMessage("Password skal indeholde mindst ét stort bogstav.")
+ .Matches("[a-z]").WithMessage("Password skal indeholde mindst ét lille bogstav.")
+ .Matches("[0-9]").WithMessage("Password skal indeholde mindst ét tal.")
+ .Matches("[^a-zA-Z0-9]").WithMessage("Password skal indeholde mindst ét specialtegn.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/PlanTempus.Components/Users/Create/UserCreationResult.cs b/PlanTempus.Components/Users/Create/UserCreationResult.cs
new file mode 100644
index 0000000..35f4aa6
--- /dev/null
+++ b/PlanTempus.Components/Users/Create/UserCreationResult.cs
@@ -0,0 +1,8 @@
+namespace PlanTempus.Components.Users.Create
+{
+ public class UserCreationResult
+ {
+ public long Id { get; set; }
+ public DateTime CreatedAt { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Tests/PasswordHasherTest.cs b/Tests/PasswordHasherTest.cs
index e8129f5..60521f6 100644
--- a/Tests/PasswordHasherTest.cs
+++ b/Tests/PasswordHasherTest.cs
@@ -1,10 +1,10 @@
using System.Diagnostics;
using Sodium;
-using PlanTempus.Core.Entities.Users;
+using PlanTempus.Core;
namespace PlanTempus.Tests
{
- [TestClass]
+ [TestClass]
public class PasswordHasherTests : TestFixture
{
@@ -35,7 +35,7 @@ namespace PlanTempus.Tests
string password = "TestPassword123";
// Act
- string hashedPassword = PasswordHasher.HashPassword(password);
+ string hashedPassword = new SecureTokenizer().TokenizeText(password);
string[] parts = hashedPassword.Split('.');
// Assert
@@ -48,10 +48,10 @@ namespace PlanTempus.Tests
{
// Arrange
string password = "TestPassword123";
- string hashedPassword = PasswordHasher.HashPassword(password);
+ string hashedPassword = new SecureTokenizer().TokenizeText(password);
// Act
- bool result = PasswordHasher.VerifyPassword(hashedPassword, password);
+ bool result = new SecureTokenizer().VerifyToken(hashedPassword, password);
// Assert
Assert.IsTrue(result);
@@ -63,10 +63,10 @@ namespace PlanTempus.Tests
// Arrange
string correctPassword = "TestPassword123";
string wrongPassword = "WrongPassword123";
- string hashedPassword = PasswordHasher.HashPassword(correctPassword);
+ string hashedPassword = new SecureTokenizer().TokenizeText(correctPassword);
// Act
- bool result = PasswordHasher.VerifyPassword(hashedPassword, wrongPassword);
+ bool result = new SecureTokenizer().VerifyToken(hashedPassword, wrongPassword);
// Assert
Assert.IsFalse(result);
@@ -80,7 +80,7 @@ namespace PlanTempus.Tests
string invalidHash = "InvalidHash";
// Act
- bool result = PasswordHasher.VerifyPassword(invalidHash, password);
+ bool result = new SecureTokenizer().VerifyToken(invalidHash, password);
// Assert
Assert.IsFalse(result);