Merge pull request 'last changes merge' (#2) from development into main
All checks were successful
/ tests (push) Successful in 49s
/ build (push) Successful in 5m10s
/ build-docker (push) Successful in 8m12s

Reviewed-on: #2
This commit is contained in:
cuqmbr 2025-06-04 09:16:21 +00:00
commit 89420ce1ee
628 changed files with 39260 additions and 4768 deletions

View File

@ -25,7 +25,6 @@ jobs:
cache: true
cache-dependency-path: |
src/Application/packages.lock.json
src/Identity/packages.lock.json
src/Infrastructure/packages.lock.json
src/Persistence/packages.lock.json
src/Configuration/packages.lock.json

View File

@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infra
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpApi", "src\HttpApi\HttpApi.csproj", "{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "src\Identity\Identity.csproj", "{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configuration", "src\Configuration\Configuration.csproj", "{1DCFA4EE-A545-42FE-A3BC-A606D2961298}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.IntegrationTests", "tst\Application.IntegrationTests\Application.IntegrationTests.csproj", "{B52B8651-10B8-488D-8ACF-9C4499F8A723}"
@ -48,10 +46,6 @@ Global
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.Build.0 = Release|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.Build.0 = Release|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Release|Any CPU.ActiveCfg = Release|Any CPU

View File

@ -0,0 +1,58 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Addresses;
public sealed class AddressDto : IMapFrom<Address>
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public string VehicleType { get; set; }
public Guid CountryUuid { get; set; }
public string CountryName { get; set; }
public Guid RegionUuid { get; set; }
public string RegionName { get; set; }
public Guid CityUuid { get; set; }
public string CityName { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Address, AddressDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.VehicleType,
opt => opt.MapFrom(s => s.VehicleType.Name))
.ForMember(
d => d.CountryUuid,
opt => opt.MapFrom(s => s.City.Region.Country.Guid))
.ForMember(
d => d.CountryName,
opt => opt.MapFrom(s => s.City.Region.Country.Name))
.ForMember(
d => d.RegionUuid,
opt => opt.MapFrom(s => s.City.Region.Guid))
.ForMember(
d => d.RegionName,
opt => opt.MapFrom(s => s.City.Region.Name))
.ForMember(
d => d.CityUuid,
opt => opt.MapFrom(s => s.City.Guid))
.ForMember(
d => d.CityName,
opt => opt.MapFrom(s => s.City.Name));
}
}

View File

