add account creation when adding an employee
This commit is contained in:
parent
7229a10ad5
commit
bb309d7c20
@ -9,7 +9,7 @@ public static class CustomValidators
|
||||
{
|
||||
return
|
||||
ruleBuilder
|
||||
.Matches(@"^[a-z0-9-_.]*$");
|
||||
.Matches(@"^[a-z0-9-_\.]*$");
|
||||
}
|
||||
|
||||
// According to RFC 5321.
|
||||
@ -18,7 +18,7 @@ public static class CustomValidators
|
||||
{
|
||||
return
|
||||
ruleBuilder
|
||||
.Matches(@"^[\w\.-]{1,64}@[\w\.-]{1,251}\.\w{2,4}$");
|
||||
.Matches(@"^[a-z0-9-_\.]{1,64}@[a-z0-9-_\.]{1,251}\.[a-z0-9-_]{2,4}$");
|
||||
}
|
||||
|
||||
// According to ITU-T E.164, no spaces.
|
||||
|
@ -20,4 +20,11 @@ public record AddEmployeeCommand : IRequest<EmployeeDto>
|
||||
public Guid CompanyGuid { get; set; }
|
||||
|
||||
public ICollection<EmployeeDocumentModel> Documents { get; set; }
|
||||
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
@ -4,6 +4,10 @@ using cuqmbr.TravelGuide.Domain.Entities;
|
||||
using AutoMapper;
|
||||
using cuqmbr.TravelGuide.Application.Common.Exceptions;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using cuqmbr.TravelGuide.Domain.Enums;
|
||||
using System.Security.Cryptography;
|
||||
using cuqmbr.TravelGuide.Application.Common.Services;
|
||||
using System.Text;
|
||||
|
||||
namespace cuqmbr.TravelGuide.Application.Employees.Commands.AddEmployee;
|
||||
|
||||
@ -13,15 +17,15 @@ public class AddEmployeeCommandHandler :
|
||||
private readonly UnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly IStringLocalizer _localizer;
|
||||
private readonly PasswordHasherService _passwordHasher;
|
||||
|
||||
public AddEmployeeCommandHandler(
|
||||
UnitOfWork unitOfWork,
|
||||
IMapper mapper,
|
||||
IStringLocalizer localizer)
|
||||
public AddEmployeeCommandHandler(UnitOfWork unitOfWork, IMapper mapper,
|
||||
IStringLocalizer localizer, PasswordHasherService passwordHasher)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
_localizer = localizer;
|
||||
_passwordHasher = passwordHasher;
|
||||
}
|
||||
|
||||
public async Task<EmployeeDto> Handle(
|
||||
@ -52,6 +56,45 @@ public class AddEmployeeCommandHandler :
|
||||
throw new DuplicateEntityException();
|
||||
}
|
||||
|
||||
|
||||
// Create new account for employee
|
||||
|
||||
var account = await _unitOfWork.AccountRepository.GetOneAsync(
|
||||
e => e.Email == request.Email,
|
||||
cancellationToken);
|
||||
|
||||
if (account != null)
|
||||
{
|
||||
throw new DuplicateEntityException();
|
||||
}
|
||||
|
||||
|
||||
var role = (await _unitOfWork.RoleRepository.GetPageAsync(
|
||||
1, IdentityRole.Enumerations.Count(), cancellationToken))
|
||||
.Items
|
||||
.First(r => r.Value.Equals(IdentityRole.CompanyEmployee));
|
||||
|
||||
var salt = RandomNumberGenerator.GetBytes(128 / 8);
|
||||
var hash = await _passwordHasher.HashAsync(
|
||||
Encoding.UTF8.GetBytes(request.Password),
|
||||
salt, cancellationToken);
|
||||
|
||||
var saltBase64 = Convert.ToBase64String(salt);
|
||||
var hashBase64 = Convert.ToBase64String(hash);
|
||||
|
||||
account = new Account()
|
||||
{
|
||||
Username = request.Username,
|
||||
Email = request.Email,
|
||||
PasswordHash = hashBase64,
|
||||
PasswordSalt = saltBase64,
|
||||
AccountRoles = new AccountRole[] { new() { RoleId = role.Id } }
|
||||
};
|
||||
|
||||
account = await _unitOfWork.AccountRepository.AddOneAsync(
|
||||
account, cancellationToken);
|
||||
|
||||
|
||||
entity = new Employee()
|
||||
{
|
||||
FirstName = request.FirstName,
|
||||
@ -66,12 +109,14 @@ public class AddEmployeeCommandHandler :
|
||||
Information = d.Information
|
||||
})
|
||||
.ToArray(),
|
||||
Company = parentEntity
|
||||
Company = parentEntity,
|
||||
Account = account
|
||||
};
|
||||
|
||||
entity = await _unitOfWork.EmployeeRepository.AddOneAsync(
|
||||
entity, cancellationToken);
|
||||
|
||||
|
||||
await _unitOfWork.SaveAsync(cancellationToken);
|
||||
_unitOfWork.Dispose();
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
using cuqmbr.TravelGuide.Application.Common.FluentValidation;
|
||||
using cuqmbr.TravelGuide.Application.Common.Services;
|
||||
using cuqmbr.TravelGuide.Domain.Enums;
|
||||
using FluentValidation;
|
||||
@ -79,5 +80,46 @@ public class AddEmployeeCommandValidator : AbstractValidator<AddEmployeeCommand>
|
||||
localizer["FluentValidation.MaximumLength"],
|
||||
256));
|
||||
});
|
||||
|
||||
|
||||
RuleFor(v => v.Username)
|
||||
.NotEmpty()
|
||||
.WithMessage(localizer["FluentValidation.NotEmpty"])
|
||||
.MinimumLength(1)
|
||||
.WithMessage(
|
||||
String.Format(
|
||||
cultureService.Culture,
|
||||
localizer["FluentValidation.MinimumLength"],
|
||||
1))
|
||||
.MaximumLength(32)
|
||||
.WithMessage(
|
||||
String.Format(
|
||||
cultureService.Culture,
|
||||
localizer["FluentValidation.MaximumLength"],
|
||||
32))
|
||||
.IsUsername()
|
||||
.WithMessage(localizer["FluentValidation.IsUsername"]);
|
||||
|
||||
RuleFor(v => v.Email)
|
||||
.NotEmpty()
|
||||
.WithMessage(localizer["FluentValidation.NotEmpty"])
|
||||
.IsEmail()
|
||||
.WithMessage(localizer["FluentValidation.IsEmail"]);
|
||||
|
||||
RuleFor(v => v.Password)
|
||||
.NotEmpty()
|
||||
.WithMessage(localizer["FluentValidation.NotEmpty"])
|
||||
.MinimumLength(8)
|
||||
.WithMessage(
|
||||
String.Format(
|
||||
cultureService.Culture,
|
||||
localizer["FluentValidation.MinimumLength"],
|
||||
8))
|
||||
.MaximumLength(64)
|
||||
.WithMessage(
|
||||
String.Format(
|
||||
cultureService.Culture,
|
||||
localizer["FluentValidation.MaximumLength"],
|
||||
64));
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ public class DeleteEmployeeCommandHandler : IRequestHandler<DeleteEmployeeComman
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
|
||||
e => e.Guid == request.Guid, cancellationToken);
|
||||
e => e.Guid == request.Guid, e => e.Account, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
@ -31,6 +31,9 @@ public class DeleteEmployeeCommandHandler : IRequestHandler<DeleteEmployeeComman
|
||||
await _unitOfWork.EmployeeRepository.DeleteOneAsync(
|
||||
entity, cancellationToken);
|
||||
|
||||
await _unitOfWork.AccountRepository.DeleteOneAsync(
|
||||
entity.Account, cancellationToken);
|
||||
|
||||
await _unitOfWork.SaveAsync(cancellationToken);
|
||||
_unitOfWork.Dispose();
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ public class UpdateEmployeeCommandHandler :
|
||||
}
|
||||
|
||||
|
||||
var entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
|
||||
var employee = await _unitOfWork.EmployeeRepository.GetOneAsync(
|
||||
e =>
|
||||
e.FirstName == request.FirstName &&
|
||||
e.LastName == request.LastName &&
|
||||
@ -49,30 +49,34 @@ public class UpdateEmployeeCommandHandler :
|
||||
e.Guid != request.Guid,
|
||||
cancellationToken);
|
||||
|
||||
if (entity != null)
|
||||
if (employee != null)
|
||||
{
|
||||
throw new DuplicateEntityException();
|
||||
}
|
||||
|
||||
|
||||
entity = await _unitOfWork.EmployeeRepository.GetOneAsync(
|
||||
employee = await _unitOfWork.EmployeeRepository.GetOneAsync(
|
||||
e => e.Guid == request.Guid, e => e.Documents, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
if (employee == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var account = await _unitOfWork.AccountRepository.GetOneAsync(
|
||||
a => a.Id == employee.AccountId, cancellationToken);
|
||||
|
||||
entity.Guid = request.Guid;
|
||||
entity.FirstName = request.FirstName;
|
||||
entity.LastName = request.LastName;
|
||||
entity.Patronymic = request.Patronymic;
|
||||
entity.Sex = request.Sex;
|
||||
entity.BirthDate = request.BirthDate;
|
||||
entity.CompanyId = parentEntity.Id;
|
||||
|
||||
entity.Company = parentEntity;
|
||||
employee.Guid = request.Guid;
|
||||
employee.FirstName = request.FirstName;
|
||||
employee.LastName = request.LastName;
|
||||
employee.Patronymic = request.Patronymic;
|
||||
employee.Sex = request.Sex;
|
||||
employee.BirthDate = request.BirthDate;
|
||||
employee.CompanyId = parentEntity.Id;
|
||||
|
||||
employee.Company = parentEntity;
|
||||
employee.Account = account;
|
||||
|
||||
|
||||
var requestEmployeeDocuments = request.Documents.Select(
|
||||
@ -82,27 +86,27 @@ public class UpdateEmployeeCommandHandler :
|
||||
Information = d.Information
|
||||
});
|
||||
|
||||
var commonEmployeeDocuments = entity.Documents.IntersectBy(
|
||||
var commonEmployeeDocuments = employee.Documents.IntersectBy(
|
||||
requestEmployeeDocuments.Select(
|
||||
ed => (ed.DocumentType, ed.Information)),
|
||||
ed => (ed.DocumentType, ed.Information));
|
||||
|
||||
var newEmployeeDocuments = requestEmployeeDocuments.ExceptBy(
|
||||
entity.Documents.Select(ed => (ed.DocumentType, ed.Information)),
|
||||
employee.Documents.Select(ed => (ed.DocumentType, ed.Information)),
|
||||
ed => (ed.DocumentType, ed.Information));
|
||||
|
||||
var combinedEmployeeDocuments = commonEmployeeDocuments.UnionBy(
|
||||
newEmployeeDocuments, ed => (ed.DocumentType, ed.Information));
|
||||
|
||||
entity.Documents = combinedEmployeeDocuments.ToList();
|
||||
employee.Documents = combinedEmployeeDocuments.ToList();
|
||||
|
||||
|
||||
entity = await _unitOfWork.EmployeeRepository.UpdateOneAsync(
|
||||
entity, cancellationToken);
|
||||
employee = await _unitOfWork.EmployeeRepository.UpdateOneAsync(
|
||||
employee, cancellationToken);
|
||||
|
||||
await _unitOfWork.SaveAsync(cancellationToken);
|
||||
_unitOfWork.Dispose();
|
||||
|
||||
return _mapper.Map<EmployeeDto>(entity);
|
||||
return _mapper.Map<EmployeeDto>(employee);
|
||||
}
|
||||
}
|
||||
|
11
src/Application/Employees/EmployeeAccountDto.cs
Normal file
11
src/Application/Employees/EmployeeAccountDto.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using cuqmbr.TravelGuide.Application.Common.Mappings;
|
||||
using cuqmbr.TravelGuide.Domain.Entities;
|
||||
|
||||
namespace cuqmbr.TravelGuide.Application.Employees;
|
||||
|
||||
public sealed class EmployeeAccountDto : IMapFrom<Account>
|
||||
{
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
}
|
@ -22,6 +22,8 @@ public sealed class EmployeeDto : IMapFrom<Employee>
|
||||
|
||||
public ICollection<EmployeeDocumentDto> Documents { get; set; }
|
||||
|
||||
public EmployeeAccountDto Account { get; set; }
|
||||
|
||||
public void Mapping(MappingProfile profile)
|
||||
{
|
||||
profile.CreateMap<Employee, EmployeeDto>()
|
||||
|
@ -33,13 +33,18 @@ public class GetEmployeeQueryHandler :
|
||||
}
|
||||
|
||||
|
||||
// Hydrate employees with companies
|
||||
// Hydrate employee
|
||||
|
||||
var company = await _unitOfWork.CompanyRepository.GetOneAsync(
|
||||
e => e.Id == entity.CompanyId, cancellationToken);
|
||||
|
||||
entity.Company = company;
|
||||
|
||||
var account = await _unitOfWork.AccountRepository.GetOneAsync(
|
||||
e => e.Id == entity.AccountId, cancellationToken);
|
||||
|
||||
entity.Account = account;
|
||||
|
||||
|
||||
_unitOfWork.Dispose();
|
||||
|
||||
|
@ -49,7 +49,7 @@ public class GetEmployeesPageQueryHandler :
|
||||
cancellationToken);
|
||||
|
||||
|
||||
// Hydrate employees with companies
|
||||
// Hydrate employees
|
||||
|
||||
var companies = await _unitOfWork.CompanyRepository.GetPageAsync(
|
||||
e => paginatedList.Items.Select(e => e.CompanyId).Contains(e.Id),
|
||||
@ -61,6 +61,16 @@ public class GetEmployeesPageQueryHandler :
|
||||
companies.Items.First(c => c.Id == employee.CompanyId);
|
||||
}
|
||||
|
||||
var accounts = await _unitOfWork.AccountRepository.GetPageAsync(
|
||||
e => paginatedList.Items.Select(e => e.AccountId).Contains(e.Id),
|
||||
1, paginatedList.Items.Count, cancellationToken);
|
||||
|
||||
foreach (var employee in paginatedList.Items)
|
||||
{
|
||||
employee.Account =
|
||||
accounts.Items.First(a => a.Id == employee.AccountId);
|
||||
}
|
||||
|
||||
|
||||
var mappedItems = _mapper
|
||||
.ProjectTo<EmployeeDto>(paginatedList.Items.AsQueryable());
|
||||
|
@ -16,4 +16,11 @@ public sealed class AddEmployeeViewModel
|
||||
public Guid CompanyUuid { get; set; }
|
||||
|
||||
public ICollection<EmployeeDocumentViewModel> Documents { get; set; }
|
||||
|
||||
|
||||
public string Username { get; set; }
|
||||
|
||||
public string Email { get; set; }
|
||||
|
||||
public string Password { get; set; }
|
||||
}
|
||||
|
@ -13,4 +13,7 @@ public sealed class Account : EntityBase
|
||||
public ICollection<AccountRole> AccountRoles { get; set; }
|
||||
|
||||
public ICollection<RefreshToken> RefreshTokens { get; set; }
|
||||
|
||||
|
||||
public Employee? Employee { get; set; }
|
||||
}
|
||||
|
@ -22,4 +22,9 @@ public sealed class Employee : EntityBase
|
||||
public ICollection<EmployeeDocument> Documents { get; set; }
|
||||
|
||||
public ICollection<VehicleEnrollmentEmployee> VehicleEnrollmentEmployees { get; set; }
|
||||
|
||||
|
||||
public long AccountId { get; set; }
|
||||
|
||||
public Account Account { get; set; }
|
||||
}
|
||||
|
@ -59,7 +59,10 @@ public class EmployeesController : ControllerBase
|
||||
Information = e.Information
|
||||
|
||||
}).ToArray(),
|
||||
CompanyGuid = viewModel.CompanyUuid
|
||||
CompanyGuid = viewModel.CompanyUuid,
|
||||
Username = viewModel.Username,
|
||||
Email = viewModel.Email,
|
||||
Password = viewModel.Password
|
||||
},
|
||||
cancellationToken));
|
||||
}
|
||||
|
@ -77,5 +77,29 @@ public class EmployeeConfiguration : BaseConfiguration<Employee>
|
||||
"ix_" +
|
||||
$"{builder.Metadata.GetTableName()}_" +
|
||||
$"{builder.Property(e => e.CompanyId).Metadata.GetColumnName()}");
|
||||
|
||||
|
||||
builder
|
||||
.Property(e => e.AccountId)
|
||||
.HasColumnName("account_id")
|
||||
.HasColumnType("bigint")
|
||||
.IsRequired(true);
|
||||
|
||||
builder
|
||||
.HasOne(e => e.Account)
|
||||
.WithOne(a => a.Employee)
|
||||
.HasForeignKey<Employee>(e => e.AccountId)
|
||||
.HasConstraintName(
|
||||
"fk_" +
|
||||
$"{builder.Metadata.GetTableName()}_" +
|
||||
$"{builder.Property(e => e.AccountId).Metadata.GetColumnName()}")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder
|
||||
.HasIndex(e => e.AccountId)
|
||||
.HasDatabaseName(
|
||||
"ix_" +
|
||||
$"{builder.Metadata.GetTableName()}_" +
|
||||
$"{builder.Property(e => e.AccountId).Metadata.GetColumnName()}");
|
||||
}
|
||||
}
|
||||
|
1313
src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs
generated
Normal file
1313
src/Persistence/PostgreSql/Migrations/20250528141733_Add_navigation_from_Employee_to_Account.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,58 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Persistence.PostgreSql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_navigation_from_Employee_to_Account : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "account_id",
|
||||
schema: "application",
|
||||
table: "employees",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_employees_account_id",
|
||||
schema: "application",
|
||||
table: "employees",
|
||||
column: "account_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_employees_account_id",
|
||||
schema: "application",
|
||||
table: "employees",
|
||||
column: "account_id",
|
||||
principalSchema: "application",
|
||||
principalTable: "accounts",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_employees_account_id",
|
||||
schema: "application",
|
||||
table: "employees");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_employees_account_id",
|
||||
schema: "application",
|
||||
table: "employees");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "account_id",
|
||||
schema: "application",
|
||||
table: "employees");
|
||||
}
|
||||
}
|
||||
}
|
@ -306,6 +306,10 @@ namespace Persistence.PostgreSql.Migrations
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseSequence(b.Property<long>("Id"), "employees_id_sequence");
|
||||
|
||||
b.Property<long>("AccountId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<DateOnly>("BirthDate")
|
||||
.HasColumnType("date")
|
||||
.HasColumnName("birth_date");
|
||||
@ -344,6 +348,10 @@ namespace Persistence.PostgreSql.Migrations
|
||||
b.HasAlternateKey("Guid")
|
||||
.HasName("altk_employees_uuid");
|
||||
|
||||
b.HasIndex("AccountId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_employees_account_id");
|
||||
|
||||
b.HasIndex("CompanyId")
|
||||
.HasDatabaseName("ix_employees_company_id");
|
||||
|
||||
@ -1030,6 +1038,13 @@ namespace Persistence.PostgreSql.Migrations
|
||||
|
||||
modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Employee", b =>
|
||||
{
|
||||
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Account", "Account")
|
||||
.WithOne("Employee")
|
||||
.HasForeignKey("cuqmbr.TravelGuide.Domain.Entities.Employee", "AccountId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_employees_account_id");
|
||||
|
||||
b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Company", "Company")
|
||||
.WithMany("Employees")
|
||||
.HasForeignKey("CompanyId")
|
||||
@ -1037,6 +1052,8 @@ namespace Persistence.PostgreSql.Migrations
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_employees_company_id");
|
||||
|
||||
b.Navigation("Account");
|
||||
|
||||
b.Navigation("Company");
|
||||
});
|
||||
|
||||
@ -1213,6 +1230,8 @@ namespace Persistence.PostgreSql.Migrations
|
||||
{
|
||||
b.Navigation("AccountRoles");
|
||||
|
||||
b.Navigation("Employee");
|
||||
|
||||
b.Navigation("RefreshTokens");
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user