add account management

This commit is contained in:
cuqmbr 2025-05-28 15:40:30 +03:00
parent 2d7d23d26b
commit 7229a10ad5
Signed by: cuqmbr
GPG Key ID: 0AA446880C766199
24 changed files with 813 additions and 165 deletions

View File

@ -1,10 +1,13 @@
using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Domain.Enums;
using MediatR; using MediatR;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; namespace cuqmbr.TravelGuide.Application.Identity
.Accounts.Commands.AddAccount;
public record AddAccountCommand : IRequest<AccountDto> public record AddAccountCommand : IRequest<AccountDto>
{ {
public string Username { get; set; }
public string Email { get; set; } public string Email { get; set; }
public string Password { get; set; } public string Password { get; set; }

View File

@ -3,7 +3,8 @@ using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums; using cuqmbr.TravelGuide.Domain.Enums;
using MediatR.Behaviors.Authorization; using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; namespace cuqmbr.TravelGuide.Application
.Identity.Accounts.Commands.AddAccount;
public class AddAccountCommandAuthorizer : public class AddAccountCommandAuthorizer :
AbstractRequestAuthorizer<AddAccountCommand> AbstractRequestAuthorizer<AddAccountCommand>

View File

@ -7,34 +7,33 @@ using cuqmbr.TravelGuide.Application.Common.Services;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; namespace cuqmbr.TravelGuide.Application
.Identity.Accounts.Commands.AddAccount;
public class AddAccountCommandHandler : public class AddAccountCommandHandler :
IRequestHandler<AddAccountCommand, AccountDto> IRequestHandler<AddAccountCommand, AccountDto>
{ {
private readonly UnitOfWork _unitOfWork; private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly PasswordHasherService _passwordHasherService; private readonly PasswordHasherService _passwordHasher;
public AddAccountCommandHandler( public AddAccountCommandHandler(UnitOfWork unitOfWork,
UnitOfWork unitOfWork, IMapper mapper, PasswordHasherService passwordHasher)
IMapper mapper,
PasswordHasherService passwordHasherService)
{ {
_unitOfWork = unitOfWork; _unitOfWork = unitOfWork;
_mapper = mapper; _mapper = mapper;
_passwordHasherService = passwordHasherService; _passwordHasher = passwordHasher;
} }
public async Task<AccountDto> Handle( public async Task<AccountDto> Handle(
AddAccountCommand request, AddAccountCommand request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var user = await _unitOfWork.AccountRepository.GetOneAsync( var account = await _unitOfWork.AccountRepository.GetOneAsync(
e => e.Email == request.Email, e => e.Email == request.Email,
cancellationToken); cancellationToken);
if (user != null) if (account != null)
{ {
throw new DuplicateEntityException(); throw new DuplicateEntityException();
} }
@ -47,15 +46,16 @@ public class AddAccountCommandHandler :
.Items; .Items;
var salt = RandomNumberGenerator.GetBytes(128 / 8); var salt = RandomNumberGenerator.GetBytes(128 / 8);
var hash = await _passwordHasherService.HashAsync( var hash = await _passwordHasher.HashAsync(
Encoding.UTF8.GetBytes(request.Password), Encoding.UTF8.GetBytes(request.Password),
salt, cancellationToken); salt, cancellationToken);
var saltBase64 = Convert.ToBase64String(salt); var saltBase64 = Convert.ToBase64String(salt);
var hashBase64 = Convert.ToBase64String(hash); var hashBase64 = Convert.ToBase64String(hash);
user = new Account() account = new Account()
{ {
Username = request.Username,
Email = request.Email, Email = request.Email,
PasswordHash = hashBase64, PasswordHash = hashBase64,
PasswordSalt = saltBase64, PasswordSalt = saltBase64,
@ -66,12 +66,12 @@ public class AddAccountCommandHandler :
.ToArray() .ToArray()
}; };
user = await _unitOfWork.AccountRepository.AddOneAsync( account = await _unitOfWork.AccountRepository.AddOneAsync(
user, cancellationToken); account, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken); await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose(); _unitOfWork.Dispose();
return _mapper.Map<AccountDto>(user); return _mapper.Map<AccountDto>(account);
} }
} }

View File

@ -1,16 +1,37 @@
using cuqmbr.TravelGuide.Application.Common.FluentValidation; using cuqmbr.TravelGuide.Application.Common.FluentValidation;
using cuqmbr.TravelGuide.Application.Common.Services; using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation; using FluentValidation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; namespace cuqmbr.TravelGuide.Application
.Identity.Accounts.Commands.AddAccount;
public class AddAccountCommandValidator : AbstractValidator<AddAccountCommand> public class AddAccountCommandValidator :
AbstractValidator<AddAccountCommand>
{ {
public AddAccountCommandValidator( public AddAccountCommandValidator(
IStringLocalizer localizer, IStringLocalizer localizer,
SessionCultureService cultureService) SessionCultureService cultureService)
{ {
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) RuleFor(v => v.Email)
.NotEmpty() .NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]) .WithMessage(localizer["FluentValidation.NotEmpty"])
@ -32,5 +53,18 @@ public class AddAccountCommandValidator : AbstractValidator<AddAccountCommand>
cultureService.Culture, cultureService.Culture,
localizer["FluentValidation.MaximumLength"], localizer["FluentValidation.MaximumLength"],
64)); 64));
RuleFor(v => v.Roles ?? new IdentityRole[0])
.IsUnique(r => r)
.WithMessage(localizer["FluentValidation.IsUnique"]);
RuleForEach(v => v.Roles)
.Must(r => IdentityRole.Enumerations.ContainsValue(r))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
IdentityRole.Enumerations.Values.Select(e => e.Name))));
} }
} }