@ -0,0 +1,17 @@
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress;
public record AddAddressCommand : IRequest<AddressDto>
{
public string Name { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public VehicleType VehicleType { get; set; }
public Guid CityGuid { 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.Addresses.Commands.AddAddress;
public class AddAddressCommandAuthorizer :
AbstractRequestAuthorizer<AddAddressCommand>
{
private readonly SessionUserService _sessionUserService;
public AddAddressCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddAddressCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInAnyOfRolesRequirement
{
RequiredRoles =
[IdentityRole.Administrator, IdentityRole.CompanyOwner],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,64 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress;
public class AddAddressCommandHandler :
IRequestHandler<AddAddressCommand, AddressDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddAddressCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AddressDto> Handle(
AddAddressCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AddressRepository.GetOneAsync(
e => e.Name == request.Name && e.City.Guid == request.CityGuid,
cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Address with given name already exists.");
}
var parentEntity = await _unitOfWork.CityRepository.GetOneAsync(
e => e.Guid == request.CityGuid, e => e.Region.Country,
cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CityGuid} not found.");
}
entity = new Address()
{
Name = request.Name,
Longitude = request.Longitude,
Latitude = request.Latitude,
VehicleType = request.VehicleType,
CityId = parentEntity.Id
};
entity = await _unitOfWork.AddressRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<AddressDto>(entity);
}
}

View File

@ -0,0 +1,65 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Enums;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress;
public class AddAddressCommandValidator : AbstractValidator<AddAddressCommand>
{
public AddAddressCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Latitude)
.GreaterThanOrEqualTo(-90)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
-90))
.LessThanOrEqualTo(90)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
90));
RuleFor(v => v.Longitude)
.GreaterThanOrEqualTo(-180)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.GreaterThanOrEqualTo"],
-180))
.LessThanOrEqualTo(180)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.LessThanOrEqualTo"],
180));
RuleFor(v => v.VehicleType)
.Must((v, vt) => VehicleType.Enumerations.ContainsValue(vt))
.WithMessage(
String.Format(
localizer["FluentValidation.MustBeInEnum"],
String.Join(
", ",
VehicleType.Enumerations.Values.Select(e => e.Name))));
RuleFor(v => v.CityGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress;
public record DeleteAddressCommand : IRequest
{
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.Addresses.Commands.DeleteAddress;
public class DeleteAddressCommandAuthorizer :
AbstractRequestAuthorizer<DeleteAddressCommand>
{
private readonly SessionUserService _sessionUserService;
public DeleteAddressCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(DeleteAddressCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInAnyOfRolesRequirement
{
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.Addresses.Commands.DeleteAddress;
public class DeleteAddressCommandHandler : IRequestHandler<DeleteAddressCommand>
{
private readonly UnitOfWork _unitOfWork;
public DeleteAddressCommandHandler(UnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Handle(
DeleteAddressCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AddressRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
await _unitOfWork.AddressRepository.DeleteOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

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

View File

@ -0,0 +1,19 @@
using MediatR;
using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress;
public record UpdateAddressCommand : IRequest<AddressDto>
{
public Guid Guid { get; set; }
public string Name { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public VehicleType VehicleType { get; set; }
public Guid CityGuid { 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.Addresses.Commands.UpdateAddress;
public class UpdateAddressCommandAuthorizer :
AbstractRequestAuthorizer<UpdateAddressCommand>
{
private readonly SessionUserService _sessionUserService;
public UpdateAddressCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(UpdateAddressCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInAnyOfRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,58 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress;
public class UpdateAddressCommandHandler :
IRequestHandler<UpdateAddressCommand, AddressDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateAddressCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AddressDto> Handle(
UpdateAddressCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AddressRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.City.Region.Country,
cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var parentEntity = await _unitOfWork.CityRepository.GetOneAsync(
e => e.Guid == request.CityGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CityGuid} not found.");
}
entity.Name = request.Name;
entity.Longitude = request.Longitude;
entity.Latitude = request.Latitude;
entity.VehicleType = request.VehicleType;
entity.CityId = parentEntity.Id;
entity = await _unitOfWork.AddressRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<AddressDto>(entity);
}
}

View File

@ -0,0 +1,31 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress;
public class UpdateAddressCommandValidator : AbstractValidator<UpdateAddressCommand>
{
public UpdateAddressCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.CityGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress;
public record GetAddressQuery : IRequest<AddressDto>
{
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.Addresses.Queries.GetAddress;
public class GetAddressQueryAuthorizer :
AbstractRequestAuthorizer<GetAddressQuery>
{
private readonly SessionUserService _sessionUserService;
public GetAddressQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetAddressQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInAnyOfRolesRequirement
{
RequiredRoles =
[IdentityRole.Administrator, IdentityRole.CompanyOwner],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,39 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress;
public class GetAddressQueryHandler :
IRequestHandler<GetAddressQuery, AddressDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAddressQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AddressDto> Handle(
GetAddressQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AddressRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.City.Region.Country,
cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<AddressDto>(entity);
}
}

View File

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

View File

@ -0,0 +1,32 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage;
public record GetAddressesPageQuery : IRequest<PaginatedList<AddressDto>>
{
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 Guid? CountryGuid { get; set; }
public Guid? RegionGuid { get; set; }
public Guid? CityGuid { get; set; }
public double? LongitudeGreaterOrEqualThan { get; set; }
public double? LongitudeLessOrEqualThan { get; set; }
public double? LatitudeGreaterOrEqualThan { get; set; }
public double? LatitudeLessOrEqualThan { get; set; }
public VehicleType? VehicleType { 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.Addresses.Queries.GetAddressesPage;
public class GetAddressesPageQueryAuthorizer :
AbstractRequestAuthorizer<GetAddressesPageQuery>
{
private readonly SessionUserService _sessionUserService;
public GetAddressesPageQueryAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(GetAddressesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInAnyOfRolesRequirement
{
RequiredRoles =
[IdentityRole.Administrator, IdentityRole.CompanyOwner],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,74 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage;
public class GetAddressesPageQueryHandler :
IRequestHandler<GetAddressesPageQuery, PaginatedList<AddressDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAddressesPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<AddressDto>> Handle(
GetAddressesPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.AddressRepository.GetPageAsync(
e =>
(e.Name.ToLower().Contains(request.Search.ToLower()) ||
e.City.Name.ToLower().Contains(request.Search.ToLower()) ||
e.City.Region.Name.ToLower().Contains(request.Search.ToLower()) ||
e.City.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) &&
(request.LongitudeGreaterOrEqualThan != null
? e.Longitude >= request.LongitudeGreaterOrEqualThan
: true) &&
(request.LongitudeLessOrEqualThan != null
? e.Longitude <= request.LongitudeLessOrEqualThan
: true) &&
(request.LatitudeGreaterOrEqualThan != null
? e.Latitude >= request.LatitudeGreaterOrEqualThan
: true) &&
(request.LatitudeLessOrEqualThan != null
? e.Latitude <= request.LatitudeLessOrEqualThan
: true) &&
(request.VehicleType != null
? e.VehicleType == request.VehicleType
: true) &&
(request.CityGuid != null
? e.City.Guid == request.CityGuid
: true) &&
(request.RegionGuid != null
? e.City.Region.Guid == request.RegionGuid
: true) &&
(request.CountryGuid != null
? e.City.Region.Country.Guid == request.CountryGuid
: true),
e => e.City.Region.Country,
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<AddressDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<AddressDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<AddressDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddressesPage;
public class GetAddressesPageQueryValidator : AbstractValidator<GetAddressesPageQuery>
{
public GetAddressesPageQueryValidator(
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

@ -0,0 +1,14 @@
namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels;
public sealed class AddAddressViewModel
{
public string Name { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public string VehicleType { get; set; }
public Guid CityUuid { get; set; }
}

View File

@ -0,0 +1,20 @@
namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels;
public sealed class GetAddressesPageFilterViewModel
{
public Guid? CountryUuid { get; set; }
public Guid? RegionUuid { get; set; }
public Guid? CityUuid { get; set; }
public double? LongitudeGreaterOrEqualThan { get; set; }
public double? LongitudeLessOrEqualThan { get; set; }
public double? LatitudeGreaterOrEqualThan { get; set; }
public double? LatitudeLessOrEqualThan { get; set; }
public string? VehicleType { get; set; }
}

View File

@ -0,0 +1,14 @@
namespace cuqmbr.TravelGuide.Application.Addresses.ViewModels;
public sealed class UpdateAddressViewModel
{
public string Name { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public string VehicleType { get; set; }
public Guid CityUuid { get; set; }
}

View File

@ -0,0 +1,28 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Aircrafts;
public sealed class AircraftDto : IMapFrom<Aircraft>
{
public Guid Uuid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Aircraft, AircraftDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.CompanyUuid,
opt => opt.MapFrom(s => s.Company.Guid));
}
}

View File

@ -0,0 +1,14 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
public record AddAircraftCommand : IRequest<AircraftDto>
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
public class AddAircraftCommandAuthorizer :
AbstractRequestAuthorizer<AddAircraftCommand>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public AddAircraftCommandAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(AddAircraftCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var company = _unitOfWork.CompanyRepository
.GetOneAsync(
e => e.Guid == request.CompanyGuid, e => e.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = company?.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,63 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
public class AddAircraftCommandHandler :
IRequestHandler<AddAircraftCommand, AircraftDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddAircraftCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AircraftDto> Handle(
AddAircraftCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Number == request.Number, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Aircraft with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity = new Aircraft()
{
Number = request.Number,
Model = request.Model,
Capacity = request.Capacity,
CompanyId = parentEntity.Id,
Company = parentEntity
};
entity = await _unitOfWork.AircraftRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<AircraftDto>(entity);
}
}

View File

@ -0,0 +1,41 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
public class AddAircraftCommandValidator : AbstractValidator<AddAircraftCommand>
{
public AddAircraftCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft;
public record DeleteAircraftCommand : IRequest
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft;
public class DeleteAircraftCommandAuthorizer :
AbstractRequestAuthorizer<DeleteAircraftCommand>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public DeleteAircraftCommandAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(DeleteAircraftCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var vehicel = _unitOfWork.VehicleRepository
.GetOneAsync(
e => e.Guid == request.Guid, e => e.Company.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = vehicel?.Company.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

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

View File

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

View File

@ -0,0 +1,16 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
public record UpdateAircraftCommand : IRequest<AircraftDto>
{
public Guid Guid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
public class UpdateAircraftCommandAuthorizer :
AbstractRequestAuthorizer<UpdateAircraftCommand>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public UpdateAircraftCommandAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(UpdateAircraftCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var company = _unitOfWork.CompanyRepository
.GetOneAsync(
e => e.Guid == request.CompanyGuid, e => e.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = company?.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,67 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
public class UpdateAircraftCommandHandler :
IRequestHandler<UpdateAircraftCommand, AircraftDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateAircraftCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AircraftDto> Handle(
UpdateAircraftCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var duplicateEntity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Number == request.Number && e.Guid != request.Guid,
cancellationToken);
if (duplicateEntity != null)
{
throw new DuplicateEntityException(
"Aircraft with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity.Number = request.Number;
entity.Model = request.Model;
entity.Capacity = request.Capacity;
entity.CompanyId = parentEntity.Id;
entity.Company = parentEntity;
entity = await _unitOfWork.AircraftRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<AircraftDto>(entity);
}
}

View File

@ -0,0 +1,45 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
public class UpdateAircraftCommandValidator : AbstractValidator<UpdateAircraftCommand>
{
public UpdateAircraftCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
public record GetAircraftQuery : IRequest<AircraftDto>
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
public class GetAircraftQueryAuthorizer :
AbstractRequestAuthorizer<GetAircraftQuery>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public GetAircraftQueryAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(GetAircraftQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var vehicel = _unitOfWork.VehicleRepository
.GetOneAsync(
e => e.Guid == request.Guid, e => e.Company.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = vehicel?.Company.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,39 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
public class GetAircraftQueryHandler :
IRequestHandler<GetAircraftQuery, AircraftDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAircraftQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<AircraftDto> Handle(
GetAircraftQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.AircraftRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.Company,
cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<AircraftDto>(entity);
}
}

View File

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

View File

@ -0,0 +1,21 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
public record GetAircraftsPageQuery : IRequest<PaginatedList<AircraftDto>>
{
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 Guid? CompanyGuid { get; set; }
public short? CapacityGreaterThanOrEqualTo { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
public class GetAircraftsPageQueryAuthorizer :
AbstractRequestAuthorizer<GetAircraftsPageQuery>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public GetAircraftsPageQueryAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(GetAircraftsPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var company = _unitOfWork.CompanyRepository
.GetOneAsync(
e => e.Guid == request.CompanyGuid, e => e.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = company?.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,57 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
public class GetAircraftsPageQueryHandler :
IRequestHandler<GetAircraftsPageQuery, PaginatedList<AircraftDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetAircraftsPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<AircraftDto>> Handle(
GetAircraftsPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.AircraftRepository.GetPageAsync(
e =>
(e.Number.ToLower().Contains(request.Search.ToLower()) ||
e.Model.ToLower().Contains(request.Search.ToLower())) &&
(request.CompanyGuid != null
? e.Company.Guid == request.CompanyGuid
: true) &&
(request.CapacityGreaterThanOrEqualTo != null
? e.Capacity >= request.CapacityGreaterThanOrEqualTo
: true) &&
(request.CapacityLessThanOrEqualTo != null
? e.Capacity <= request.CapacityLessThanOrEqualTo
: true),
e => e.Company,
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<AircraftDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<AircraftDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<AircraftDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
public class GetAircraftsPageQueryValidator : AbstractValidator<GetAircraftsPageQuery>
{
public GetAircraftsPageQueryValidator(
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

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
public sealed class AddAircraftViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -0,0 +1,13 @@
namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
public sealed class GetAircraftsPageFilterViewModel
{
public Guid? CompanyUuid { get; set; }
// TODO: Consider adding strict equals rule although it is not
// necessarily needed to filter with exact capacity
public short? CapacityGreaterThanOrEqualTo { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.Aircrafts.ViewModels;
public sealed class UpdateAircraftViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -16,7 +16,12 @@
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.11.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.11.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="QuikGraph" Version="2.5.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
</ItemGroup>

View File

@ -4,6 +4,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public record RegisterCommand : IRequest
{
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }

View File

@ -0,0 +1,13 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandAuthorizer :
AbstractRequestAuthorizer<RegisterCommand>
{
public override void BuildPolicy(RegisterCommand request)
{
UseRequirement(new AllowAllRequirement());
}
}

View File

@ -1,21 +1,83 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using System.Security.Cryptography;
using System.Text;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Entities;
using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandHandler : IRequestHandler<RegisterCommand>
{
private readonly AuthenticationService _authenticationService;
private readonly IReadOnlyCollection<IdentityRole> DefaultRoles =
new IdentityRole[] { IdentityRole.User };
public RegisterCommandHandler(AuthenticationService authenticationService)
private readonly UnitOfWork _unitOfWork;
private readonly PasswordHasherService _passwordHasher;
public RegisterCommandHandler(UnitOfWork unitOfWork,
PasswordHasherService passwordHasher)
{
_authenticationService = authenticationService;
_unitOfWork = unitOfWork;
_passwordHasher = passwordHasher;
}
public async Task Handle(
RegisterCommand request, CancellationToken cancellationToken)
public async Task Handle(RegisterCommand request,
CancellationToken cancellationToken)
{
await _authenticationService.RegisterAsync(
request.Email, request.Password, cancellationToken);
var datastoreAccount = await _unitOfWork.AccountRepository
.GetOneAsync(
e =>
e.Email == request.Email ||
e.Username == request.Username,
cancellationToken);
if (datastoreAccount != null)
{
throw new RegistrationException(
"User with given email or username already registered.");
}
var defaultRoleIds = (await _unitOfWork.RoleRepository
.GetPageAsync(
r => DefaultRoles.Contains(r.Value),
1, DefaultRoles.Count, cancellationToken))
.Items
.Select(r => r.Id);
var password = Encoding.UTF8.GetBytes(request.Password);
var salt = RandomNumberGenerator.GetBytes(128 / 8);
var hash = await _passwordHasher
.HashAsync(password, salt, cancellationToken);
var saltBase64 = Convert.ToBase64String(salt);
var hashBase64 = Convert.ToBase64String(hash);
var newAccount = new Account
{
Username = request.Username,
Email = request.Email,
PasswordHash = hashBase64,
PasswordSalt = saltBase64,
AccountRoles = defaultRoleIds.Select(id =>
new AccountRole()
{
RoleId = id
})
.ToArray()
};
await _unitOfWork.AccountRepository
.AddOneAsync(newAccount, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -1,31 +1,54 @@
using cuqmbr.TravelGuide.Application.Common.FluentValidation;
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandValidator : AbstractValidator<RegisterCommand>
{
public RegisterCommandValidator()
public RegisterCommandValidator(
IStringLocalizer localizer,
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)
.NotEmpty()
.WithMessage("Email address is required.")
.Matches(@"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b")
.WithMessage("Email address is invalid.");
.WithMessage(localizer["FluentValidation.NotEmpty"])
.IsEmail()
.WithMessage(localizer["FluentValidation.IsEmail"]);
RuleFor(v => v.Password)
.NotEmpty()
.WithMessage("Password is required.")
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MinimumLength(8)
.WithMessage("Password must be at least 8 characters long.")
.MaximumLength(64)
.WithMessage("Password must be at most 64 characters long.")
.Matches(@"(?=.*[A-Z]).*")
.WithMessage("Password must contain at least one uppercase letter.")
.Matches(@"(?=.*[a-z]).*")
.WithMessage("Password must contain at least one lowercase letter.")
.Matches(@"(?=.*[\d]).*")
.WithMessage("Password must contain at least one digit.")
.Matches(@"(?=.*[!@#$%^&*()]).*")
.WithMessage("Password must contain at least one of the following special charactters: !@#$%^&*().");
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MinimumLength"],
8))
.MaximumLength(256)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
256));
}
}

View File

@ -1,5 +1,4 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
@ -7,18 +6,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken
public class RenewAccessTokenCommandAuthorizer :
AbstractRequestAuthorizer<RenewAccessTokenCommand>
{
private readonly SessionUserService _sessionUserService;
public RenewAccessTokenCommandAuthorizer(SessionUserService currentUserService)
{
_sessionUserService = currentUserService;
}
public override void BuildPolicy(RenewAccessTokenCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
UseRequirement(new AllowAllRequirement());
}
}

View File

@ -1,22 +1,95 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
public class RenewAccessTokenCommandHandler :
IRequestHandler<RenewAccessTokenCommand, TokensModel>
{
private readonly AuthenticationService _authenticationService;
private readonly UnitOfWork _unitOfWork;
private readonly JsonWebTokenConfigurationOptions _jwtConfiguration;
public RenewAccessTokenCommandHandler(AuthenticationService authenticationService)
public RenewAccessTokenCommandHandler(UnitOfWork unitOfWork,
IOptions<ConfigurationOptions> configurationOptions)
{
_authenticationService = authenticationService;
_unitOfWork = unitOfWork;
_jwtConfiguration = configurationOptions.Value.JsonWebToken;
}
public async Task<TokensModel> Handle(
RenewAccessTokenCommand request, CancellationToken cancellationToken)
{
return await _authenticationService.RenewAccessTokenAsync(
request.RefreshToken, cancellationToken);
var refreshToken = (await _unitOfWork.RefreshTokenRepository
.GetOneAsync(e => e.Value == request.RefreshToken,
cancellationToken));
if (refreshToken == null)
{
throw new AuthenticationException($"Refresh token was not found.");
}
if (!refreshToken.IsActive)
{
throw new AuthenticationException("Refresh token is inactive.");
}
var account = await _unitOfWork.AccountRepository.GetOneAsync(
a => a.RefreshTokens.Contains(refreshToken),
a => a.AccountRoles, cancellationToken);
var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken);
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
return new TokensModel(accessToken, refreshToken.Value);
}
private async Task<JwtSecurityToken> CreateJwtAsync(
Account account, CancellationToken cancellationToken)
{
var roleIds = account.AccountRoles.Select(ar => ar.RoleId);
var roles = (await _unitOfWork.RoleRepository
.GetPageAsync(
r => roleIds.Contains(r.Id),
1, roleIds.Count(), cancellationToken))
.Items.Select(r => r.Value);
var roleClaims = new List<Claim>();
foreach (var role in roles)
{
roleClaims.Add(new Claim("roles", role.Name));
}
var claims = new List<Claim>()
{
new Claim(JwtRegisteredClaimNames.Sub, account.Guid.ToString()),
new Claim(JwtRegisteredClaimNames.Nickname, account.Username),
new Claim(JwtRegisteredClaimNames.Email, account.Email)
}
.Union(roleClaims);
var expirationDateTimeUtc = DateTime.UtcNow.Add(
_jwtConfiguration.AccessTokenValidity);
var symmetricSecurityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_jwtConfiguration.IssuerSigningKey));
var signingCredentials = new SigningCredentials(
symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _jwtConfiguration.Issuer,
audience: _jwtConfiguration.Audience,
claims: claims,
expires: expirationDateTimeUtc,
signingCredentials: signingCredentials);
return jwtSecurityToken;
}
}

View File

@ -1,6 +0,0 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public record RenewAccessTokenWithCookieCommand : IRequest<TokensModel> { }

View File

@ -1,26 +0,0 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public class RenewAccessTokenWithCookieCommandAuthorizer :
AbstractRequestAuthorizer<RenewAccessTokenWithCookieCommand>
{
private readonly SessionUserService _sessionUserService;
public RenewAccessTokenWithCookieCommandAuthorizer(
SessionUserService currentUserService)
{
_sessionUserService = currentUserService;
}
public override void BuildPolicy(RenewAccessTokenWithCookieCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
}
}

View File

@ -1,28 +0,0 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public class RenewAccessTokenWithCookieCommandHandler :
IRequestHandler<RenewAccessTokenWithCookieCommand, TokensModel>
{
private readonly AuthenticationService _authenticationService;
private readonly SessionUserService _sessionUserService;
public RenewAccessTokenWithCookieCommandHandler(
AuthenticationService authenticationService,
SessionUserService sessionUserService)
{
_authenticationService = authenticationService;
_sessionUserService = sessionUserService;
}
public async Task<TokensModel> Handle(
RenewAccessTokenWithCookieCommand request,
CancellationToken cancellationToken)
{
return await _authenticationService.RenewAccessTokenAsync(
_sessionUserService.RefreshToken, cancellationToken);
}
}

View File

@ -1,10 +0,0 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RenewAccessTokenWithCookie;
public class RenewAccessTokenWithCookieCommandValidator :
AbstractValidator<RenewAccessTokenWithCookieCommand>
{
public RenewAccessTokenWithCookieCommandValidator() { }
}

View File

@ -1,5 +1,5 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;

View File

@ -1,4 +1,5 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
@ -6,17 +7,36 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshTok
public class RevokeRefreshTokenCommandHandler :
IRequestHandler<RevokeRefreshTokenCommand>
{
private readonly AuthenticationService _authenticationService;
private readonly UnitOfWork _unitOfWork;
public RevokeRefreshTokenCommandHandler(AuthenticationService authenticationService)
public RevokeRefreshTokenCommandHandler(UnitOfWork unitOfWork)
{
_authenticationService = authenticationService;
_unitOfWork = unitOfWork;
}
public async Task Handle(
RevokeRefreshTokenCommand request, CancellationToken cancellationToken)
{
await _authenticationService.RevokeRefreshTokenAsync(
request.RefreshToken, cancellationToken);
var refreshToken = (await _unitOfWork.RefreshTokenRepository
.GetOneAsync(e => e.Value == request.RefreshToken,
cancellationToken));
if (refreshToken == null)
{
throw new AuthenticationException("Invalid refreshToken");
}
if (!refreshToken.IsActive)
{
throw new AuthenticationException("RefreshToken already revoked");
}
refreshToken.RevocationTime = DateTimeOffset.UtcNow;
await _unitOfWork.RefreshTokenRepository
.UpdateOneAsync(refreshToken, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
}
}

View File

@ -1,6 +0,0 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public record RevokeRefreshTokenWithCookieCommand : IRequest { }

View File

@ -1,26 +0,0 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public class RevokeRefreshTokenWithCookieCommandAuthorizer :
AbstractRequestAuthorizer<RevokeRefreshTokenWithCookieCommand>
{
private readonly SessionUserService _sessionUserService;
public RevokeRefreshTokenWithCookieCommandAuthorizer(
SessionUserService currentUserService)
{
_sessionUserService = currentUserService;
}
public override void BuildPolicy(RevokeRefreshTokenWithCookieCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
}
}

View File

@ -1,28 +0,0 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public class RevokeRefreshTokenWithCookieCommandHandler :
IRequestHandler<RevokeRefreshTokenWithCookieCommand>
{
private readonly AuthenticationService _authenticationService;
private readonly SessionUserService _sessionUserService;
public RevokeRefreshTokenWithCookieCommandHandler(
AuthenticationService authenticationService,
SessionUserService sessionUserService)
{
_authenticationService = authenticationService;
_sessionUserService = sessionUserService;
}
public async Task Handle(
RevokeRefreshTokenWithCookieCommand request,
CancellationToken cancellationToken)
{
await _authenticationService.RevokeRefreshTokenAsync(
_sessionUserService.RefreshToken, cancellationToken);
}
}

View File

@ -1,10 +0,0 @@
using FluentValidation;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
.RevokeRefreshTokenWithCookie;
public class RevokeRefreshTokenWithCookieCommandValidator :
AbstractValidator<RevokeRefreshTokenWithCookieCommand>
{
public RevokeRefreshTokenWithCookieCommandValidator() { }
}

View File

@ -4,7 +4,7 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public record LoginQuery : IRequest<TokensModel>
{
public string Email { get; set; }
public string EmailOrUsername { get; set; }
public string Password { get; set; }
}

View File

@ -0,0 +1,12 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryAuthorizer : AbstractRequestAuthorizer<LoginQuery>
{
public override void BuildPolicy(LoginQuery request)
{
UseRequirement(new AllowAllRequirement());
}
}

View File

@ -1,21 +1,140 @@
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using cuqmbr.TravelGuide.Domain.Entities;
using MediatR;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryHandler : IRequestHandler<LoginQuery, TokensModel>
{
private readonly AuthenticationService _authenticationService;
private readonly UnitOfWork _unitOfWork;
private readonly PasswordHasherService _passwordHasher;
private readonly JsonWebTokenConfigurationOptions _jwtConfiguration;
public LoginQueryHandler(AuthenticationService authenticationService)
public LoginQueryHandler(UnitOfWork unitOfWork,
PasswordHasherService passwordHasher,
IOptions<ConfigurationOptions> configurationOptions)
{
_authenticationService = authenticationService;
_unitOfWork = unitOfWork;
_passwordHasher = passwordHasher;
_jwtConfiguration = configurationOptions.Value.JsonWebToken;
}
public async Task<TokensModel> Handle(
LoginQuery request, CancellationToken cancellationToken)
{
return await _authenticationService.LoginAsync(
request.Email, request.Password, cancellationToken);
var account = await _unitOfWork.AccountRepository
.GetOneAsync(
a =>
a.Email == request.EmailOrUsername ||
a.Username == request.EmailOrUsername,
a => a.AccountRoles, cancellationToken);
if (account == null)
{
throw new LoginException("No users registered with given email.");
}
var hash = Convert.FromBase64String(account.PasswordHash);
var salt = Convert.FromBase64String(account.PasswordSalt);
var password = Encoding.UTF8.GetBytes(request.Password);
var isValidPassword = await _passwordHasher
.IsValidHashAsync(hash, password, salt, cancellationToken);
if (!isValidPassword)
{
throw new LoginException("Given password is incorrect.");
}
var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken);
var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
var refreshToken = (await _unitOfWork.RefreshTokenRepository
.GetPageAsync(
e =>
e.AccountId == account.Id &&
e.RevocationTime == null &&
e.ExpirationTime > DateTimeOffset.UtcNow,
1, int.MaxValue, cancellationToken))
.Items.FirstOrDefault();
if (refreshToken == null)
{
refreshToken = CreateRefreshToken();
refreshToken.AccountId = account.Id;
await _unitOfWork.RefreshTokenRepository
.AddOneAsync(refreshToken, cancellationToken);
}
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return new TokensModel(accessToken, refreshToken.Value);
}
private async Task<JwtSecurityToken> CreateJwtAsync(
Account account, CancellationToken cancellationToken)
{
var roleIds = account.AccountRoles.Select(ar => ar.RoleId);
var roles = (await _unitOfWork.RoleRepository
.GetPageAsync(
r => roleIds.Contains(r.Id),
1, roleIds.Count(), cancellationToken))
.Items.Select(r => r.Value);
var roleClaims = new List<Claim>();
foreach (var role in roles)
{
roleClaims.Add(new Claim("roles", role.Name));
}
var claims = new List<Claim>()
{
new Claim(JwtRegisteredClaimNames.Sub, account.Guid.ToString()),
new Claim(JwtRegisteredClaimNames.Nickname, account.Username),
new Claim(JwtRegisteredClaimNames.Email, account.Email)
}
.Union(roleClaims);
var expirationDateTimeUtc = DateTime.UtcNow.Add(
_jwtConfiguration.AccessTokenValidity);
var symmetricSecurityKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_jwtConfiguration.IssuerSigningKey));
var signingCredentials = new SigningCredentials(
symmetricSecurityKey, SecurityAlgorithms.HmacSha256);
var jwtSecurityToken = new JwtSecurityToken(
issuer: _jwtConfiguration.Issuer,
audience: _jwtConfiguration.Audience,
claims: claims,
expires: expirationDateTimeUtc,
signingCredentials: signingCredentials);
return jwtSecurityToken;
}
private RefreshToken CreateRefreshToken()
{
var token = RandomNumberGenerator.GetBytes(128 / 8);
return new RefreshToken
{
Guid = Guid.NewGuid(),
Value = Convert.ToBase64String(token),
CreationTime = DateTimeOffset.UtcNow,
ExpirationTime = DateTimeOffset.UtcNow.Add(
_jwtConfiguration.RefreshTokenValidity)
};
}
}

View File

@ -1,15 +1,18 @@
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryValidator : AbstractValidator<LoginQuery>
{
public LoginQueryValidator()
public LoginQueryValidator(IStringLocalizer localizer)
{
RuleFor(v => v.Email)
.NotEmpty().WithMessage("Email address is required.");
RuleFor(v => v.EmailOrUsername)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Password)
.NotEmpty().WithMessage("Password is required.");
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,28 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Buses;
public sealed class BusDto : IMapFrom<Bus>
{
public Guid Uuid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<Bus, BusDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.CompanyUuid,
opt => opt.MapFrom(s => s.Company.Guid));
}
}

View File

@ -0,0 +1,14 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
public record AddBusCommand : IRequest<BusDto>
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
public class AddBusCommandAuthorizer :
AbstractRequestAuthorizer<AddBusCommand>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public AddBusCommandAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(AddBusCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var company = _unitOfWork.CompanyRepository
.GetOneAsync(
e => e.Guid == request.CompanyGuid, e => e.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = company?.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,62 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
public class AddBusCommandHandler :
IRequestHandler<AddBusCommand, BusDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddBusCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<BusDto> Handle(
AddBusCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Number == request.Number, cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"Bus with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity = new Bus()
{
Number = request.Number,
Model = request.Model,
Capacity = request.Capacity,
CompanyId = parentEntity.Id,
Company = parentEntity
};
entity = await _unitOfWork.BusRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<BusDto>(entity);
}
}

View File

@ -0,0 +1,41 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
public class AddBusCommandValidator : AbstractValidator<AddBusCommand>
{
public AddBusCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus;
public record DeleteBusCommand : IRequest
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus;
public class DeleteBusCommandAuthorizer :
AbstractRequestAuthorizer<DeleteBusCommand>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public DeleteBusCommandAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(DeleteBusCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var vehicel = _unitOfWork.VehicleRepository
.GetOneAsync(
e => e.Guid == request.Guid, e => e.Company.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = vehicel?.Company.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

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

View File

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

View File

@ -0,0 +1,16 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
public record UpdateBusCommand : IRequest<BusDto>
{
public Guid Guid { get; set; }
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyGuid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
public class UpdateBusCommandAuthorizer :
AbstractRequestAuthorizer<UpdateBusCommand>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public UpdateBusCommandAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(UpdateBusCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var company = _unitOfWork.CompanyRepository
.GetOneAsync(
e => e.Guid == request.CompanyGuid, e => e.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = company?.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,67 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
public class UpdateBusCommandHandler :
IRequestHandler<UpdateBusCommand, BusDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public UpdateBusCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<BusDto> Handle(
UpdateBusCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Guid == request.Guid, cancellationToken);
if (entity == null)
{
throw new NotFoundException();
}
var duplicateEntity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Number == request.Number && e.Guid != request.Guid,
cancellationToken);
if (duplicateEntity != null)
{
throw new DuplicateEntityException(
"Bus with given number already exists.");
}
var parentEntity = await _unitOfWork.CompanyRepository.GetOneAsync(
e => e.Guid == request.CompanyGuid, cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.CompanyGuid} not found.");
}
entity.Number = request.Number;
entity.Model = request.Model;
entity.Capacity = request.Capacity;
entity.CompanyId = parentEntity.Id;
entity.Company = parentEntity;
entity = await _unitOfWork.BusRepository.UpdateOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<BusDto>(entity);
}
}

View File

@ -0,0 +1,45 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
public class UpdateBusCommandValidator : AbstractValidator<UpdateBusCommand>
{
public UpdateBusCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Guid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Number)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(32)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
32));
RuleFor(v => v.Model)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.Capacity)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.CompanyGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

View File

@ -0,0 +1,8 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
public record GetBusQuery : IRequest<BusDto>
{
public Guid Guid { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
public class GetBusQueryAuthorizer :
AbstractRequestAuthorizer<GetBusQuery>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public GetBusQueryAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(GetBusQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var vehicel = _unitOfWork.VehicleRepository
.GetOneAsync(
e => e.Guid == request.Guid, e => e.Company.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = vehicel?.Company.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,39 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
using AutoMapper;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
public class GetBusQueryHandler :
IRequestHandler<GetBusQuery, BusDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetBusQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<BusDto> Handle(
GetBusQuery request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.BusRepository.GetOneAsync(
e => e.Guid == request.Guid, e => e.Company,
cancellationToken);
_unitOfWork.Dispose();
if (entity == null)
{
throw new NotFoundException();
}
return _mapper.Map<BusDto>(entity);
}
}

View File

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

View File

@ -0,0 +1,21 @@
using cuqmbr.TravelGuide.Application.Common.Models;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
public record GetBusesPageQuery : IRequest<PaginatedList<BusDto>>
{
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 Guid? CompanyGuid { get; set; }
public short? CapacityGreaterThanOrEqualTo { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -0,0 +1,42 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
public class GetBusesPageQueryAuthorizer :
AbstractRequestAuthorizer<GetBusesPageQuery>
{
private readonly SessionUserService _sessionUserService;
private readonly UnitOfWork _unitOfWork;
public GetBusesPageQueryAuthorizer(
SessionUserService sessionUserService,
UnitOfWork unitOfWork)
{
_sessionUserService = sessionUserService;
_unitOfWork = unitOfWork;
}
public override void BuildPolicy(GetBusesPageQuery request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated = _sessionUserService.IsAuthenticated
});
var company = _unitOfWork.CompanyRepository
.GetOneAsync(
e => e.Guid == request.CompanyGuid, e => e.Account,
CancellationToken.None)
.Result;
UseRequirement(new MustBeObjectOwnerOrAdminRequirement
{
UserRoles = _sessionUserService.Roles,
RequiredGuid = company?.Account.Guid,
UserGuid = _sessionUserService.Guid
});
}
}

View File

@ -0,0 +1,57 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Models;
using cuqmbr.TravelGuide.Application.Common.Extensions;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
public class GetBusesPageQueryHandler :
IRequestHandler<GetBusesPageQuery, PaginatedList<BusDto>>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public GetBusesPageQueryHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<PaginatedList<BusDto>> Handle(
GetBusesPageQuery request,
CancellationToken cancellationToken)
{
var paginatedList = await _unitOfWork.BusRepository.GetPageAsync(
e =>
(e.Number.ToLower().Contains(request.Search.ToLower()) ||
e.Model.ToLower().Contains(request.Search.ToLower())) &&
(request.CompanyGuid != null
? e.Company.Guid == request.CompanyGuid
: true) &&
(request.CapacityGreaterThanOrEqualTo != null
? e.Capacity >= request.CapacityGreaterThanOrEqualTo
: true) &&
(request.CapacityLessThanOrEqualTo != null
? e.Capacity <= request.CapacityLessThanOrEqualTo
: true),
e => e.Company,
request.PageNumber, request.PageSize,
cancellationToken);
var mappedItems = _mapper
.ProjectTo<BusDto>(paginatedList.Items.AsQueryable());
mappedItems = QueryableExtension<BusDto>
.ApplySort(mappedItems, request.Sort);
_unitOfWork.Dispose();
return new PaginatedList<BusDto>(
mappedItems.ToList(),
paginatedList.TotalCount, request.PageNumber,
request.PageSize);
}
}

View File

@ -0,0 +1,43 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
public class GetBusesPageQueryValidator : AbstractValidator<GetBusesPageQuery>
{
public GetBusesPageQueryValidator(
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

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.Buses.ViewModels;
public sealed class AddBusViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace cuqmbr.TravelGuide.Application.Buses.ViewModels;
public sealed class GetBusesPageFilterViewModel
{
public Guid? CompanyUuid { get; set; }
public short? CapacityGreaterThanOrEqualTo { get; set; }
public short? CapacityLessThanOrEqualTo { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace cuqmbr.TravelGuide.Application.Buses.ViewModels;
public sealed class UpdateBusViewModel
{
public string Number { get; set; }
public string Model { get; set; }
public short Capacity { get; set; }
public Guid CompanyUuid { get; set; }
}

View File

@ -0,0 +1,39 @@
using cuqmbr.TravelGuide.Application.Common.Mappings;
using cuqmbr.TravelGuide.Domain.Entities;
namespace cuqmbr.TravelGuide.Application.Cities;
public sealed class CityDto : IMapFrom<City>
{
public Guid Uuid { get; set; }
public string Name { get; set; }
public Guid CountryUuid { get; set; }
public string CountryName { get; set; }
public Guid RegionUuid { get; set; }
public string RegionName { get; set; }
public void Mapping(MappingProfile profile)
{
profile.CreateMap<City, CityDto>()
.ForMember(
d => d.Uuid,
opt => opt.MapFrom(s => s.Guid))
.ForMember(
d => d.CountryUuid,
opt => opt.MapFrom(s => s.Region.Country.Guid))
.ForMember(
d => d.CountryName,
opt => opt.MapFrom(s => s.Region.Country.Name))
.ForMember(
d => d.RegionUuid,
opt => opt.MapFrom(s => s.Region.Guid))
.ForMember(
d => d.RegionName,
opt => opt.MapFrom(s => s.Region.Name));
}
}

View File

@ -0,0 +1,10 @@
using MediatR;
namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity;
public record AddCityCommand : IRequest<CityDto>
{
public string Name { get; set; }
public Guid RegionGuid { 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.Cities.Commands.AddCity;
public class AddCityCommandAuthorizer :
AbstractRequestAuthorizer<AddCityCommand>
{
private readonly SessionUserService _sessionUserService;
public AddCityCommandAuthorizer(SessionUserService sessionUserService)
{
_sessionUserService = sessionUserService;
}
public override void BuildPolicy(AddCityCommand request)
{
UseRequirement(new MustBeAuthenticatedRequirement
{
IsAuthenticated= _sessionUserService.IsAuthenticated
});
UseRequirement(new MustBeInAnyOfRolesRequirement
{
RequiredRoles = [IdentityRole.Administrator],
UserRoles = _sessionUserService.Roles
});
}
}

View File

@ -0,0 +1,61 @@
using MediatR;
using cuqmbr.TravelGuide.Application.Common.Persistence;
using cuqmbr.TravelGuide.Domain.Entities;
using AutoMapper;
using cuqmbr.TravelGuide.Application.Common.Exceptions;
namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity;
public class AddCityCommandHandler :
IRequestHandler<AddCityCommand, CityDto>
{
private readonly UnitOfWork _unitOfWork;
private readonly IMapper _mapper;
public AddCityCommandHandler(
UnitOfWork unitOfWork,
IMapper mapper)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
}
public async Task<CityDto> Handle(
AddCityCommand request,
CancellationToken cancellationToken)
{
var entity = await _unitOfWork.CityRepository.GetOneAsync(
e => e.Name == request.Name && e.Region.Guid == request.RegionGuid,
cancellationToken);
if (entity != null)
{
throw new DuplicateEntityException(
"City with given name already exists.");
}
var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync(
e => e.Guid == request.RegionGuid, e => e.Country,
cancellationToken);
if (parentEntity == null)
{
throw new NotFoundException(
$"Parent entity with Guid: {request.RegionGuid} not found.");
}
entity = new City()
{
Name = request.Name,
RegionId = parentEntity.Id
};
entity = await _unitOfWork.CityRepository.AddOneAsync(
entity, cancellationToken);
await _unitOfWork.SaveAsync(cancellationToken);
_unitOfWork.Dispose();
return _mapper.Map<CityDto>(entity);
}
}

View File

@ -0,0 +1,27 @@
using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity;
public class AddCityCommandValidator : AbstractValidator<AddCityCommand>
{
public AddCityCommandValidator(
IStringLocalizer localizer,
SessionCultureService cultureService)
{
RuleFor(v => v.Name)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"])
.MaximumLength(64)
.WithMessage(
String.Format(
cultureService.Culture,
localizer["FluentValidation.MaximumLength"],
64));
RuleFor(v => v.RegionGuid)
.NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}

Some files were not shown because too many files have changed in this diff Show More