Compare commits

...

36 Commits

Author SHA1 Message Date
89420ce1ee 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
2025-06-04 09:16:21 +00:00
0508c89c2d
add authorization requirements
All checks were successful
/ build (push) Successful in 7m55s
/ tests (push) Successful in 40s
/ build-docker (push) Successful in 8m27s
2025-06-03 18:00:07 +03:00
120963f3cc
add optional ticket group binding to account
All checks were successful
/ tests (push) Successful in 26s
/ build (push) Successful in 10m28s
/ build-docker (push) Successful in 5m49s
2025-05-30 16:40:28 +03:00
4d1f6edc2e
add ticket group queries 2025-05-30 13:14:20 +03:00
e5b3180220
update uk-UA localization 2025-05-29 18:02:26 +03:00
6a9504d6ff
add payment email notifications 2025-05-29 18:02:02 +03:00
68a9e06eeb
add email sender service 2025-05-29 13:13:41 +03:00
41158b34c5
flatten configuration file structure 2025-05-29 12:24:13 +03:00
9ccd0bb68d
add account creation when adding a company 2025-05-29 11:56:34 +03:00
bb309d7c20
add account creation when adding an employee 2025-05-28 17:55:32 +03:00
7229a10ad5
add account management 2025-05-28 15:40:30 +03:00
2d7d23d26b
update ci pipeline to reflect Identity project removeal
All checks were successful
/ build (push) Successful in 6m7s
/ tests (push) Successful in 27s
/ build-docker (push) Successful in 5m11s
2025-05-28 12:35:04 +03:00
fafb665cd2
rewrite identity system 2025-05-28 12:33:49 +03:00
4ae17c5a91
add vehicle enrollment employee management 2025-05-27 18:10:53 +03:00
57264b384c
add vehicle and company info to vehicle enrollment search dto 2025-05-27 14:08:40 +03:00
a97b95a704
remove unnecessary todos 2025-05-27 12:58:14 +03:00
91805bc9ad
fix: pass cancellationToken to library methods that accept them
All checks were successful
/ build (push) Successful in 4m1s
/ tests (push) Successful in 26s
/ build-docker (push) Successful in 4m15s
2025-05-26 12:36:48 +03:00
afe626bd78
add LiqPay integration for ticket purchase
All checks were successful
/ build (push) Successful in 5m8s
/ tests (push) Successful in 33s
/ build-docker (push) Successful in 4m15s
hosted services deletes ticket groups with reserved status that were created more than 10 minutes ago

payment link expires in 10 minutes from the time it was created
2025-05-26 12:16:46 +03:00
e3dd2dd582
add all vehicle enrollment with transfers search
All checks were successful
/ build (push) Successful in 9m40s
/ tests (push) Successful in 1m29s
/ build-docker (push) Successful in 10m30s
2025-05-24 11:17:43 +03:00
d5ffedbdb9
add shortest vehicle enrollment with transfers search
All checks were successful
/ build (push) Successful in 10m38s
/ tests (push) Successful in 1m14s
/ build-docker (push) Successful in 9m39s
2025-05-23 14:19:47 +03:00
6830fea563
add ticket group creation 2025-05-20 20:39:09 +03:00
5982fa7285
add employee management 2025-05-16 15:22:44 +03:00
f4611f029f
add companies management 2025-05-15 19:18:52 +03:00
74dc7ceff3
rename user setings services 2025-05-14 17:48:48 +03:00
b1aceac750
add currency converter service and integrated it with vehicle enrollment management 2025-05-14 17:43:10 +03:00
5ee8c9c5df
add vehicle enrollments management 2025-05-11 10:51:19 +03:00
3ebd0c3a2c
add vehicles hierarchy management
All checks were successful
/ build (push) Successful in 6m13s
/ tests (push) Successful in 47s
/ build-docker (push) Successful in 6m48s
2025-05-03 10:09:52 +03:00
201e6e7dfc
remove duplicate indices on pk and altk 2025-05-02 21:38:15 +03:00
09f3a46edc
add more filter options for GetAddressesPageQuery
All checks were successful
/ build (push) Successful in 6m39s
/ tests (push) Successful in 38s
/ build-docker (push) Successful in 5m9s
2025-05-01 21:04:02 +03:00
bd87ab9133
fix VehicleType type convertation issue in InMemory datastore
without the fix tests would fail
2025-05-01 20:54:59 +03:00
fdf147fe83
add route entity management 2025-05-01 20:50:22 +03:00
d500d1f84c
add address entity management 2025-05-01 11:48:43 +03:00
e70c807b7c
update Region DTO to include country name 2025-04-30 17:44:29 +03:00
0345f58f7b
add city entity management 2025-04-30 17:29:40 +03:00
16457fc2cc
change default listen endpoint to 0.0.0.0:8080 2025-04-30 13:41:36 +03:00
a02d750c8b
complete region integration tests 2025-04-30 12:49:38 +03:00
628 changed files with 39260 additions and 4768 deletions