View File

@ -0,0 +1,9 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Identity
.Accounts.Commands.DeleteAccount;
public record DeleteAccountCommand : IRequest
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,32 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Identity
.Accounts.Commands.DeleteAccount;
public class DeleteAccountCommandAuthorizer :
AbstractRequestAuthorizer<DeleteAccountCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteAccountCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteAccountCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,34 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount;
public class DeleteAccountCommandHandler : IRequestHandler<DeleteAccountCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteAccountCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteAccountCommand request,
CancellationToken cancellationToken)
{
var account = await _unitOfWork.AccountRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (account == null)
{
throw new NotFoundException();
}
await _unitOfWork.AccountRepository.DeleteOneAsync(
account, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount;
public class DeleteAccountCommandValidator : AbstractValidator<DeleteAccountCommand>
{
public DeleteAccountCommandValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,18 @@
using MediatR;
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application
.Identity.Accounts.Commands.UpdateAccount;
public record UpdateAccountCommand : IRequest<AccountDto>
{
public Guid Guid { get; set; }
public string? Username { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public ICollection<IdentityRole>? Roles { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.UpdateAccount;
public class UpdateAccountCommandAuthorizer :
AbstractRequestAuthorizer<UpdateAccountCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateAccountCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateAccountCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,109 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using System.Security.Cryptography;
using System.Text;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application
.Identity.Accounts.Commands.UpdateAccount;
public class UpdateAccountCommandHandler :
IRequestHandler<UpdateAccountCommand, AccountDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly PasswordHasherService _passwordHasher;
public UpdateAccountCommandHandler(UnitOfWork unitOfWork,
IMapper mapper, PasswordHasherService passwordHasher)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_passwordHasher = passwordHasher;
}
public async Task<AccountDto> Handle(
UpdateAccountCommand request,
CancellationToken cancellationToken)
{
var account = await _unitOfWork.AccountRepository
.GetOneAsync(e => e.Guid == request.Guid,
e => e.AccountRoles, cancellationToken);
if (account == null)
{
throw new NotFoundException();
}
account.Username = request.Username ?? account.Username;
account.Email = request.Email ?? account.Email;
if (request.Password != null)
{
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.PasswordHash = hashBase64;
account.PasswordSalt = saltBase64;
}
if (request.Roles != null)
{
var requestRoleIds = (await _unitOfWork.RoleRepository
.GetPageAsync(
r => request.Roles.Contains(r.Value),
1, request.Roles.Count, cancellationToken))
.Items
.Select(r => r.Id);
var accountRoles = account.AccountRoles;
var accountRoleIds = accountRoles.Select(ar => ar.RoleId);
var commonRoleIds = requestRoleIds.Intersect(accountRoleIds);
var newRoleIds = requestRoleIds.Except(accountRoleIds);
var combinedRoleIds = commonRoleIds.Union(newRoleIds);
account.AccountRoles = combinedRoleIds.Select(rId =>
new AccountRole()
{
Id = accountRoles.FirstOrDefault(ar =>
ar.RoleId == rId)?.Id ?? default,
RoleId = rId
})
.ToList();
}
else
{
var accountRoleIds = account.AccountRoles.Select(ar => ar.RoleId);
var accountRoles = (await _unitOfWork.AccountRoleRepository
.GetPageAsync(
ar => accountRoleIds.Contains(ar.RoleId),
ar => ar.Role,
1, accountRoleIds.Count(), cancellationToken))
.Items;
account.AccountRoles = accountRoles.ToList();
}
account = await _unitOfWork.AccountRepository.UpdateOneAsync(
account, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<AccountDto>(account);
}
}

View File

@ -0,0 +1,68 @@
using cuqmbr.TravelGuide.Application.Common.FluentValidation;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application
.Identity.Accounts.Commands.UpdateAccount;
public class UpdateAccountCommandValidator :
AbstractValidator<UpdateAccountCommand>
{
public UpdateAccountCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Username)
.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)
.IsEmail()
.WithMessage(localizer["FluentValidation.IsEmail"]);
RuleFor(v => v.Password)
.MinimumLength(8)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MinimumLength"],
8))
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Roles ?? new IdentityRole[0])
.IsUnique(r => r)
.WithMessage(localizer["FluentValidation.IsUnique"]);
RuleForEach(v => v.Roles)
.Must(r => IdentityRole.Enumerations.ContainsValue(r))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
IdentityRole.Enumerations.Values.Select(e => e.Name))));
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount;
public record GetAccountQuery : IRequest<AccountDto>
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount;
public class GetAccountQueryAuthorizer :
AbstractRequestAuthorizer<GetAccountQuery>
{
private readonly SessionUserService _sessionUserService;
public GetAccountQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetAccountQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,51 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount;
public class GetAccountQueryHandler :
IRequestHandler<GetAccountQuery, AccountDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAccountQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AccountDto> Handle(
GetAccountQuery request,
CancellationToken cancellationToken)
{
var account = await _unitOfWork.AccountRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.AccountRoles,
cancellationToken);
if (account == null)
{
throw new NotFoundException();
}
var accountRoleIds = account.AccountRoles.Select(ar => ar.RoleId);
var accountRoles = (await _unitOfWork.AccountRoleRepository
.GetPageAsync(
ar => accountRoleIds.Contains(ar.RoleId),
ar => ar.Role,
1, accountRoleIds.Count(), cancellationToken))
.Items;
account.AccountRoles = accountRoles.ToList();
_unitOfWork.Dispose();
return _mapper.Map<AccountDto>(account);
}
}

