Страница профиля
This commit is contained in:
+4
-1
@@ -95,7 +95,7 @@ builder.Services.AddCors(options =>
|
|||||||
{
|
{
|
||||||
options.AddPolicy("FrontendPolicy", policy =>
|
options.AddPolicy("FrontendPolicy", policy =>
|
||||||
{
|
{
|
||||||
policy.WithOrigins("http://localhost:3000", "http://127.0.0.1:3000")
|
policy.WithOrigins("http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:3001", "http://127.0.0.1:3001")
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
.AllowAnyHeader()
|
.AllowAnyHeader()
|
||||||
.AllowCredentials();
|
.AllowCredentials();
|
||||||
@@ -116,6 +116,9 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseCors("FrontendPolicy");
|
app.UseCors("FrontendPolicy");
|
||||||
|
|
||||||
|
// Включение статических файлов для аватаров
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
// Включение аутентификации и авторизации
|
// Включение аутентификации и авторизации
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||||
|
using Nashel.Modules.Identity.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
|
public record ChangePhoneNumberCommand : IRequest
|
||||||
|
{
|
||||||
|
public string NewPhone { get; init; } = default!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChangePhoneNumberCommandHandler : IRequestHandler<ChangePhoneNumberCommand>
|
||||||
|
{
|
||||||
|
private readonly IAccountRepository _accountRepository;
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
|
||||||
|
public ChangePhoneNumberCommandHandler(IAccountRepository accountRepository, ICurrentUserService currentUserService)
|
||||||
|
{
|
||||||
|
_accountRepository = accountRepository;
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(ChangePhoneNumberCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = _currentUserService.UserId;
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка уникальности нового номера
|
||||||
|
if (await _accountRepository.GetByPhoneAsync(request.NewPhone, cancellationToken) != null)
|
||||||
|
{
|
||||||
|
throw new Exception("Этот номер телефона уже используется");
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = await _accountRepository.GetByIdAsync(userId.Value, cancellationToken);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Account not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
account.ChangePhone(request.NewPhone);
|
||||||
|
|
||||||
|
await _accountRepository.UpdateAsync(account, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,19 +34,24 @@ public class LoginCommandHandler : IRequestHandler<LoginCommand, string>
|
|||||||
{
|
{
|
||||||
private readonly IAccountRepository _accountRepository;
|
private readonly IAccountRepository _accountRepository;
|
||||||
private readonly IJwtTokenGenerator _jwtTokenGenerator;
|
private readonly IJwtTokenGenerator _jwtTokenGenerator;
|
||||||
|
private readonly IPasswordHasher _passwordHasher;
|
||||||
|
|
||||||
public LoginCommandHandler(IAccountRepository accountRepository, IJwtTokenGenerator jwtTokenGenerator)
|
public LoginCommandHandler(
|
||||||
|
IAccountRepository accountRepository,
|
||||||
|
IJwtTokenGenerator jwtTokenGenerator,
|
||||||
|
IPasswordHasher passwordHasher)
|
||||||
{
|
{
|
||||||
_accountRepository = accountRepository;
|
_accountRepository = accountRepository;
|
||||||
_jwtTokenGenerator = jwtTokenGenerator;
|
_jwtTokenGenerator = jwtTokenGenerator;
|
||||||
|
_passwordHasher = passwordHasher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> Handle(LoginCommand request, CancellationToken cancellationToken)
|
public async Task<string> Handle(LoginCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var account = await _accountRepository.GetByPhoneAsync(request.Phone, cancellationToken);
|
var account = await _accountRepository.GetByPhoneAsync(request.Phone, cancellationToken);
|
||||||
if (account == null || account.PasswordHash != request.Password) // Сравнение хешей в реальной жизни
|
if (account == null || !_passwordHasher.VerifyPassword(request.Password, account.PasswordHash))
|
||||||
{
|
{
|
||||||
throw new Exception("Неверные учетные данные"); // Использовать паттерн Result
|
throw new Exception("Неверные учетные данные");
|
||||||
}
|
}
|
||||||
|
|
||||||
return _jwtTokenGenerator.GenerateToken(account.Id, account.Roles);
|
return _jwtTokenGenerator.GenerateToken(account.Id, account.Roles);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using MediatR;
|
|||||||
using Nashel.Modules.Identity.Domain.Aggregates;
|
using Nashel.Modules.Identity.Domain.Aggregates;
|
||||||
using Nashel.Modules.Identity.Domain.Enums;
|
using Nashel.Modules.Identity.Domain.Enums;
|
||||||
using Nashel.Modules.Identity.Domain.Repositories;
|
using Nashel.Modules.Identity.Domain.Repositories;
|
||||||
|
using Nashel.Modules.Identity.Domain.Services;
|
||||||
|
|
||||||
namespace Nashel.Modules.Identity.Application.Commands;
|
namespace Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
@@ -20,22 +21,41 @@ public record RegisterUserCommand : IRequest<Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Password { get; init; } = default!;
|
public string Password { get; init; } = default!;
|
||||||
|
|
||||||
public RegisterUserCommand(string phone, string password)
|
/// <summary>
|
||||||
{
|
/// Имя.
|
||||||
Phone = phone;
|
/// </summary>
|
||||||
Password = password;
|
public string FirstName { get; init; } = default!;
|
||||||
}
|
|
||||||
|
|
||||||
public RegisterUserCommand() { }
|
/// <summary>
|
||||||
|
/// Фамилия.
|
||||||
|
/// </summary>
|
||||||
|
public string LastName { get; init; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отчество (необязательно).
|
||||||
|
/// </summary>
|
||||||
|
public string? Patronymic { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Название компании (необязательно).
|
||||||
|
/// </summary>
|
||||||
|
public string? CompanyName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Роль (по умолчанию User).
|
||||||
|
/// </summary>
|
||||||
|
public Role Role { get; init; } = Role.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, Guid>
|
public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, Guid>
|
||||||
{
|
{
|
||||||
private readonly IAccountRepository _accountRepository;
|
private readonly IAccountRepository _accountRepository;
|
||||||
|
private readonly IPasswordHasher _passwordHasher;
|
||||||
|
|
||||||
public RegisterUserCommandHandler(IAccountRepository accountRepository)
|
public RegisterUserCommandHandler(IAccountRepository accountRepository, IPasswordHasher passwordHasher)
|
||||||
{
|
{
|
||||||
_accountRepository = accountRepository;
|
_accountRepository = accountRepository;
|
||||||
|
_passwordHasher = passwordHasher;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Guid> Handle(RegisterUserCommand request, CancellationToken cancellationToken)
|
public async Task<Guid> Handle(RegisterUserCommand request, CancellationToken cancellationToken)
|
||||||
@@ -43,16 +63,31 @@ public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, G
|
|||||||
// Проверка существования пользователя
|
// Проверка существования пользователя
|
||||||
if (await _accountRepository.GetByPhoneAsync(request.Phone, cancellationToken) != null)
|
if (await _accountRepository.GetByPhoneAsync(request.Phone, cancellationToken) != null)
|
||||||
{
|
{
|
||||||
throw new Exception("Пользователь уже существует"); // Использовать паттерн Result в будущем
|
throw new Exception("Пользователь уже существует");
|
||||||
}
|
}
|
||||||
|
|
||||||
var account = Account.Create(request.Phone, request.Password); // Хешировать пароль в хендлере или конструкторе?
|
var passwordHash = _passwordHasher.HashPassword(request.Password);
|
||||||
// Конструктор сущности обычно принимает хешированный пароль.
|
var account = Account.Create(request.Phone, passwordHash);
|
||||||
// Предположим, что хешируем здесь или используем сервис.
|
|
||||||
// Для простоты пока передаем как есть, но нужно хешировать.
|
// Добавляем роль, если она отличается от User
|
||||||
// TODO: Правильно хешировать пароль.
|
if (request.Role != Role.User && request.Role != Role.Admin)
|
||||||
|
{
|
||||||
|
// Здесь можно добавить логику проверки прав, но для MVP разрешим
|
||||||
|
account.Roles.Add(request.Role);
|
||||||
|
}
|
||||||
|
|
||||||
|
var profile = UserProfile.Create(
|
||||||
|
account.Id,
|
||||||
|
request.FirstName,
|
||||||
|
request.LastName,
|
||||||
|
request.Patronymic,
|
||||||
|
request.CompanyName);
|
||||||
|
|
||||||
|
account.SetProfile(profile);
|
||||||
|
|
||||||
await _accountRepository.AddAsync(account, cancellationToken);
|
await _accountRepository.AddAsync(account, cancellationToken);
|
||||||
|
// Предполагаем, что AccountRepository.AddAsync сохранит всё дерево сущностей
|
||||||
|
|
||||||
return account.Id;
|
return account.Id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||||
|
using Nashel.Modules.Identity.Domain.Repositories;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
|
public record UpdateProfileCommand : IRequest
|
||||||
|
{
|
||||||
|
public string FirstName { get; init; } = default!;
|
||||||
|
public string LastName { get; init; } = default!;
|
||||||
|
public string? Patronymic { get; init; }
|
||||||
|
public string? CompanyName { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateProfileCommandHandler : IRequestHandler<UpdateProfileCommand>
|
||||||
|
{
|
||||||
|
private readonly IAccountRepository _accountRepository;
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
|
||||||
|
public UpdateProfileCommandHandler(IAccountRepository accountRepository, ICurrentUserService currentUserService)
|
||||||
|
{
|
||||||
|
_accountRepository = accountRepository;
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(UpdateProfileCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = _currentUserService.UserId;
|
||||||
|
if (userId == null)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException();
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = await _accountRepository.GetByIdAsync(userId.Value, cancellationToken);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Account not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (account.Profile == null)
|
||||||
|
{
|
||||||
|
throw new Exception("Profile not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
account.Profile.Update(
|
||||||
|
request.FirstName,
|
||||||
|
request.LastName,
|
||||||
|
request.Patronymic,
|
||||||
|
request.CompanyName);
|
||||||
|
|
||||||
|
await _accountRepository.UpdateAsync(account, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
|
public record UpdateProfileCompetenciesCommand(List<string> CompetencyNames) : IRequest;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
|
public record UploadAvatarCommand(IFormFile File) : IRequest<string>;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
|
public class UploadAvatarCommandHandler : IRequestHandler<UploadAvatarCommand, string>
|
||||||
|
{
|
||||||
|
private const long MaxFileSize = 5 * 1024 * 1024; // 5MB
|
||||||
|
private static readonly string[] AllowedExtensions = { ".jpg", ".jpeg", ".png" };
|
||||||
|
|
||||||
|
public async Task<string> Handle(UploadAvatarCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var file = request.File;
|
||||||
|
|
||||||
|
// Валидация размера
|
||||||
|
if (file.Length > MaxFileSize)
|
||||||
|
throw new InvalidOperationException("Размер файла не должен превышать 5 МБ");
|
||||||
|
|
||||||
|
// Валидация формата
|
||||||
|
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||||
|
if (Array.IndexOf(AllowedExtensions, extension) == -1)
|
||||||
|
throw new InvalidOperationException("Допустимые форматы: JPG, PNG");
|
||||||
|
|
||||||
|
// Создаем папку uploads если её нет
|
||||||
|
var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads");
|
||||||
|
if (!Directory.Exists(uploadsPath))
|
||||||
|
Directory.CreateDirectory(uploadsPath);
|
||||||
|
|
||||||
|
// Генерируем уникальное имя файла
|
||||||
|
var fileName = $"{Guid.NewGuid()}{extension}";
|
||||||
|
var filePath = Path.Combine(uploadsPath, fileName);
|
||||||
|
|
||||||
|
// Сохраняем файл
|
||||||
|
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||||
|
{
|
||||||
|
await file.CopyToAsync(stream, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем URL
|
||||||
|
return $"/uploads/{fileName}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MediatR" Version="14.0.0" />
|
<PackageReference Include="MediatR" Version="14.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<RootNamespace>Nashel.Modules.Identity.Application</RootNamespace>
|
<RootNamespace>Nashel.Modules.Identity.Application</RootNamespace>
|
||||||
|
|||||||
@@ -6,7 +6,17 @@ namespace Nashel.Modules.Identity.Application.Queries.GetProfile;
|
|||||||
|
|
||||||
public record GetProfileQuery : IRequest<ProfileResponse>;
|
public record GetProfileQuery : IRequest<ProfileResponse>;
|
||||||
|
|
||||||
public record ProfileResponse(Guid Id, string Phone, List<string> Roles);
|
public record ProfileResponse(
|
||||||
|
Guid Id,
|
||||||
|
string Phone,
|
||||||
|
List<string> Roles,
|
||||||
|
string FirstName,
|
||||||
|
string LastName,
|
||||||
|
string? Patronymic,
|
||||||
|
string? CompanyName,
|
||||||
|
string FullName,
|
||||||
|
string? AvatarUrl,
|
||||||
|
List<string> Competencies);
|
||||||
|
|
||||||
public class GetProfileQueryHandler : IRequestHandler<GetProfileQuery, ProfileResponse>
|
public class GetProfileQueryHandler : IRequestHandler<GetProfileQuery, ProfileResponse>
|
||||||
{
|
{
|
||||||
@@ -33,9 +43,22 @@ public class GetProfileQueryHandler : IRequestHandler<GetProfileQuery, ProfileRe
|
|||||||
throw new Exception("Account not found");
|
throw new Exception("Account not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fullName = $"{account.Profile.FirstName} {account.Profile.LastName}";
|
||||||
|
if (!string.IsNullOrEmpty(account.Profile.Patronymic))
|
||||||
|
{
|
||||||
|
fullName += $" {account.Profile.Patronymic}";
|
||||||
|
}
|
||||||
|
|
||||||
return new ProfileResponse(
|
return new ProfileResponse(
|
||||||
account.Id,
|
account.Id,
|
||||||
account.Phone,
|
account.Phone,
|
||||||
account.Roles.Select(r => r.ToString()).ToList());
|
account.Roles.Select(r => r.ToString()).ToList(),
|
||||||
|
account.Profile.FirstName,
|
||||||
|
account.Profile.LastName,
|
||||||
|
account.Profile.Patronymic,
|
||||||
|
account.Profile.CompanyName,
|
||||||
|
fullName,
|
||||||
|
account.Profile.AvatarUrl,
|
||||||
|
account.Profile.Competencies.Select(c => c.Name).ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Queries;
|
||||||
|
|
||||||
|
public record SearchCompetenciesQuery(string Query) : IRequest<List<string>>;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Validators;
|
||||||
|
|
||||||
|
public class ChangePhoneNumberCommandValidator : AbstractValidator<ChangePhoneNumberCommand>
|
||||||
|
{
|
||||||
|
public ChangePhoneNumberCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.NewPhone)
|
||||||
|
.NotEmpty().WithMessage("Новый телефон обязателен")
|
||||||
|
.Matches(@"^\+\d{11,15}$").WithMessage("Формат телефона должен быть E.164 (начинаться с +, 11-15 цифр)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Nashel.Modules.Identity.Application.Commands;
|
||||||
|
using Nashel.Modules.Identity.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Validators;
|
||||||
|
|
||||||
|
public class RegisterUserCommandValidator : AbstractValidator<RegisterUserCommand>
|
||||||
|
{
|
||||||
|
public RegisterUserCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Phone)
|
||||||
|
.NotEmpty().WithMessage("Телефон обязателен")
|
||||||
|
.Matches(@"^\+\d{11,15}$").WithMessage("Формат телефона должен быть E.164 (начинаться с +, 11-15 цифр)");
|
||||||
|
|
||||||
|
RuleFor(x => x.Password)
|
||||||
|
.NotEmpty().WithMessage("Пароль обязателен")
|
||||||
|
.MinimumLength(6).WithMessage("Пароль должен быть не менее 6 символов");
|
||||||
|
|
||||||
|
RuleFor(x => x.FirstName)
|
||||||
|
.NotEmpty().WithMessage("Имя обязательно")
|
||||||
|
.MinimumLength(2).WithMessage("Имя должно содержать минимум 2 символа");
|
||||||
|
|
||||||
|
RuleFor(x => x.LastName)
|
||||||
|
.NotEmpty().WithMessage("Фамилия обязательна")
|
||||||
|
.MinimumLength(2).WithMessage("Фамилия должна содержать минимум 2 символа");
|
||||||
|
|
||||||
|
RuleFor(x => x.CompanyName)
|
||||||
|
.NotEmpty().WithMessage("Название компании обязательно для юридических лиц")
|
||||||
|
.When(x => x.Role == Role.Company);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Nashel.Modules.Identity.Application.Commands;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Application.Validators;
|
||||||
|
|
||||||
|
public class UpdateProfileCommandValidator : AbstractValidator<UpdateProfileCommand>
|
||||||
|
{
|
||||||
|
public UpdateProfileCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.FirstName)
|
||||||
|
.NotEmpty().WithMessage("Имя обязательно")
|
||||||
|
.MinimumLength(2).WithMessage("Имя должно содержать минимум 2 символа");
|
||||||
|
|
||||||
|
RuleFor(x => x.LastName)
|
||||||
|
.NotEmpty().WithMessage("Фамилия обязательна")
|
||||||
|
.MinimumLength(2).WithMessage("Фамилия должна содержать минимум 2 символа");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ public class Account : AggregateRoot<Guid>
|
|||||||
public string Phone { get; private set; }
|
public string Phone { get; private set; }
|
||||||
public string PasswordHash { get; private set; }
|
public string PasswordHash { get; private set; }
|
||||||
public List<Role> Roles { get; private set; } = new();
|
public List<Role> Roles { get; private set; } = new();
|
||||||
|
public UserProfile Profile { get; private set; } = default!;
|
||||||
|
|
||||||
// Конструктор для EF Core
|
// Конструктор для EF Core
|
||||||
private Account() { }
|
private Account() { }
|
||||||
@@ -27,6 +28,16 @@ public class Account : AggregateRoot<Guid>
|
|||||||
return new Account(Guid.NewGuid(), phone, passwordHash);
|
return new Account(Guid.NewGuid(), phone, passwordHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetProfile(UserProfile profile)
|
||||||
|
{
|
||||||
|
Profile = profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangePhone(string newPhone)
|
||||||
|
{
|
||||||
|
Phone = newPhone;
|
||||||
|
}
|
||||||
|
|
||||||
public void BecomePerformer()
|
public void BecomePerformer()
|
||||||
{
|
{
|
||||||
if (!Roles.Contains(Role.Candidate) && !Roles.Contains(Role.Master))
|
if (!Roles.Contains(Role.Candidate) && !Roles.Contains(Role.Master))
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Nashel.BuildingBlocks.Domain;
|
||||||
|
using Nashel.Modules.Identity.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Domain.Aggregates;
|
||||||
|
|
||||||
|
public class UserProfile : Entity<Guid>
|
||||||
|
{
|
||||||
|
private const int MaxCompetencies = 50;
|
||||||
|
private readonly List<Competency> _competencies = new();
|
||||||
|
|
||||||
|
public string FirstName { get; private set; }
|
||||||
|
public string LastName { get; private set; }
|
||||||
|
public string? Patronymic { get; private set; }
|
||||||
|
public string? CompanyName { get; private set; }
|
||||||
|
public string? AvatarUrl { get; private set; }
|
||||||
|
public IReadOnlyCollection<Competency> Competencies => _competencies.AsReadOnly();
|
||||||
|
|
||||||
|
// EF Core constructor
|
||||||
|
private UserProfile() { }
|
||||||
|
|
||||||
|
private UserProfile(Guid id, string firstName, string lastName, string? patronymic, string? companyName)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
FirstName = firstName;
|
||||||
|
LastName = lastName;
|
||||||
|
Patronymic = patronymic;
|
||||||
|
CompanyName = companyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserProfile Create(Guid id, string firstName, string lastName, string? patronymic, string? companyName)
|
||||||
|
{
|
||||||
|
return new UserProfile(id, firstName, lastName, patronymic, companyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update(string firstName, string lastName, string? patronymic, string? companyName)
|
||||||
|
{
|
||||||
|
FirstName = firstName;
|
||||||
|
LastName = lastName;
|
||||||
|
Patronymic = patronymic;
|
||||||
|
CompanyName = companyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateAvatar(string? avatarUrl)
|
||||||
|
{
|
||||||
|
AvatarUrl = avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateCompetencies(IEnumerable<Competency> competencies)
|
||||||
|
{
|
||||||
|
var competencyList = competencies.ToList();
|
||||||
|
|
||||||
|
if (competencyList.Count > MaxCompetencies)
|
||||||
|
throw new InvalidOperationException($"Нельзя добавить больше {MaxCompetencies} компетенций");
|
||||||
|
|
||||||
|
_competencies.Clear();
|
||||||
|
_competencies.AddRange(competencyList);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace Nashel.Modules.Identity.Domain.Entities;
|
||||||
|
|
||||||
|
public class Competency
|
||||||
|
{
|
||||||
|
public Guid Id { get; private set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
// EF Core constructor
|
||||||
|
private Competency() { }
|
||||||
|
|
||||||
|
private Competency(string name)
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Competency Create(string name)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
throw new ArgumentException("Название компетенции не может быть пустым", nameof(name));
|
||||||
|
|
||||||
|
return new Competency { Id = Guid.NewGuid(), Name = name.Trim() };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Nashel.Modules.Identity.Domain.Services;
|
||||||
|
|
||||||
|
public interface IPasswordHasher
|
||||||
|
{
|
||||||
|
string HashPassword(string password);
|
||||||
|
bool VerifyPassword(string password, string hash);
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Nashel.BuildingBlocks.Application.Abstractions;
|
||||||
|
using Nashel.Modules.Identity.Application.Commands;
|
||||||
|
using Nashel.Modules.Identity.Domain.Entities;
|
||||||
|
using Nashel.Modules.Identity.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.CommandHandlers;
|
||||||
|
|
||||||
|
public class UpdateProfileCompetenciesCommandHandler : IRequestHandler<UpdateProfileCompetenciesCommand>
|
||||||
|
{
|
||||||
|
private readonly IdentityDbContext _context;
|
||||||
|
private readonly ICurrentUserService _currentUserService;
|
||||||
|
|
||||||
|
public UpdateProfileCompetenciesCommandHandler(IdentityDbContext context, ICurrentUserService currentUserService)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_currentUserService = currentUserService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Handle(UpdateProfileCompetenciesCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();
|
||||||
|
|
||||||
|
var profile = await _context.UserProfiles
|
||||||
|
.Include(p => p.Competencies)
|
||||||
|
.FirstOrDefaultAsync(p => p.Id == userId, cancellationToken);
|
||||||
|
|
||||||
|
if (profile == null)
|
||||||
|
throw new InvalidOperationException("Профиль не найден");
|
||||||
|
|
||||||
|
var competencies = new List<Competency>();
|
||||||
|
|
||||||
|
foreach (var name in request.CompetencyNames.Distinct())
|
||||||
|
{
|
||||||
|
// Ищем существующую компетенцию (case-insensitive)
|
||||||
|
var existing = await _context.Competencies
|
||||||
|
.FirstOrDefaultAsync(c => c.Name.ToLower() == name.ToLower(), cancellationToken);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
competencies.Add(existing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Создаем новую
|
||||||
|
var newCompetency = Competency.Create(name);
|
||||||
|
_context.Competencies.Add(newCompetency);
|
||||||
|
competencies.Add(newCompetency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем компетенции профиля (проверка лимита внутри метода)
|
||||||
|
profile.UpdateCompetencies(competencies);
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IAccountRepository, AccountRepository>();
|
services.AddScoped<IAccountRepository, AccountRepository>();
|
||||||
services.AddScoped<IJwtTokenGenerator, JwtTokenGenerator>();
|
services.AddScoped<IJwtTokenGenerator, JwtTokenGenerator>();
|
||||||
services.AddScoped<ICurrentUserService, CurrentUserService>();
|
services.AddScoped<ICurrentUserService, CurrentUserService>();
|
||||||
|
services.AddScoped<IPasswordHasher, PasswordHasher>();
|
||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
services.AddMediatR(cfg =>
|
services.AddMediatR(cfg =>
|
||||||
|
|||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Nashel.Modules.Identity.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
public class CompetencyConfiguration : IEntityTypeConfiguration<Competency>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Competency> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Competencies", "identity");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.Property(x => x.Name)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
// Unique index on Name (case-insensitive)
|
||||||
|
builder.HasIndex(x => x.Name)
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Competencies_Name");
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Nashel.BuildingBlocks.Domain;
|
||||||
|
using Nashel.Modules.Identity.Domain.Aggregates;
|
||||||
|
using Nashel.Modules.Identity.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
public class UserProfileConfiguration : IEntityTypeConfiguration<UserProfile>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<UserProfile> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("UserProfiles", "identity");
|
||||||
|
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
|
||||||
|
builder.Property(x => x.FirstName)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder.Property(x => x.LastName)
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder.Property(x => x.Patronymic)
|
||||||
|
.HasMaxLength(100);
|
||||||
|
|
||||||
|
builder.Property(x => x.CompanyName)
|
||||||
|
.HasMaxLength(200);
|
||||||
|
|
||||||
|
builder.Property(x => x.AvatarUrl)
|
||||||
|
.HasMaxLength(500);
|
||||||
|
|
||||||
|
// Many-to-Many relationship with Competencies
|
||||||
|
builder.HasMany(x => x.Competencies)
|
||||||
|
.WithMany()
|
||||||
|
.UsingEntity<Dictionary<string, object>>(
|
||||||
|
"UserProfileCompetencies",
|
||||||
|
j => j.HasOne<Competency>().WithMany().HasForeignKey("CompetencyId"),
|
||||||
|
j => j.HasOne<UserProfile>().WithMany().HasForeignKey("UserProfileId"),
|
||||||
|
j =>
|
||||||
|
{
|
||||||
|
j.ToTable("UserProfileCompetencies", "identity");
|
||||||
|
j.HasKey("UserProfileId", "CompetencyId");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1:1 relationship with Account
|
||||||
|
builder.HasOne<Account>()
|
||||||
|
.WithOne(x => x.Profile)
|
||||||
|
.HasForeignKey<UserProfile>(x => x.Id)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Nashel.Modules.Identity.Domain.Aggregates;
|
using Nashel.Modules.Identity.Domain.Aggregates;
|
||||||
|
using Nashel.Modules.Identity.Domain.Entities;
|
||||||
using Nashel.Modules.Identity.Infrastructure.Persistence.Configurations;
|
using Nashel.Modules.Identity.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
namespace Nashel.Modules.Identity.Infrastructure.Persistence;
|
namespace Nashel.Modules.Identity.Infrastructure.Persistence;
|
||||||
@@ -11,10 +12,14 @@ public class IdentityDbContext : DbContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
public DbSet<Account> Accounts { get; set; }
|
public DbSet<Account> Accounts { get; set; }
|
||||||
|
public DbSet<UserProfile> UserProfiles { get; set; }
|
||||||
|
public DbSet<Competency> Competencies { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
modelBuilder.ApplyConfiguration(new AccountConfiguration());
|
modelBuilder.ApplyConfiguration(new AccountConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new UserProfileConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new CompetencyConfiguration());
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
src/Modules/Identity/Infrastructure/Persistence/Migrations/20260213085826_AddUserProfile.Designer.cs
Generated
+104
@@ -0,0 +1,104 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Nashel.Modules.Identity.Infrastructure.Persistence;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(IdentityDbContext))]
|
||||||
|
[Migration("20260213085826_AddUserProfile")]
|
||||||
|
partial class AddUserProfile
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.2")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Roles")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Phone")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Accounts", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Patronymic")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("UserProfiles", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.Account", null)
|
||||||
|
.WithOne("Profile")
|
||||||
|
.HasForeignKey("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", "Id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Profile")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddUserProfile : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserProfiles",
|
||||||
|
schema: "identity",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FirstName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
LastName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||||
|
Patronymic = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||||
|
CompanyName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||||
|
AvatarUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserProfiles", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserProfiles_Accounts_Id",
|
||||||
|
column: x => x.Id,
|
||||||
|
principalSchema: "identity",
|
||||||
|
principalTable: "Accounts",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserProfiles",
|
||||||
|
schema: "identity");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Nashel.Modules.Identity.Infrastructure.Persistence;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(IdentityDbContext))]
|
||||||
|
[Migration("20260213103833_AddCompetencies")]
|
||||||
|
partial class AddCompetencies
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.2")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(20)
|
||||||
|
.HasColumnType("character varying(20)");
|
||||||
|
|
||||||
|
b.Property<string>("Roles")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Phone")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Accounts", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Patronymic")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("UserProfiles", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Entities.Competency", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Competencies_Name");
|
||||||
|
|
||||||
|
b.ToTable("Competencies", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UserProfileCompetencies", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserProfileId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("CompetencyId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserProfileId", "CompetencyId");
|
||||||
|
|
||||||
|
b.HasIndex("CompetencyId");
|
||||||
|
|
||||||
|
b.ToTable("UserProfileCompetencies", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.Account", null)
|
||||||
|
.WithOne("Profile")
|
||||||
|
.HasForeignKey("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", "Id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UserProfileCompetencies", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Identity.Domain.Entities.Competency", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CompetencyId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserProfileId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Profile")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddCompetencies : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Competencies",
|
||||||
|
schema: "identity",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Competencies", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "UserProfileCompetencies",
|
||||||
|
schema: "identity",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
UserProfileId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CompetencyId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_UserProfileCompetencies", x => new { x.UserProfileId, x.CompetencyId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserProfileCompetencies_Competencies_CompetencyId",
|
||||||
|
column: x => x.CompetencyId,
|
||||||
|
principalSchema: "identity",
|
||||||
|
principalTable: "Competencies",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_UserProfileCompetencies_UserProfiles_UserProfileId",
|
||||||
|
column: x => x.UserProfileId,
|
||||||
|
principalSchema: "identity",
|
||||||
|
principalTable: "UserProfiles",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Competencies_Name",
|
||||||
|
schema: "identity",
|
||||||
|
table: "Competencies",
|
||||||
|
column: "Name",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_UserProfileCompetencies_CompetencyId",
|
||||||
|
schema: "identity",
|
||||||
|
table: "UserProfileCompetencies",
|
||||||
|
column: "CompetencyId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "UserProfileCompetencies",
|
||||||
|
schema: "identity");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Competencies",
|
||||||
|
schema: "identity");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+97
@@ -48,6 +48,103 @@ namespace Nashel.Modules.Identity.Infrastructure.Persistence.Migrations
|
|||||||
|
|
||||||
b.ToTable("Accounts", "identity");
|
b.ToTable("Accounts", "identity");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AvatarUrl")
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("character varying(500)");
|
||||||
|
|
||||||
|
b.Property<string>("CompanyName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<string>("FirstName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Patronymic")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("UserProfiles", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Entities.Competency", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("IX_Competencies_Name");
|
||||||
|
|
||||||
|
b.ToTable("Competencies", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UserProfileCompetencies", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("UserProfileId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("CompetencyId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("UserProfileId", "CompetencyId");
|
||||||
|
|
||||||
|
b.HasIndex("CompetencyId");
|
||||||
|
|
||||||
|
b.ToTable("UserProfileCompetencies", "identity");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.Account", null)
|
||||||
|
.WithOne("Profile")
|
||||||
|
.HasForeignKey("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", "Id")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("UserProfileCompetencies", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Nashel.Modules.Identity.Domain.Entities.Competency", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CompetencyId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Nashel.Modules.Identity.Domain.Aggregates.UserProfile", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserProfileId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Nashel.Modules.Identity.Domain.Aggregates.Account", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Profile")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Nashel.Modules.Identity.Application.Queries;
|
||||||
|
using Nashel.Modules.Identity.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.QueryHandlers;
|
||||||
|
|
||||||
|
public class SearchCompetenciesQueryHandler : IRequestHandler<SearchCompetenciesQuery, List<string>>
|
||||||
|
{
|
||||||
|
private readonly IdentityDbContext _context;
|
||||||
|
|
||||||
|
public SearchCompetenciesQueryHandler(IdentityDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> Handle(SearchCompetenciesQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Query))
|
||||||
|
return new List<string>();
|
||||||
|
|
||||||
|
var query = request.Query.ToLower();
|
||||||
|
|
||||||
|
var competencies = await _context.Competencies
|
||||||
|
.Where(c => c.Name.ToLower().Contains(query))
|
||||||
|
.Select(c => c.Name)
|
||||||
|
.Take(10) // Ограничиваем результаты
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return competencies;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,12 +22,17 @@ public class AccountRepository : IAccountRepository
|
|||||||
|
|
||||||
public async Task<Account?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
|
public async Task<Account?> GetByIdAsync(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _context.Accounts.FindAsync(new object[] { id }, cancellationToken);
|
return await _context.Accounts
|
||||||
|
.Include(a => a.Profile)
|
||||||
|
.ThenInclude(p => p.Competencies)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Account?> GetByPhoneAsync(string phone, CancellationToken cancellationToken)
|
public async Task<Account?> GetByPhoneAsync(string phone, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _context.Accounts.FirstOrDefaultAsync(a => a.Phone == phone, cancellationToken);
|
return await _context.Accounts
|
||||||
|
.Include(a => a.Profile)
|
||||||
|
.FirstOrDefaultAsync(a => a.Phone == phone, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateAsync(Account account, CancellationToken cancellationToken)
|
public async Task UpdateAsync(Account account, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Nashel.Modules.Identity.Domain.Services;
|
||||||
|
|
||||||
|
namespace Nashel.Modules.Identity.Infrastructure.Services;
|
||||||
|
|
||||||
|
public class PasswordHasher : IPasswordHasher
|
||||||
|
{
|
||||||
|
private readonly PasswordHasher<object> _hasher = new();
|
||||||
|
private static readonly object _dummy = new();
|
||||||
|
|
||||||
|
public string HashPassword(string password)
|
||||||
|
{
|
||||||
|
return _hasher.HashPassword(_dummy, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool VerifyPassword(string password, string hash)
|
||||||
|
{
|
||||||
|
var result = _hasher.VerifyHashedPassword(_dummy, hash, password);
|
||||||
|
return result != PasswordVerificationResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
using Nashel.Modules.Identity.Application.Commands;
|
using Nashel.Modules.Identity.Application.Commands;
|
||||||
|
using Nashel.Modules.Identity.Application.Queries;
|
||||||
using Nashel.Modules.Identity.Application.Queries.GetProfile;
|
using Nashel.Modules.Identity.Application.Queries.GetProfile;
|
||||||
|
|
||||||
namespace Nashel.Modules.Identity.Presentation.Endpoints;
|
namespace Nashel.Modules.Identity.Presentation.Endpoints;
|
||||||
@@ -39,6 +40,22 @@ public static class IdentityEndpoints
|
|||||||
.WithName("GetProfile")
|
.WithName("GetProfile")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Получить профиль" });
|
.WithOpenApi(operation => new(operation) { Summary = "Получить профиль" });
|
||||||
|
|
||||||
|
profileGroup.MapPut("/", async (UpdateProfileCommand command, ISender sender) =>
|
||||||
|
{
|
||||||
|
await sender.Send(command);
|
||||||
|
return Results.NoContent();
|
||||||
|
})
|
||||||
|
.WithName("UpdateProfile")
|
||||||
|
.WithOpenApi(operation => new(operation) { Summary = "Обновить профиль" });
|
||||||
|
|
||||||
|
profileGroup.MapPost("/change-phone", async (ChangePhoneNumberCommand command, ISender sender) =>
|
||||||
|
{
|
||||||
|
await sender.Send(command);
|
||||||
|
return Results.NoContent();
|
||||||
|
})
|
||||||
|
.WithName("ChangePhone")
|
||||||
|
.WithOpenApi(operation => new(operation) { Summary = "Сменить номер телефона" });
|
||||||
|
|
||||||
profileGroup.MapPost("/become-performer", async (ISender sender) =>
|
profileGroup.MapPost("/become-performer", async (ISender sender) =>
|
||||||
{
|
{
|
||||||
await sender.Send(new BecomePerformerCommand());
|
await sender.Send(new BecomePerformerCommand());
|
||||||
@@ -46,7 +63,35 @@ public static class IdentityEndpoints
|
|||||||
})
|
})
|
||||||
.WithName("BecomePerformer")
|
.WithName("BecomePerformer")
|
||||||
.WithOpenApi(operation => new(operation) { Summary = "Стать исполнителем" });
|
.WithOpenApi(operation => new(operation) { Summary = "Стать исполнителем" });
|
||||||
|
|
||||||
|
profileGroup.MapPost("/avatar", async (IFormFile file, ISender sender) =>
|
||||||
|
{
|
||||||
|
var url = await sender.Send(new UploadAvatarCommand(file));
|
||||||
|
return Results.Ok(new { avatarUrl = url });
|
||||||
|
})
|
||||||
|
.WithName("UploadAvatar")
|
||||||
|
.WithOpenApi(operation => new(operation) { Summary = "Загрузить аватар" })
|
||||||
|
.DisableAntiforgery();
|
||||||
|
|
||||||
|
profileGroup.MapPut("/competencies", async (UpdateCompetenciesRequest request, ISender sender) =>
|
||||||
|
{
|
||||||
|
await sender.Send(new UpdateProfileCompetenciesCommand(request.Competencies));
|
||||||
|
return Results.NoContent();
|
||||||
|
})
|
||||||
|
.WithName("UpdateCompetencies")
|
||||||
|
.WithOpenApi(operation => new(operation) { Summary = "Обновить компетенции" });
|
||||||
|
|
||||||
|
var competenciesGroup = app.MapGroup("/api/competencies").WithTags("Competencies");
|
||||||
|
|
||||||
|
competenciesGroup.MapGet("/search", async (string q, ISender sender) =>
|
||||||
|
{
|
||||||
|
var results = await sender.Send(new SearchCompetenciesQuery(q));
|
||||||
|
return Results.Ok(results);
|
||||||
|
})
|
||||||
|
.WithName("SearchCompetencies")
|
||||||
|
.WithOpenApi(operation => new(operation) { Summary = "Поиск компетенций" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record LoginResponse(string AccessToken, string RefreshToken);
|
public record LoginResponse(string AccessToken, string RefreshToken);
|
||||||
|
public record UpdateCompetenciesRequest(List<string> Competencies);
|
||||||
|
|||||||
@@ -13,13 +13,18 @@ public class LoginCommandHandlerTests
|
|||||||
{
|
{
|
||||||
private readonly Mock<IAccountRepository> _mockRepo;
|
private readonly Mock<IAccountRepository> _mockRepo;
|
||||||
private readonly Mock<IJwtTokenGenerator> _mockTokenGen;
|
private readonly Mock<IJwtTokenGenerator> _mockTokenGen;
|
||||||
|
private readonly Mock<IPasswordHasher> _mockPasswordHasher;
|
||||||
private readonly LoginCommandHandler _handler;
|
private readonly LoginCommandHandler _handler;
|
||||||
|
|
||||||
public LoginCommandHandlerTests()
|
public LoginCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_mockRepo = new Mock<IAccountRepository>();
|
_mockRepo = new Mock<IAccountRepository>();
|
||||||
_mockTokenGen = new Mock<IJwtTokenGenerator>();
|
_mockTokenGen = new Mock<IJwtTokenGenerator>();
|
||||||
_handler = new LoginCommandHandler(_mockRepo.Object, _mockTokenGen.Object);
|
_mockPasswordHasher = new Mock<IPasswordHasher>();
|
||||||
|
_handler = new LoginCommandHandler(
|
||||||
|
_mockRepo.Object,
|
||||||
|
_mockTokenGen.Object,
|
||||||
|
_mockPasswordHasher.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -33,6 +38,9 @@ public class LoginCommandHandlerTests
|
|||||||
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(account);
|
.ReturnsAsync(account);
|
||||||
|
|
||||||
|
_mockPasswordHasher.Setup(p => p.VerifyPassword(password, account.PasswordHash))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
_mockTokenGen.Setup(t => t.GenerateToken(It.IsAny<Guid>(), It.IsAny<List<Role>>()))
|
_mockTokenGen.Setup(t => t.GenerateToken(It.IsAny<Guid>(), It.IsAny<List<Role>>()))
|
||||||
.Returns("jwt_token");
|
.Returns("jwt_token");
|
||||||
|
|
||||||
@@ -54,6 +62,9 @@ public class LoginCommandHandlerTests
|
|||||||
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(account);
|
.ReturnsAsync(account);
|
||||||
|
|
||||||
|
_mockPasswordHasher.Setup(p => p.VerifyPassword("wrong_password", account.PasswordHash))
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var act = async () => await _handler.Handle(new LoginCommand(phone, "wrong_password"), CancellationToken.None);
|
var act = async () => await _handler.Handle(new LoginCommand(phone, "wrong_password"), CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Moq;
|
|||||||
using Nashel.Modules.Identity.Application.Commands;
|
using Nashel.Modules.Identity.Application.Commands;
|
||||||
using Nashel.Modules.Identity.Domain.Aggregates;
|
using Nashel.Modules.Identity.Domain.Aggregates;
|
||||||
using Nashel.Modules.Identity.Domain.Repositories;
|
using Nashel.Modules.Identity.Domain.Repositories;
|
||||||
|
using Nashel.Modules.Identity.Domain.Services;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Nashel.Modules.Identity.Tests.Application;
|
namespace Nashel.Modules.Identity.Tests.Application;
|
||||||
@@ -10,12 +11,14 @@ namespace Nashel.Modules.Identity.Tests.Application;
|
|||||||
public class RegisterUserCommandHandlerTests
|
public class RegisterUserCommandHandlerTests
|
||||||
{
|
{
|
||||||
private readonly Mock<IAccountRepository> _mockRepo;
|
private readonly Mock<IAccountRepository> _mockRepo;
|
||||||
|
private readonly Mock<IPasswordHasher> _mockPasswordHasher;
|
||||||
private readonly RegisterUserCommandHandler _handler;
|
private readonly RegisterUserCommandHandler _handler;
|
||||||
|
|
||||||
public RegisterUserCommandHandlerTests()
|
public RegisterUserCommandHandlerTests()
|
||||||
{
|
{
|
||||||
_mockRepo = new Mock<IAccountRepository>();
|
_mockRepo = new Mock<IAccountRepository>();
|
||||||
_handler = new RegisterUserCommandHandler(_mockRepo.Object);
|
_mockPasswordHasher = new Mock<IPasswordHasher>();
|
||||||
|
_handler = new RegisterUserCommandHandler(_mockRepo.Object, _mockPasswordHasher.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -26,8 +29,17 @@ public class RegisterUserCommandHandlerTests
|
|||||||
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
_mockRepo.Setup(r => r.GetByPhoneAsync(phone, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync((Account?)null);
|
.ReturnsAsync((Account?)null);
|
||||||
|
|
||||||
|
_mockPasswordHasher.Setup(p => p.HashPassword("pass"))
|
||||||
|
.Returns("hashed_pass");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = await _handler.Handle(new RegisterUserCommand(phone, "pass"), CancellationToken.None);
|
var result = await _handler.Handle(new RegisterUserCommand
|
||||||
|
{
|
||||||
|
Phone = phone,
|
||||||
|
Password = "pass",
|
||||||
|
FirstName = "Ivan",
|
||||||
|
LastName = "Ivanov"
|
||||||
|
}, CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.Should().NotBeEmpty();
|
result.Should().NotBeEmpty();
|
||||||
@@ -43,7 +55,13 @@ public class RegisterUserCommandHandlerTests
|
|||||||
.ReturnsAsync(Account.Create(phone, "pass"));
|
.ReturnsAsync(Account.Create(phone, "pass"));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var act = async () => await _handler.Handle(new RegisterUserCommand(phone, "pass"), CancellationToken.None);
|
var act = async () => await _handler.Handle(new RegisterUserCommand
|
||||||
|
{
|
||||||
|
Phone = phone,
|
||||||
|
Password = "pass",
|
||||||
|
FirstName = "Ivan",
|
||||||
|
LastName = "Ivanov"
|
||||||
|
}, CancellationToken.None);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await act.Should().ThrowAsync<Exception>().WithMessage("Пользователь уже существует");
|
await act.Should().ThrowAsync<Exception>().WithMessage("Пользователь уже существует");
|
||||||
|
|||||||
Reference in New Issue
Block a user