View File

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

View File

@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infra
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpApi", "src\HttpApi\HttpApi.csproj", "{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpApi", "src\HttpApi\HttpApi.csproj", "{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}"
EndProject 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}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configuration", "src\Configuration\Configuration.csproj", "{1DCFA4EE-A545-42FE-A3BC-A606D2961298}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.IntegrationTests", "tst\Application.IntegrationTests\Application.IntegrationTests.csproj", "{B52B8651-10B8-488D-8ACF-9C4499F8A723}" 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}.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.ActiveCfg = Release|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.Build.0 = 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 {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="FluentValidation" Version="11.11.0" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MediatR.Behaviors.Authorization" Version="12.2.0" /> <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.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" /> <PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.2" />
</ItemGroup> </ItemGroup>

View File

@ -4,6 +4,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public record RegisterCommand : IRequest public record RegisterCommand : IRequest
{ {
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,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; using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandHandler : IRequestHandler<RegisterCommand> 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( public async Task Handle(RegisterCommand request,
RegisterCommand request, CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await _authenticationService.RegisterAsync( var datastoreAccount = await _unitOfWork.AccountRepository
request.Email, request.Password, cancellationToken); .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 FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandValidator : AbstractValidator<RegisterCommand> 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) RuleFor(v => v.Email)
.NotEmpty() .NotEmpty()
.WithMessage("Email address is required.") .WithMessage(localizer["FluentValidation.NotEmpty"])
.Matches(@"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b") .IsEmail()
.WithMessage("Email address is invalid."); .WithMessage(localizer["FluentValidation.IsEmail"]);
RuleFor(v => v.Password) RuleFor(v => v.Password)
.NotEmpty() .NotEmpty()
.WithMessage("Password is required.") .WithMessage(localizer["FluentValidation.NotEmpty"])
.MinimumLength(8) .MinimumLength(8)
.WithMessage("Password must be at least 8 characters long.") .WithMessage(
.MaximumLength(64) String.Format(
.WithMessage("Password must be at most 64 characters long.") cultureService.Culture,
.Matches(@"(?=.*[A-Z]).*") localizer["FluentValidation.MinimumLength"],
.WithMessage("Password must contain at least one uppercase letter.") 8))
.Matches(@"(?=.*[a-z]).*") .MaximumLength(256)
.WithMessage("Password must contain at least one lowercase letter.") .WithMessage(
.Matches(@"(?=.*[\d]).*") String.Format(
.WithMessage("Password must contain at least one digit.") cultureService.Culture,
.Matches(@"(?=.*[!@#$%^&*()]).*") localizer["FluentValidation.MaximumLength"],
.WithMessage("Password must contain at least one of the following special charactters: !@#$%^&*()."); 256));
} }
} }

View File

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

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 MediatR;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
public class RenewAccessTokenCommandHandler : public class RenewAccessTokenCommandHandler :
IRequestHandler<RenewAccessTokenCommand, TokensModel> 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( public async Task<TokensModel> Handle(
RenewAccessTokenCommand request, CancellationToken cancellationToken) RenewAccessTokenCommand request, CancellationToken cancellationToken)
{ {
return await _authenticationService.RenewAccessTokenAsync( var refreshToken = (await _unitOfWork.RefreshTokenRepository
request.RefreshToken, cancellationToken); .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.Authorization;
using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization; using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; 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; using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
@ -6,17 +7,36 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshTok
public class RevokeRefreshTokenCommandHandler : public class RevokeRefreshTokenCommandHandler :
IRequestHandler<RevokeRefreshTokenCommand> 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( public async Task Handle(
RevokeRefreshTokenCommand request, CancellationToken cancellationToken) RevokeRefreshTokenCommand request, CancellationToken cancellationToken)
{ {
await _authenticationService.RevokeRefreshTokenAsync( var refreshToken = (await _unitOfWork.RefreshTokenRepository
request.RefreshToken, cancellationToken); .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 record LoginQuery : IRequest<TokensModel>
{ {
public string Email { get; set; } public string EmailOrUsername { get; set; }
public string Password { 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 MediatR;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryHandler : IRequestHandler<LoginQuery, TokensModel> 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( public async Task<TokensModel> Handle(
LoginQuery request, CancellationToken cancellationToken) LoginQuery request, CancellationToken cancellationToken)
{ {
return await _authenticationService.LoginAsync( var account = await _unitOfWork.AccountRepository
request.Email, request.Password, cancellationToken); .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 FluentValidation;
using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryValidator : AbstractValidator<LoginQuery> public class LoginQueryValidator : AbstractValidator<LoginQuery>
{ {
public LoginQueryValidator() public LoginQueryValidator(IStringLocalizer localizer)
{ {
RuleFor(v => v.Email) RuleFor(v => v.EmailOrUsername)
.NotEmpty().WithMessage("Email address is required."); .NotEmpty()
.WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Password) 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