View File

@ -0,0 +1,14 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount;
public class GetAccountQueryValidator : AbstractValidator<GetAccountQuery>
{
public GetAccountQueryValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,18 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage;
public record GetAccountsPageQuery : IRequest<PaginatedList<AccountDto>>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string Search { get; set; } = String.Empty;
public string Sort { get; set; } = String.Empty;
public ICollection<IdentityRole>? Roles { get; set; }
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage;
public class GetAccountsPageQueryAuthorizer :
AbstractRequestAuthorizer<GetAccountsPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetAccountsPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetAccountsPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,81 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage;
public class GetAccountsPageQueryHandler :
IRequestHandler<GetAccountsPageQuery, PaginatedList<AccountDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAccountsPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<AccountDto>> Handle(
GetAccountsPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.AccountRepository.GetPageAsync(
a =>
(a.Username.ToLower().Contains(request.Search.ToLower()) ||
a.Email.ToLower().Contains(request.Search.ToLower())) &&
(request.Roles != null
? request.Roles.All(r => a.AccountRoles.Any(ar => ar.Role.Value == r))
: true),
a => a.AccountRoles,
request.PageNumber, request.PageSize, cancellationToken);
var accounts = paginatedList.Items;
var accountsRoleIds = accounts
.SelectMany(a => a.AccountRoles)
.Select(ar => ar.RoleId)
.Distinct();
var roles = (await _unitOfWork.RoleRepository
.GetPageAsync(
r => accountsRoleIds.Contains(r.Id),
1, accountsRoleIds.Count(), cancellationToken))
.Items;
foreach (var account in accounts)
{
account.AccountRoles = account.AccountRoles.Select(ar =>
new AccountRole()
{
RoleId = ar.RoleId,
Role = roles.Single(r => r.Id == ar.RoleId),
AccountId = account.Id,
Account = account
})
.ToArray();
}
var mappedItems = _mapper
.ProjectTo<AccountDto>(accounts.AsQueryable());
mappedItems = QueryableExtension<AccountDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<AccountDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage;
public class GetAccountsPageQueryValidator : AbstractValidator<GetAccountsPageQuery>
{
public GetAccountsPageQueryValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.PageNumber)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1));
RuleFor(v => v.PageSize)
.GreaterThanOrEqualTo(1)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
1))
.LessThanOrEqualTo(50)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
50));
RuleFor(v => v.Search)
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
}
}

