Adds User Component

This commit is contained in:
Janus Knudsen 2025-03-04 17:13:02 +01:00
parent 73a1f11e99
commit 69758735de
14 changed files with 222 additions and 65 deletions

View file

@ -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;
}
}

View file

@ -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<ActionResult<CreateUserResponse>> 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);
}
}
}
}

View file

@ -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<CreateUserResponse> 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<UserCreationResult>(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; }
}
}
}

View file

@ -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; }
}
}

View file

@ -0,0 +1,23 @@
using FluentValidation;
namespace PlanTempus.Components.Users.Create
{
public class CreateUserValidator : AbstractValidator<CreateUserCommand>
{
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.");
}
}
}

View file

@ -0,0 +1,8 @@
namespace PlanTempus.Components.Users.Create
{
public class UserCreationResult
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
}
}