View File

@ -2,6 +2,8 @@ namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels;
public sealed class AddAccountViewModel public sealed class AddAccountViewModel
{ {
public string Username { get; set; }
public string Email { get; set; } public string Email { get; set; }
public string Password { get; set; } public string Password { get; set; }

View File

@ -0,0 +1,6 @@
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels;
public sealed class GetAccountsPageFilterViewModel
{
public ICollection<string>? Roles { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels;
public sealed class UpdateAccountViewModel
{
public Guid Uuid { get; set; }
public string? Username { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public ICollection<string>? Roles { get; set; }
}

View File

@ -7,12 +7,10 @@ using cuqmbr.TravelGuide.Application.Identity.Roles.Queries.GetRolesPage;
using cuqmbr.TravelGuide.Application.Identity.Accounts; using cuqmbr.TravelGuide.Application.Identity.Accounts;
using cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels; using cuqmbr.TravelGuide.Application.Identity.Accounts.ViewModels;
using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount; using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.AddAccount;
// using cuqmbr.TravelGuide.Application.Identity.Commands.AddIdentity; using cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccountsPage;
// using cuqmbr.TravelGuide.Application.Identity.Queries.GetIdentityPage; using cuqmbr.TravelGuide.Application.Identity.Accounts.Queries.GetAccount;
// using cuqmbr.TravelGuide.Application.Identity.Queries.GetIdentity; using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.UpdateAccount;
// using cuqmbr.TravelGuide.Application.Identity.Commands.UpdateIdentity; using cuqmbr.TravelGuide.Application.Identity.Accounts.Commands.DeleteAccount;
// using cuqmbr.TravelGuide.Application.Identity.Commands.DeleteIdentity;
// using cuqmbr.TravelGuide.Application.Identity.ViewModels;
namespace cuqmbr.TravelGuide.HttpApi.Controllers; namespace cuqmbr.TravelGuide.HttpApi.Controllers;
@ -78,7 +76,7 @@ public class IdentityController : ControllerBase
[SwaggerResponse( [SwaggerResponse(
StatusCodes.Status500InternalServerError, "Internal server error", StatusCodes.Status500InternalServerError, "Internal server error",
typeof(ProblemDetails))] typeof(ProblemDetails))]
public async Task<ActionResult<AccountDto>> Add( public async Task<ActionResult<AccountDto>> AddAccount(
[FromBody] AddAccountViewModel viewModel, [FromBody] AddAccountViewModel viewModel,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -87,6 +85,7 @@ public class IdentityController : ControllerBase
await Mediator.Send( await Mediator.Send(
new AddAccountCommand() new AddAccountCommand()
{ {
Username = viewModel.Username,
Email = viewModel.Email, Email = viewModel.Email,
Password = viewModel.Password, Password = viewModel.Password,
Roles = viewModel.Roles Roles = viewModel.Roles
@ -96,147 +95,144 @@ public class IdentityController : ControllerBase
cancellationToken)); cancellationToken));
} }
[HttpGet("accounts")]
[SwaggerOperation("Get a list of all accounts")]
[SwaggerResponse(
// [HttpPost] StatusCodes.Status200OK, "Request successful",
// [SwaggerOperation("Add an identity")] typeof(PaginatedList<AccountDto>))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status201Created, "Object successfuly created", StatusCodes.Status400BadRequest, "Input data validation error",
// typeof(IdentityDto))] typeof(HttpValidationProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Object already exists", StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
// typeof(ProblemDetails))] typeof(ProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Input data validation error", StatusCodes.Status403Forbidden,
// typeof(HttpValidationProblemDetails))] "Not enough privileges to perform an action",
// [SwaggerResponse( typeof(ProblemDetails))]
// StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", [SwaggerResponse(
// typeof(ProblemDetails))] StatusCodes.Status500InternalServerError, "Internal server error",
// [SwaggerResponse( typeof(ProblemDetails))]
// StatusCodes.Status403Forbidden, public async Task<PaginatedList<AccountDto>> GetAccountsPage(
// "Not enough privileges to perform an action", [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery,
// typeof(ProblemDetails))] [FromQuery] SortQuery sortQuery,
// [SwaggerResponse( [FromQuery] GetAccountsPageFilterViewModel filterQuery,
// StatusCodes.Status404NotFound, "Parent object not found", CancellationToken cancellationToken)
// typeof(ProblemDetails))] {
// [SwaggerResponse( return await Mediator.Send(
// StatusCodes.Status500InternalServerError, "Internal server error", new GetAccountsPageQuery()
// typeof(ProblemDetails))] {
// public async Task<ActionResult<IdentityDto>> Add( PageNumber = pageQuery.PageNumber,
// [FromBody] AddIdentityViewModel viewModel, PageSize = pageQuery.PageSize,
// CancellationToken cancellationToken) Search = searchQuery.Search,
// { Sort = sortQuery.Sort,
// return StatusCode( Roles = filterQuery.Roles == null ? null :
// StatusCodes.Status201Created, filterQuery.Roles
// await Mediator.Send( .Select(s => IdentityRole.FromName(s))
// new AddIdentityCommand() .ToArray()
// { },
// Name = viewModel.Name, cancellationToken);
// Longitude = viewModel.Longitude, }
// Latitude = viewModel.Latitude,
// VehicleType = VehicleType.FromName(viewModel.VehicleType), [HttpGet("accounts/{uuid:guid}")]
// CityGuid = viewModel.CityUuid [SwaggerOperation("Get an account by uuid")]
// }, [SwaggerResponse(
// cancellationToken)); StatusCodes.Status200OK, "Request successful", typeof(AccountDto))]
// } [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Input data validation error",
// [HttpGet("{uuid:guid}")] typeof(HttpValidationProblemDetails))]
// [SwaggerOperation("Get an identity by uuid")] [SwaggerResponse(
// [SwaggerResponse( StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
// StatusCodes.Status200OK, "Request successful", typeof(IdentityDto))] typeof(ProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Input data validation error", StatusCodes.Status403Forbidden,
// typeof(HttpValidationProblemDetails))] "Not enough privileges to perform an action",
// [SwaggerResponse( typeof(ProblemDetails))]
// StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", [SwaggerResponse(
// typeof(ProblemDetails))] StatusCodes.Status404NotFound, "Object not found", typeof(ProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status403Forbidden, StatusCodes.Status500InternalServerError, "Internal server error",
// "Not enough privileges to perform an action", typeof(ProblemDetails))]
// typeof(ProblemDetails))] public async Task<AccountDto> GetAccount(
// [SwaggerResponse( [FromRoute] Guid uuid,
// StatusCodes.Status404NotFound, "Object not found", typeof(IdentityDto))] CancellationToken cancellationToken)
// [SwaggerResponse( {
// StatusCodes.Status500InternalServerError, "Internal server error", return await Mediator.Send(new GetAccountQuery() { Guid = uuid },
// typeof(ProblemDetails))] cancellationToken);
// public async Task<IdentityDto> Get( }
// [FromRoute] Guid uuid,
// CancellationToken cancellationToken) [HttpPut("accounts/{uuid:guid}")]
// { [SwaggerOperation("Update an account")]
// return await Mediator.Send(new GetIdentityQuery() { Guid = uuid }, [SwaggerResponse(
// cancellationToken); StatusCodes.Status200OK, "Request successful", typeof(AccountDto))]
// } [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Object already exists",
// [HttpPut("{uuid:guid}")] typeof(ProblemDetails))]
// [SwaggerOperation("Update an identity")] [SwaggerResponse(
// [SwaggerResponse( StatusCodes.Status400BadRequest, "Input data validation error",
// StatusCodes.Status200OK, "Request successful", typeof(IdentityDto))] typeof(HttpValidationProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Object already exists", StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
// typeof(ProblemDetails))] typeof(ProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Input data validation error", StatusCodes.Status403Forbidden,
// typeof(HttpValidationProblemDetails))] "Not enough privileges to perform an action",
// [SwaggerResponse( typeof(ProblemDetails))]
// StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", [SwaggerResponse(
// typeof(ProblemDetails))] StatusCodes.Status404NotFound, "Object not found",
// [SwaggerResponse( typeof(ProblemDetails))]
// StatusCodes.Status403Forbidden, [SwaggerResponse(
// "Not enough privileges to perform an action", StatusCodes.Status404NotFound, "Parent object not found",
// typeof(ProblemDetails))] typeof(ProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status404NotFound, "Object not found", typeof(IdentityDto))] StatusCodes.Status500InternalServerError, "Internal server error",
// [SwaggerResponse( typeof(ProblemDetails))]
// StatusCodes.Status404NotFound, "Parent object not found", public async Task<AccountDto> UpdateAccount(
// typeof(ProblemDetails))] [FromRoute] Guid uuid,
// [SwaggerResponse( [FromBody] UpdateAccountViewModel viewModel,
// StatusCodes.Status500InternalServerError, "Internal server error", CancellationToken cancellationToken)
// typeof(ProblemDetails))] {
// public async Task<IdentityDto> Update( return await Mediator.Send(
// [FromRoute] Guid uuid, new UpdateAccountCommand()
// [FromBody] UpdateIdentityViewModel viewModel, {
// CancellationToken cancellationToken) Guid = uuid,
// { Username = viewModel.Username,
// return await Mediator.Send( Email = viewModel.Email,
// new UpdateIdentityCommand() Password = viewModel.Password,
// { Roles = viewModel.Roles == null ? null :
// Guid = uuid, viewModel.Roles
// Name = viewModel.Name, .Select(s => IdentityRole.FromName(s))
// Longitude = viewModel.Longitude, .ToArray()
// Latitude = viewModel.Latitude, },
// VehicleType = VehicleType.FromName(viewModel.VehicleType), cancellationToken);
// CityGuid = viewModel.CityUuid }
// },
// cancellationToken); [HttpDelete("accounts/{uuid:guid}")]
// } [SwaggerOperation("Delete an account")]
// [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")]
// [HttpDelete("{uuid:guid}")] [SwaggerResponse(
// [SwaggerOperation("Delete an identity")] StatusCodes.Status400BadRequest, "Input data validation error",
// [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] typeof(HttpValidationProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status400BadRequest, "Input data validation error", StatusCodes.Status401Unauthorized, "Unauthorized to perform an action",
// typeof(HttpValidationProblemDetails))] typeof(ProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", StatusCodes.Status403Forbidden,
// typeof(ProblemDetails))] "Not enough privileges to perform an action",
// [SwaggerResponse( typeof(ProblemDetails))]
// StatusCodes.Status403Forbidden, [SwaggerResponse(
// "Not enough privileges to perform an action", StatusCodes.Status404NotFound, "Object not found",
// typeof(ProblemDetails))] typeof(ProblemDetails))]
// [SwaggerResponse( [SwaggerResponse(
// StatusCodes.Status404NotFound, "Object not found", StatusCodes.Status500InternalServerError, "Internal server error",
// typeof(ProblemDetails))] typeof(ProblemDetails))]
// [SwaggerResponse( public async Task<IActionResult> DeleteAccount(
// StatusCodes.Status500InternalServerError, "Internal server error", [FromRoute] Guid uuid,
// typeof(ProblemDetails))] CancellationToken cancellationToken)
// public async Task<IActionResult> Delete( {
// [FromRoute] Guid uuid, await Mediator.Send(
// CancellationToken cancellationToken) new DeleteAccountCommand() { Guid = uuid },
// { cancellationToken);
// await Mediator.Send( return StatusCode(StatusCodes.Status204NoContent);
// new DeleteIdentityCommand() { Guid = uuid }, }
// cancellationToken);
// return StatusCode(StatusCodes.Status204NoContent);
// }
} }