diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 56207bd..21dede3 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -25,7 +25,6 @@ jobs:
cache: true
cache-dependency-path: |
src/Application/packages.lock.json
- src/Identity/packages.lock.json
src/Infrastructure/packages.lock.json
src/Persistence/packages.lock.json
src/Configuration/packages.lock.json
diff --git a/TravelGuide.sln b/TravelGuide.sln
index f012d1c..75d3e9c 100644
--- a/TravelGuide.sln
+++ b/TravelGuide.sln
@@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infra
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpApi", "src\HttpApi\HttpApi.csproj", "{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Identity", "src\Identity\Identity.csproj", "{AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Configuration", "src\Configuration\Configuration.csproj", "{1DCFA4EE-A545-42FE-A3BC-A606D2961298}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application.IntegrationTests", "tst\Application.IntegrationTests\Application.IntegrationTests.csproj", "{B52B8651-10B8-488D-8ACF-9C4499F8A723}"
@@ -48,10 +46,6 @@ Global
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.Build.0 = Release|Any CPU
- {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AD3CC01F-0331-44DC-B58E-CCE6ADCB56B6}.Release|Any CPU.Build.0 = Release|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/src/Application/Addresses/AddressDto.cs b/src/Application/Addresses/AddressDto.cs
new file mode 100644
index 0000000..5d936bf
--- /dev/null
+++ b/src/Application/Addresses/AddressDto.cs
@@ -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
+{
+ 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()
+ .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));
+ }
+}
diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs
new file mode 100644
index 0000000..2b1a2f5
--- /dev/null
+++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommand.cs
@@ -0,0 +1,17 @@
+using cuqmbr.TravelGuide.Domain.Enums;
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Addresses.Commands.AddAddress;
+
+public record AddAddressCommand : IRequest
+{
+ 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; }
+}
diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs
new file mode 100644
index 0000000..b4bbe78
--- /dev/null
+++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs
new file mode 100644
index 0000000..189c8d0
--- /dev/null
+++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public AddAddressCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs
new file mode 100644
index 0000000..e1c38c8
--- /dev/null
+++ b/src/Application/Addresses/Commands/AddAddress/AddAddressCommandValidator.cs
@@ -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
+{
+ 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"]);
+ }
+}
diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs
new file mode 100644
index 0000000..87241a4
--- /dev/null
+++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress;
+
+public record DeleteAddressCommand : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs
new file mode 100644
index 0000000..66a9511
--- /dev/null
+++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs
new file mode 100644
index 0000000..fc6ea4c
--- /dev/null
+++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandHandler.cs
@@ -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
+{
+ 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();
+ }
+}
diff --git a/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs
new file mode 100644
index 0000000..10cec3c
--- /dev/null
+++ b/src/Application/Addresses/Commands/DeleteAddress/DeleteAddressCommandValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Addresses.Commands.DeleteAddress;
+
+public class DeleteAddressCommandValidator : AbstractValidator
+{
+ public DeleteAddressCommandValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs
new file mode 100644
index 0000000..b44f8cf
--- /dev/null
+++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommand.cs
@@ -0,0 +1,19 @@
+using MediatR;
+using cuqmbr.TravelGuide.Domain.Enums;
+
+namespace cuqmbr.TravelGuide.Application.Addresses.Commands.UpdateAddress;
+
+public record UpdateAddressCommand : IRequest
+{
+ 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; }
+}
diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs
new file mode 100644
index 0000000..055fd69
--- /dev/null
+++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs
new file mode 100644
index 0000000..17ab45f
--- /dev/null
+++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public UpdateAddressCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs
new file mode 100644
index 0000000..adf0322
--- /dev/null
+++ b/src/Application/Addresses/Commands/UpdateAddress/UpdateAddressCommandValidator.cs
@@ -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
+{
+ 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"]);
+ }
+}
diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs
new file mode 100644
index 0000000..fa665c4
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQuery.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress;
+
+public record GetAddressQuery : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs
new file mode 100644
index 0000000..9bb9784
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs
new file mode 100644
index 0000000..730b248
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetAddressQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs
new file mode 100644
index 0000000..5e97a2e
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddress/GetAddressQueryValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Addresses.Queries.GetAddress;
+
+public class GetAddressQueryValidator : AbstractValidator
+{
+ public GetAddressQueryValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs
new file mode 100644
index 0000000..36ed29a
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQuery.cs
@@ -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>
+{
+ 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; }
+}
diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs
new file mode 100644
index 0000000..c868ec9
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs
new file mode 100644
index 0000000..4fcce04
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryHandler.cs
@@ -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>
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetAddressesPageQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task> 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(paginatedList.Items.AsQueryable());
+
+ mappedItems = QueryableExtension
+ .ApplySort(mappedItems, request.Sort);
+
+ _unitOfWork.Dispose();
+
+ return new PaginatedList(
+ mappedItems.ToList(),
+ paginatedList.TotalCount, request.PageNumber,
+ request.PageSize);
+ }
+}
diff --git a/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs
new file mode 100644
index 0000000..ce7adfe
--- /dev/null
+++ b/src/Application/Addresses/Queries/GetAddressesPage/GetAddressesPageQueryValidator.cs
@@ -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
+{
+ 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));
+ }
+}
diff --git a/src/Application/Addresses/ViewModels/AddAddressViewModel.cs b/src/Application/Addresses/ViewModels/AddAddressViewModel.cs
new file mode 100644
index 0000000..904431c
--- /dev/null
+++ b/src/Application/Addresses/ViewModels/AddAddressViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs
new file mode 100644
index 0000000..9dc7a20
--- /dev/null
+++ b/src/Application/Addresses/ViewModels/GetAddressesPageFilterViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs b/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs
new file mode 100644
index 0000000..6bcb4e4
--- /dev/null
+++ b/src/Application/Addresses/ViewModels/UpdateAddressViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Aircrafts/AircraftDto.cs b/src/Application/Aircrafts/AircraftDto.cs
new file mode 100644
index 0000000..58c8866
--- /dev/null
+++ b/src/Application/Aircrafts/AircraftDto.cs
@@ -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
+{
+ 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()
+ .ForMember(
+ d => d.Uuid,
+ opt => opt.MapFrom(s => s.Guid))
+ .ForMember(
+ d => d.CompanyUuid,
+ opt => opt.MapFrom(s => s.Company.Guid));
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs
new file mode 100644
index 0000000..3e3a9ff
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.AddAircraft;
+
+public record AddAircraftCommand : IRequest
+{
+ public string Number { get; set; }
+
+ public string Model { get; set; }
+
+ public short Capacity { get; set; }
+
+ public Guid CompanyGuid { get; set; }
+}
diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs
new file mode 100644
index 0000000..69c650f
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs
new file mode 100644
index 0000000..fdb46dd
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public AddAircraftCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs
new file mode 100644
index 0000000..15253f2
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/AddAircraft/AddAircraftCommandValidator.cs
@@ -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
+{
+ 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"]);
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs
new file mode 100644
index 0000000..88bd0c7
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft;
+
+public record DeleteAircraftCommand : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs
new file mode 100644
index 0000000..295c08b
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs
new file mode 100644
index 0000000..1d3d867
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandHandler.cs
@@ -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
+{
+ 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();
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs
new file mode 100644
index 0000000..8d43bfc
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/DeleteAircraft/DeleteAircraftCommandValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.DeleteAircraft;
+
+public class DeleteAircraftCommandValidator : AbstractValidator
+{
+ public DeleteAircraftCommandValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs
new file mode 100644
index 0000000..0de5717
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommand.cs
@@ -0,0 +1,16 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Aircrafts.Commands.UpdateAircraft;
+
+public record UpdateAircraftCommand : IRequest
+{
+ 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; }
+}
diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs
new file mode 100644
index 0000000..a23a8bf
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs
new file mode 100644
index 0000000..9d16e38
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public UpdateAircraftCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs
new file mode 100644
index 0000000..8060418
--- /dev/null
+++ b/src/Application/Aircrafts/Commands/UpdateAircraft/UpdateAircraftCommandValidator.cs
@@ -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
+{
+ 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"]);
+ }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs
new file mode 100644
index 0000000..60fb5f1
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQuery.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
+
+public record GetAircraftQuery : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs
new file mode 100644
index 0000000..aac2bd3
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs
new file mode 100644
index 0000000..2977272
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetAircraftQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs
new file mode 100644
index 0000000..c5a5e09
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraft/GetAircraftQueryValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraft;
+
+public class GetAircraftQueryValidator : AbstractValidator
+{
+ public GetAircraftQueryValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs
new file mode 100644
index 0000000..655a576
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQuery.cs
@@ -0,0 +1,21 @@
+using cuqmbr.TravelGuide.Application.Common.Models;
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Aircrafts.Queries.GetAircraftsPage;
+
+public record GetAircraftsPageQuery : IRequest>
+{
+ 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; }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs
new file mode 100644
index 0000000..2871ed3
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs
new file mode 100644
index 0000000..586d197
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryHandler.cs
@@ -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>
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetAircraftsPageQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task> 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(paginatedList.Items.AsQueryable());
+
+ mappedItems = QueryableExtension
+ .ApplySort(mappedItems, request.Sort);
+
+ _unitOfWork.Dispose();
+
+ return new PaginatedList(
+ mappedItems.ToList(),
+ paginatedList.TotalCount, request.PageNumber,
+ request.PageSize);
+ }
+}
diff --git a/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs
new file mode 100644
index 0000000..c564c40
--- /dev/null
+++ b/src/Application/Aircrafts/Queries/GetAircraftsPage/GetAircraftsPageQueryValidator.cs
@@ -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
+{
+ 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));
+ }
+}
diff --git a/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs
new file mode 100644
index 0000000..57b64e0
--- /dev/null
+++ b/src/Application/Aircrafts/ViewModels/AddAircraftViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs
new file mode 100644
index 0000000..51994b5
--- /dev/null
+++ b/src/Application/Aircrafts/ViewModels/GetAircraftsPageFilterViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs
new file mode 100644
index 0000000..52e9374
--- /dev/null
+++ b/src/Application/Aircrafts/ViewModels/UpdateAircraftViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj
index e77ed7a..e6484da 100644
--- a/src/Application/Application.csproj
+++ b/src/Application/Application.csproj
@@ -16,7 +16,12 @@
+
+
+
+
+
diff --git a/src/Application/Authentication/Commands/Register/RegisterCommand.cs b/src/Application/Authentication/Commands/Register/RegisterCommand.cs
index 51edec4..41a1dbc 100644
--- a/src/Application/Authentication/Commands/Register/RegisterCommand.cs
+++ b/src/Application/Authentication/Commands/Register/RegisterCommand.cs
@@ -4,6 +4,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public record RegisterCommand : IRequest
{
+ public string Username { get; set; }
+
public string Email { get; set; }
public string Password { get; set; }
diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs b/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs
new file mode 100644
index 0000000..d3fc40f
--- /dev/null
+++ b/src/Application/Authentication/Commands/Register/RegisterCommandAuthorizer.cs
@@ -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
+{
+ public override void BuildPolicy(RegisterCommand request)
+ {
+ UseRequirement(new AllowAllRequirement());
+ }
+}
diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs
index 99ca0f1..f84b3e5 100644
--- a/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs
+++ b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs
@@ -1,21 +1,83 @@
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
+using System.Security.Cryptography;
+using System.Text;
+using cuqmbr.TravelGuide.Application.Common.Exceptions;
+using cuqmbr.TravelGuide.Application.Common.Persistence;
+using cuqmbr.TravelGuide.Application.Common.Services;
+using cuqmbr.TravelGuide.Domain.Entities;
+using cuqmbr.TravelGuide.Domain.Enums;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandHandler : IRequestHandler
{
- private readonly AuthenticationService _authenticationService;
+ private readonly IReadOnlyCollection DefaultRoles =
+ new IdentityRole[] { IdentityRole.User };
- public RegisterCommandHandler(AuthenticationService authenticationService)
+ private readonly UnitOfWork _unitOfWork;
+ private readonly PasswordHasherService _passwordHasher;
+
+ public RegisterCommandHandler(UnitOfWork unitOfWork,
+ PasswordHasherService passwordHasher)
{
- _authenticationService = authenticationService;
+ _unitOfWork = unitOfWork;
+ _passwordHasher = passwordHasher;
}
- public async Task Handle(
- RegisterCommand request, CancellationToken cancellationToken)
+ public async Task Handle(RegisterCommand request,
+ CancellationToken cancellationToken)
{
- await _authenticationService.RegisterAsync(
- request.Email, request.Password, cancellationToken);
+ var datastoreAccount = await _unitOfWork.AccountRepository
+ .GetOneAsync(
+ e =>
+ e.Email == request.Email ||
+ e.Username == request.Username,
+ cancellationToken);
+
+ if (datastoreAccount != null)
+ {
+ throw new RegistrationException(
+ "User with given email or username already registered.");
+ }
+
+
+ var defaultRoleIds = (await _unitOfWork.RoleRepository
+ .GetPageAsync(
+ r => DefaultRoles.Contains(r.Value),
+ 1, DefaultRoles.Count, cancellationToken))
+ .Items
+ .Select(r => r.Id);
+
+
+ var password = Encoding.UTF8.GetBytes(request.Password);
+
+ var salt = RandomNumberGenerator.GetBytes(128 / 8);
+ var hash = await _passwordHasher
+ .HashAsync(password, salt, cancellationToken);
+
+ var saltBase64 = Convert.ToBase64String(salt);
+ var hashBase64 = Convert.ToBase64String(hash);
+
+
+ var newAccount = new Account
+ {
+ Username = request.Username,
+ Email = request.Email,
+ PasswordHash = hashBase64,
+ PasswordSalt = saltBase64,
+ AccountRoles = defaultRoleIds.Select(id =>
+ new AccountRole()
+ {
+ RoleId = id
+ })
+ .ToArray()
+ };
+
+
+ await _unitOfWork.AccountRepository
+ .AddOneAsync(newAccount, cancellationToken);
+
+ await _unitOfWork.SaveAsync(cancellationToken);
+ _unitOfWork.Dispose();
}
}
diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs
index 13afc23..f009284 100644
--- a/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs
+++ b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs
@@ -1,31 +1,54 @@
+using cuqmbr.TravelGuide.Application.Common.FluentValidation;
+using cuqmbr.TravelGuide.Application.Common.Services;
using FluentValidation;
+using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register;
public class RegisterCommandValidator : AbstractValidator
{
- public RegisterCommandValidator()
+ public RegisterCommandValidator(
+ IStringLocalizer localizer,
+ SessionCultureService cultureService)
{
+ RuleFor(v => v.Username)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"])
+ .MinimumLength(1)
+ .WithMessage(
+ String.Format(
+ cultureService.Culture,
+ localizer["FluentValidation.MinimumLength"],
+ 1))
+ .MaximumLength(32)
+ .WithMessage(
+ String.Format(
+ cultureService.Culture,
+ localizer["FluentValidation.MaximumLength"],
+ 32))
+ .IsUsername()
+ .WithMessage(localizer["FluentValidation.IsUsername"]);
+
RuleFor(v => v.Email)
.NotEmpty()
- .WithMessage("Email address is required.")
- .Matches(@"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b")
- .WithMessage("Email address is invalid.");
+ .WithMessage(localizer["FluentValidation.NotEmpty"])
+ .IsEmail()
+ .WithMessage(localizer["FluentValidation.IsEmail"]);
RuleFor(v => v.Password)
.NotEmpty()
- .WithMessage("Password is required.")
+ .WithMessage(localizer["FluentValidation.NotEmpty"])
.MinimumLength(8)
- .WithMessage("Password must be at least 8 characters long.")
- .MaximumLength(64)
- .WithMessage("Password must be at most 64 characters long.")
- .Matches(@"(?=.*[A-Z]).*")
- .WithMessage("Password must contain at least one uppercase letter.")
- .Matches(@"(?=.*[a-z]).*")
- .WithMessage("Password must contain at least one lowercase letter.")
- .Matches(@"(?=.*[\d]).*")
- .WithMessage("Password must contain at least one digit.")
- .Matches(@"(?=.*[!@#$%^&*()]).*")
- .WithMessage("Password must contain at least one of the following special charactters: !@#$%^&*().");
+ .WithMessage(
+ String.Format(
+ cultureService.Culture,
+ localizer["FluentValidation.MinimumLength"],
+ 8))
+ .MaximumLength(256)
+ .WithMessage(
+ String.Format(
+ cultureService.Culture,
+ localizer["FluentValidation.MaximumLength"],
+ 256));
}
}
diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs
index e5237e1..96f0a24 100644
--- a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs
+++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs
@@ -1,5 +1,4 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
@@ -7,18 +6,8 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken
public class RenewAccessTokenCommandAuthorizer :
AbstractRequestAuthorizer
{
- private readonly SessionUserService _sessionUserService;
-
- public RenewAccessTokenCommandAuthorizer(SessionUserService currentUserService)
- {
- _sessionUserService = currentUserService;
- }
-
public override void BuildPolicy(RenewAccessTokenCommand request)
{
- UseRequirement(new MustBeAuthenticatedRequirement
- {
- IsAuthenticated = _sessionUserService.IsAuthenticated
- });
+ UseRequirement(new AllowAllRequirement());
}
}
diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs
index 0cb9016..b6f47c4 100644
--- a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs
+++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs
@@ -1,22 +1,95 @@
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using cuqmbr.TravelGuide.Application.Common.Exceptions;
+using cuqmbr.TravelGuide.Application.Common.Persistence;
+using cuqmbr.TravelGuide.Domain.Entities;
using MediatR;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken;
public class RenewAccessTokenCommandHandler :
IRequestHandler
{
- private readonly AuthenticationService _authenticationService;
+ private readonly UnitOfWork _unitOfWork;
+ private readonly JsonWebTokenConfigurationOptions _jwtConfiguration;
- public RenewAccessTokenCommandHandler(AuthenticationService authenticationService)
+ public RenewAccessTokenCommandHandler(UnitOfWork unitOfWork,
+ IOptions configurationOptions)
{
- _authenticationService = authenticationService;
+ _unitOfWork = unitOfWork;
+ _jwtConfiguration = configurationOptions.Value.JsonWebToken;
}
public async Task Handle(
RenewAccessTokenCommand request, CancellationToken cancellationToken)
{
- return await _authenticationService.RenewAccessTokenAsync(
- request.RefreshToken, cancellationToken);
+ var refreshToken = (await _unitOfWork.RefreshTokenRepository
+ .GetOneAsync(e => e.Value == request.RefreshToken,
+ cancellationToken));
+
+ if (refreshToken == null)
+ {
+ throw new AuthenticationException($"Refresh token was not found.");
+ }
+
+ if (!refreshToken.IsActive)
+ {
+ throw new AuthenticationException("Refresh token is inactive.");
+ }
+
+ var account = await _unitOfWork.AccountRepository.GetOneAsync(
+ a => a.RefreshTokens.Contains(refreshToken),
+ a => a.AccountRoles, cancellationToken);
+
+ var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken);
+ var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
+
+ return new TokensModel(accessToken, refreshToken.Value);
+ }
+
+ private async Task 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();
+ foreach (var role in roles)
+ {
+ roleClaims.Add(new Claim("roles", role.Name));
+ }
+
+ var claims = new List()
+ {
+ 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;
}
}
diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs
deleted file mode 100644
index 93f2031..0000000
--- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using MediatR;
-
-namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
- .RenewAccessTokenWithCookie;
-
-public record RenewAccessTokenWithCookieCommand : IRequest { }
diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs
deleted file mode 100644
index 481c916..0000000
--- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs
+++ /dev/null
@@ -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
-{
- private readonly SessionUserService _sessionUserService;
-
- public RenewAccessTokenWithCookieCommandAuthorizer(
- SessionUserService currentUserService)
- {
- _sessionUserService = currentUserService;
- }
-
- public override void BuildPolicy(RenewAccessTokenWithCookieCommand request)
- {
- UseRequirement(new MustBeAuthenticatedRequirement
- {
- IsAuthenticated = _sessionUserService.IsAuthenticated
- });
- }
-}
diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs
deleted file mode 100644
index 797b289..0000000
--- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
-using MediatR;
-
-namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
- .RenewAccessTokenWithCookie;
-
-public class RenewAccessTokenWithCookieCommandHandler :
- IRequestHandler
-{
- private readonly AuthenticationService _authenticationService;
- private readonly SessionUserService _sessionUserService;
-
- public RenewAccessTokenWithCookieCommandHandler(
- AuthenticationService authenticationService,
- SessionUserService sessionUserService)
- {
- _authenticationService = authenticationService;
- _sessionUserService = sessionUserService;
- }
-
- public async Task Handle(
- RenewAccessTokenWithCookieCommand request,
- CancellationToken cancellationToken)
- {
- return await _authenticationService.RenewAccessTokenAsync(
- _sessionUserService.RefreshToken, cancellationToken);
- }
-}
diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs
deleted file mode 100644
index a49e1ef..0000000
--- a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using FluentValidation;
-
-namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
- .RenewAccessTokenWithCookie;
-
-public class RenewAccessTokenWithCookieCommandValidator :
- AbstractValidator
-{
- public RenewAccessTokenWithCookieCommandValidator() { }
-}
diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs
index d298795..6ee840d 100644
--- a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs
+++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs
@@ -1,5 +1,5 @@
using cuqmbr.TravelGuide.Application.Common.Authorization;
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
+using cuqmbr.TravelGuide.Application.Common.Services;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs
index 1c0f322..2e9dee5 100644
--- a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs
+++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs
@@ -1,4 +1,5 @@
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
+using cuqmbr.TravelGuide.Application.Common.Exceptions;
+using cuqmbr.TravelGuide.Application.Common.Persistence;
using MediatR;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken;
@@ -6,17 +7,36 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshTok
public class RevokeRefreshTokenCommandHandler :
IRequestHandler
{
- private readonly AuthenticationService _authenticationService;
+ private readonly UnitOfWork _unitOfWork;
- public RevokeRefreshTokenCommandHandler(AuthenticationService authenticationService)
+ public RevokeRefreshTokenCommandHandler(UnitOfWork unitOfWork)
{
- _authenticationService = authenticationService;
+ _unitOfWork = unitOfWork;
}
public async Task Handle(
RevokeRefreshTokenCommand request, CancellationToken cancellationToken)
{
- await _authenticationService.RevokeRefreshTokenAsync(
- request.RefreshToken, cancellationToken);
+ var refreshToken = (await _unitOfWork.RefreshTokenRepository
+ .GetOneAsync(e => e.Value == request.RefreshToken,
+ cancellationToken));
+
+ if (refreshToken == null)
+ {
+ throw new AuthenticationException("Invalid refreshToken");
+ }
+
+ if (!refreshToken.IsActive)
+ {
+ throw new AuthenticationException("RefreshToken already revoked");
+ }
+
+ refreshToken.RevocationTime = DateTimeOffset.UtcNow;
+
+ await _unitOfWork.RefreshTokenRepository
+ .UpdateOneAsync(refreshToken, cancellationToken);
+
+ await _unitOfWork.SaveAsync(cancellationToken);
+ _unitOfWork.Dispose();
}
}
diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs
deleted file mode 100644
index f277cc8..0000000
--- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-using MediatR;
-
-namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
- .RevokeRefreshTokenWithCookie;
-
-public record RevokeRefreshTokenWithCookieCommand : IRequest { }
diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs
deleted file mode 100644
index eb2f8e2..0000000
--- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs
+++ /dev/null
@@ -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
-{
- private readonly SessionUserService _sessionUserService;
-
- public RevokeRefreshTokenWithCookieCommandAuthorizer(
- SessionUserService currentUserService)
- {
- _sessionUserService = currentUserService;
- }
-
- public override void BuildPolicy(RevokeRefreshTokenWithCookieCommand request)
- {
- UseRequirement(new MustBeAuthenticatedRequirement
- {
- IsAuthenticated = _sessionUserService.IsAuthenticated
- });
- }
-}
diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs
deleted file mode 100644
index 27a8339..0000000
--- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
-using MediatR;
-
-namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
- .RevokeRefreshTokenWithCookie;
-
-public class RevokeRefreshTokenWithCookieCommandHandler :
- IRequestHandler
-{
- 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);
- }
-}
diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs
deleted file mode 100644
index c378206..0000000
--- a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using FluentValidation;
-
-namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands
- .RevokeRefreshTokenWithCookie;
-
-public class RevokeRefreshTokenWithCookieCommandValidator :
- AbstractValidator
-{
- public RevokeRefreshTokenWithCookieCommandValidator() { }
-}
diff --git a/src/Application/Authentication/Queries/Login/LoginQuery.cs b/src/Application/Authentication/Queries/Login/LoginQuery.cs
index 905347c..3a2c4dd 100644
--- a/src/Application/Authentication/Queries/Login/LoginQuery.cs
+++ b/src/Application/Authentication/Queries/Login/LoginQuery.cs
@@ -4,7 +4,7 @@ namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public record LoginQuery : IRequest
{
- public string Email { get; set; }
+ public string EmailOrUsername { get; set; }
public string Password { get; set; }
}
diff --git a/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs b/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs
new file mode 100644
index 0000000..eb5ab38
--- /dev/null
+++ b/src/Application/Authentication/Queries/Login/LoginQueryAuthorizer.cs
@@ -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
+{
+ public override void BuildPolicy(LoginQuery request)
+ {
+ UseRequirement(new AllowAllRequirement());
+ }
+}
diff --git a/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs
index 693a5bb..00e360b 100644
--- a/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs
+++ b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs
@@ -1,21 +1,140 @@
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using cuqmbr.TravelGuide.Application.Common.Exceptions;
+using cuqmbr.TravelGuide.Application.Common.Persistence;
+using cuqmbr.TravelGuide.Application.Common.Services;
+using cuqmbr.TravelGuide.Domain.Entities;
using MediatR;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryHandler : IRequestHandler
{
- 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)
{
- _authenticationService = authenticationService;
+ _unitOfWork = unitOfWork;
+ _passwordHasher = passwordHasher;
+ _jwtConfiguration = configurationOptions.Value.JsonWebToken;
}
public async Task Handle(
LoginQuery request, CancellationToken cancellationToken)
{
- return await _authenticationService.LoginAsync(
- request.Email, request.Password, cancellationToken);
+ var account = await _unitOfWork.AccountRepository
+ .GetOneAsync(
+ a =>
+ a.Email == request.EmailOrUsername ||
+ a.Username == request.EmailOrUsername,
+ a => a.AccountRoles, cancellationToken);
+
+ if (account == null)
+ {
+ throw new LoginException("No users registered with given email.");
+ }
+
+ var hash = Convert.FromBase64String(account.PasswordHash);
+ var salt = Convert.FromBase64String(account.PasswordSalt);
+ var password = Encoding.UTF8.GetBytes(request.Password);
+
+ var isValidPassword = await _passwordHasher
+ .IsValidHashAsync(hash, password, salt, cancellationToken);
+
+ if (!isValidPassword)
+ {
+ throw new LoginException("Given password is incorrect.");
+ }
+
+ var jwtSecurityToken = await CreateJwtAsync(account, cancellationToken);
+ var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
+
+ var refreshToken = (await _unitOfWork.RefreshTokenRepository
+ .GetPageAsync(
+ e =>
+ e.AccountId == account.Id &&
+ e.RevocationTime == null &&
+ e.ExpirationTime > DateTimeOffset.UtcNow,
+ 1, int.MaxValue, cancellationToken))
+ .Items.FirstOrDefault();
+
+ if (refreshToken == null)
+ {
+ refreshToken = CreateRefreshToken();
+ refreshToken.AccountId = account.Id;
+
+ await _unitOfWork.RefreshTokenRepository
+ .AddOneAsync(refreshToken, cancellationToken);
+ }
+
+ await _unitOfWork.SaveAsync(cancellationToken);
+ _unitOfWork.Dispose();
+
+ return new TokensModel(accessToken, refreshToken.Value);
+ }
+
+ private async Task 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();
+ foreach (var role in roles)
+ {
+ roleClaims.Add(new Claim("roles", role.Name));
+ }
+
+ var claims = new List()
+ {
+ 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)
+ };
}
}
diff --git a/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs
index 9f3fb38..7a0ed0b 100644
--- a/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs
+++ b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs
@@ -1,15 +1,18 @@
using FluentValidation;
+using Microsoft.Extensions.Localization;
namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login;
public class LoginQueryValidator : AbstractValidator
{
- public LoginQueryValidator()
+ public LoginQueryValidator(IStringLocalizer localizer)
{
- RuleFor(v => v.Email)
- .NotEmpty().WithMessage("Email address is required.");
+ RuleFor(v => v.EmailOrUsername)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
RuleFor(v => v.Password)
- .NotEmpty().WithMessage("Password is required.");
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
}
}
diff --git a/src/Application/Buses/BusDto.cs b/src/Application/Buses/BusDto.cs
new file mode 100644
index 0000000..1be10a6
--- /dev/null
+++ b/src/Application/Buses/BusDto.cs
@@ -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
+{
+ 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()
+ .ForMember(
+ d => d.Uuid,
+ opt => opt.MapFrom(s => s.Guid))
+ .ForMember(
+ d => d.CompanyUuid,
+ opt => opt.MapFrom(s => s.Company.Guid));
+ }
+}
diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommand.cs b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs
new file mode 100644
index 0000000..00f0405
--- /dev/null
+++ b/src/Application/Buses/Commands/AddBus/AddBusCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Buses.Commands.AddBus;
+
+public record AddBusCommand : IRequest
+{
+ public string Number { get; set; }
+
+ public string Model { get; set; }
+
+ public short Capacity { get; set; }
+
+ public Guid CompanyGuid { get; set; }
+}
diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs
new file mode 100644
index 0000000..ad47b23
--- /dev/null
+++ b/src/Application/Buses/Commands/AddBus/AddBusCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs
new file mode 100644
index 0000000..3d5c276
--- /dev/null
+++ b/src/Application/Buses/Commands/AddBus/AddBusCommandHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public AddBusCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs
new file mode 100644
index 0000000..f16532a
--- /dev/null
+++ b/src/Application/Buses/Commands/AddBus/AddBusCommandValidator.cs
@@ -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
+{
+ 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"]);
+ }
+}
diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs
new file mode 100644
index 0000000..32ea1d6
--- /dev/null
+++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus;
+
+public record DeleteBusCommand : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs
new file mode 100644
index 0000000..24e4d72
--- /dev/null
+++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs
new file mode 100644
index 0000000..1755ddd
--- /dev/null
+++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandHandler.cs
@@ -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
+{
+ 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();
+ }
+}
diff --git a/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs
new file mode 100644
index 0000000..c4e71a5
--- /dev/null
+++ b/src/Application/Buses/Commands/DeleteBus/DeleteBusCommandValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Buses.Commands.DeleteBus;
+
+public class DeleteBusCommandValidator : AbstractValidator
+{
+ public DeleteBusCommandValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs
new file mode 100644
index 0000000..4a5f18a
--- /dev/null
+++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommand.cs
@@ -0,0 +1,16 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Buses.Commands.UpdateBus;
+
+public record UpdateBusCommand : IRequest
+{
+ 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; }
+}
diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs
new file mode 100644
index 0000000..11be53a
--- /dev/null
+++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs
new file mode 100644
index 0000000..64e74b8
--- /dev/null
+++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public UpdateBusCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs
new file mode 100644
index 0000000..4a11102
--- /dev/null
+++ b/src/Application/Buses/Commands/UpdateBus/UpdateBusCommandValidator.cs
@@ -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
+{
+ 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"]);
+ }
+}
diff --git a/src/Application/Buses/Queries/GetBus/GetBusQuery.cs b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs
new file mode 100644
index 0000000..8419f16
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBus/GetBusQuery.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
+
+public record GetBusQuery : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs
new file mode 100644
index 0000000..1a898b1
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBus/GetBusQueryAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs
new file mode 100644
index 0000000..55c1b86
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBus/GetBusQueryHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetBusQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs
new file mode 100644
index 0000000..b97a62f
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBus/GetBusQueryValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBus;
+
+public class GetBusQueryValidator : AbstractValidator
+{
+ public GetBusQueryValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs
new file mode 100644
index 0000000..343e576
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQuery.cs
@@ -0,0 +1,21 @@
+using cuqmbr.TravelGuide.Application.Common.Models;
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Buses.Queries.GetBusesPage;
+
+public record GetBusesPageQuery : IRequest>
+{
+ 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; }
+}
diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs
new file mode 100644
index 0000000..74b0c99
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs
new file mode 100644
index 0000000..9d29d12
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryHandler.cs
@@ -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>
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetBusesPageQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task> 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(paginatedList.Items.AsQueryable());
+
+ mappedItems = QueryableExtension
+ .ApplySort(mappedItems, request.Sort);
+
+ _unitOfWork.Dispose();
+
+ return new PaginatedList(
+ mappedItems.ToList(),
+ paginatedList.TotalCount, request.PageNumber,
+ request.PageSize);
+ }
+}
diff --git a/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs
new file mode 100644
index 0000000..432f9e3
--- /dev/null
+++ b/src/Application/Buses/Queries/GetBusesPage/GetBusesPageQueryValidator.cs
@@ -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
+{
+ 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));
+ }
+}
diff --git a/src/Application/Buses/ViewModels/AddBusViewModel.cs b/src/Application/Buses/ViewModels/AddBusViewModel.cs
new file mode 100644
index 0000000..0820527
--- /dev/null
+++ b/src/Application/Buses/ViewModels/AddBusViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs
new file mode 100644
index 0000000..725d0e9
--- /dev/null
+++ b/src/Application/Buses/ViewModels/GetBusesPageFilterViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Buses/ViewModels/UpdateBusViewModel.cs b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs
new file mode 100644
index 0000000..7ce4cde
--- /dev/null
+++ b/src/Application/Buses/ViewModels/UpdateBusViewModel.cs
@@ -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; }
+}
diff --git a/src/Application/Cities/CityDto.cs b/src/Application/Cities/CityDto.cs
new file mode 100644
index 0000000..f3a47ff
--- /dev/null
+++ b/src/Application/Cities/CityDto.cs
@@ -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
+{
+ 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()
+ .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));
+ }
+}
diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommand.cs b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs
new file mode 100644
index 0000000..72152a3
--- /dev/null
+++ b/src/Application/Cities/Commands/AddCity/AddCityCommand.cs
@@ -0,0 +1,10 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Commands.AddCity;
+
+public record AddCityCommand : IRequest
+{
+ public string Name { get; set; }
+
+ public Guid RegionGuid { get; set; }
+}
diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs
new file mode 100644
index 0000000..32ff5c8
--- /dev/null
+++ b/src/Application/Cities/Commands/AddCity/AddCityCommandAuthorizer.cs
@@ -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
+{
+ 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
+ });
+ }
+}
diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs
new file mode 100644
index 0000000..13d7708
--- /dev/null
+++ b/src/Application/Cities/Commands/AddCity/AddCityCommandHandler.cs
@@ -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
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public AddCityCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task 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(entity);
+ }
+}
diff --git a/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs
new file mode 100644
index 0000000..4dd216c
--- /dev/null
+++ b/src/Application/Cities/Commands/AddCity/AddCityCommandValidator.cs
@@ -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
+{
+ 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"]);
+ }
+}
diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs
new file mode 100644
index 0000000..94dbf43
--- /dev/null
+++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommand.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity;
+
+public record DeleteCityCommand : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs
new file mode 100644
index 0000000..9295ca7
--- /dev/null
+++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandAuthorizer.cs
@@ -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.DeleteCity;
+
+public class DeleteCityCommandAuthorizer :
+ AbstractRequestAuthorizer
+{
+ private readonly SessionUserService _sessionUserService;
+
+ public DeleteCityCommandAuthorizer(SessionUserService sessionUserService)
+ {
+ _sessionUserService = sessionUserService;
+ }
+
+ public override void BuildPolicy(DeleteCityCommand request)
+ {
+ UseRequirement(new MustBeAuthenticatedRequirement
+ {
+ IsAuthenticated= _sessionUserService.IsAuthenticated
+ });
+
+ UseRequirement(new MustBeInAnyOfRolesRequirement
+ {
+ RequiredRoles = [IdentityRole.Administrator],
+ UserRoles = _sessionUserService.Roles
+ });
+ }
+}
diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs
new file mode 100644
index 0000000..c5ebf61
--- /dev/null
+++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandHandler.cs
@@ -0,0 +1,34 @@
+using MediatR;
+using cuqmbr.TravelGuide.Application.Common.Persistence;
+using cuqmbr.TravelGuide.Application.Common.Exceptions;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity;
+
+public class DeleteCityCommandHandler : IRequestHandler
+{
+ private readonly UnitOfWork _unitOfWork;
+
+ public DeleteCityCommandHandler(UnitOfWork unitOfWork)
+ {
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task Handle(
+ DeleteCityCommand request,
+ CancellationToken cancellationToken)
+ {
+ var entity = await _unitOfWork.CityRepository.GetOneAsync(
+ e => e.Guid == request.Guid, cancellationToken);
+
+ if (entity == null)
+ {
+ throw new NotFoundException();
+ }
+
+ await _unitOfWork.CityRepository.DeleteOneAsync(
+ entity, cancellationToken);
+
+ await _unitOfWork.SaveAsync(cancellationToken);
+ _unitOfWork.Dispose();
+ }
+}
diff --git a/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs
new file mode 100644
index 0000000..582aa0b
--- /dev/null
+++ b/src/Application/Cities/Commands/DeleteCity/DeleteCityCommandValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Commands.DeleteCity;
+
+public class DeleteCityCommandValidator : AbstractValidator
+{
+ public DeleteCityCommandValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs
new file mode 100644
index 0000000..acafa27
--- /dev/null
+++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommand.cs
@@ -0,0 +1,12 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity;
+
+public record UpdateCityCommand : IRequest
+{
+ public Guid Guid { get; set; }
+
+ public string Name { get; set; }
+
+ public Guid RegionGuid { get; set; }
+}
diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs
new file mode 100644
index 0000000..376f57d
--- /dev/null
+++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandAuthorizer.cs
@@ -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.UpdateCity;
+
+public class UpdateCityCommandAuthorizer :
+ AbstractRequestAuthorizer
+{
+ private readonly SessionUserService _sessionUserService;
+
+ public UpdateCityCommandAuthorizer(SessionUserService sessionUserService)
+ {
+ _sessionUserService = sessionUserService;
+ }
+
+ public override void BuildPolicy(UpdateCityCommand request)
+ {
+ UseRequirement(new MustBeAuthenticatedRequirement
+ {
+ IsAuthenticated= _sessionUserService.IsAuthenticated
+ });
+
+ UseRequirement(new MustBeInAnyOfRolesRequirement
+ {
+ RequiredRoles = [IdentityRole.Administrator],
+ UserRoles = _sessionUserService.Roles
+ });
+ }
+}
diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs
new file mode 100644
index 0000000..8e71fb3
--- /dev/null
+++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandHandler.cs
@@ -0,0 +1,55 @@
+using MediatR;
+using cuqmbr.TravelGuide.Application.Common.Persistence;
+using AutoMapper;
+using cuqmbr.TravelGuide.Application.Common.Exceptions;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity;
+
+public class UpdateCityCommandHandler :
+ IRequestHandler
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public UpdateCityCommandHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task Handle(
+ UpdateCityCommand request,
+ CancellationToken cancellationToken)
+ {
+ var entity = await _unitOfWork.CityRepository.GetOneAsync(
+ e => e.Guid == request.Guid, e => e.Region.Country,
+ cancellationToken);
+
+ if (entity == null)
+ {
+ throw new NotFoundException();
+ }
+
+ var parentEntity = await _unitOfWork.RegionRepository.GetOneAsync(
+ e => e.Guid == request.RegionGuid, cancellationToken);
+
+ if (parentEntity == null)
+ {
+ throw new NotFoundException(
+ $"Parent entity with Guid: {request.RegionGuid} not found.");
+ }
+
+ entity.Name = request.Name;
+ entity.RegionId = parentEntity.Id;
+
+ entity = await _unitOfWork.CityRepository.UpdateOneAsync(
+ entity, cancellationToken);
+
+ await _unitOfWork.SaveAsync(cancellationToken);
+ _unitOfWork.Dispose();
+
+ return _mapper.Map(entity);
+ }
+}
diff --git a/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs
new file mode 100644
index 0000000..6d1ef78
--- /dev/null
+++ b/src/Application/Cities/Commands/UpdateCity/UpdateCityCommandValidator.cs
@@ -0,0 +1,31 @@
+using cuqmbr.TravelGuide.Application.Common.Services;
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Commands.UpdateCity;
+
+public class UpdateCityCommandValidator : AbstractValidator
+{
+ public UpdateCityCommandValidator(
+ 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.RegionGuid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs
new file mode 100644
index 0000000..2fdcac5
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQuery.cs
@@ -0,0 +1,19 @@
+using cuqmbr.TravelGuide.Application.Common.Models;
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage;
+
+public record GetCitiesPageQuery : IRequest>
+{
+ 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; }
+}
diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs
new file mode 100644
index 0000000..a14f95d
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryAuthorizer.cs
@@ -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.Cities.Queries.GetCitiesPage;
+
+public class GetCitiesPageQueryAuthorizer :
+ AbstractRequestAuthorizer
+{
+ private readonly SessionUserService _sessionUserService;
+
+ public GetCitiesPageQueryAuthorizer(SessionUserService sessionUserService)
+ {
+ _sessionUserService = sessionUserService;
+ }
+
+ public override void BuildPolicy(GetCitiesPageQuery request)
+ {
+ UseRequirement(new MustBeAuthenticatedRequirement
+ {
+ IsAuthenticated = _sessionUserService.IsAuthenticated
+ });
+
+ UseRequirement(new MustBeInAnyOfRolesRequirement
+ {
+ RequiredRoles =
+ [IdentityRole.Administrator, IdentityRole.CompanyOwner],
+ UserRoles = _sessionUserService.Roles
+ });
+ }
+}
diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs
new file mode 100644
index 0000000..f3b45af
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryHandler.cs
@@ -0,0 +1,55 @@
+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.Cities.Queries.GetCitiesPage;
+
+public class GetCitiesPageQueryHandler :
+ IRequestHandler>
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetCitiesPageQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task> Handle(
+ GetCitiesPageQuery request,
+ CancellationToken cancellationToken)
+ {
+ var paginatedList = await _unitOfWork.CityRepository.GetPageAsync(
+ e =>
+ (e.Name.ToLower().Contains(request.Search.ToLower()) ||
+ e.Region.Name.ToLower().Contains(request.Search.ToLower()) ||
+ e.Region.Country.Name.ToLower().Contains(request.Search.ToLower())) &&
+ (request.RegionGuid != null
+ ? e.Region.Guid == request.RegionGuid
+ : true) &&
+ (request.CountryGuid != null
+ ? e.Region.Country.Guid == request.CountryGuid
+ : true),
+ e => e.Region.Country,
+ request.PageNumber, request.PageSize,
+ cancellationToken);
+
+ var mappedItems = _mapper
+ .ProjectTo(paginatedList.Items.AsQueryable());
+
+ mappedItems = QueryableExtension
+ .ApplySort(mappedItems, request.Sort);
+
+ _unitOfWork.Dispose();
+
+ return new PaginatedList(
+ mappedItems.ToList(),
+ paginatedList.TotalCount, request.PageNumber,
+ request.PageSize);
+ }
+}
diff --git a/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs
new file mode 100644
index 0000000..9bc82fa
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCitiesPage/GetCitiesPageQueryValidator.cs
@@ -0,0 +1,43 @@
+using cuqmbr.TravelGuide.Application.Common.Services;
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCitiesPage;
+
+public class GetCitiesPageQueryValidator : AbstractValidator
+{
+ public GetCitiesPageQueryValidator(
+ 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));
+ }
+}
diff --git a/src/Application/Cities/Queries/GetCity/GetCityQuery.cs b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs
new file mode 100644
index 0000000..4f2c898
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCity/GetCityQuery.cs
@@ -0,0 +1,8 @@
+using MediatR;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity;
+
+public record GetCityQuery : IRequest
+{
+ public Guid Guid { get; set; }
+}
diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs
new file mode 100644
index 0000000..d811478
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCity/GetCityQueryAuthorizer.cs
@@ -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.Cities.Queries.GetCity;
+
+public class GetCityQueryAuthorizer :
+ AbstractRequestAuthorizer
+{
+ private readonly SessionUserService _sessionUserService;
+
+ public GetCityQueryAuthorizer(SessionUserService sessionUserService)
+ {
+ _sessionUserService = sessionUserService;
+ }
+
+ public override void BuildPolicy(GetCityQuery request)
+ {
+ UseRequirement(new MustBeAuthenticatedRequirement
+ {
+ IsAuthenticated = _sessionUserService.IsAuthenticated
+ });
+
+ UseRequirement(new MustBeInAnyOfRolesRequirement
+ {
+ RequiredRoles =
+ [IdentityRole.Administrator, IdentityRole.CompanyOwner],
+ UserRoles = _sessionUserService.Roles
+ });
+ }
+}
diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs
new file mode 100644
index 0000000..49827e2
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCity/GetCityQueryHandler.cs
@@ -0,0 +1,39 @@
+using MediatR;
+using cuqmbr.TravelGuide.Application.Common.Persistence;
+using cuqmbr.TravelGuide.Application.Common.Exceptions;
+using AutoMapper;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity;
+
+public class GetCityQueryHandler :
+ IRequestHandler
+{
+ private readonly UnitOfWork _unitOfWork;
+ private readonly IMapper _mapper;
+
+ public GetCityQueryHandler(
+ UnitOfWork unitOfWork,
+ IMapper mapper)
+ {
+ _unitOfWork = unitOfWork;
+ _mapper = mapper;
+ }
+
+ public async Task Handle(
+ GetCityQuery request,
+ CancellationToken cancellationToken)
+ {
+ var entity = await _unitOfWork.CityRepository.GetOneAsync(
+ e => e.Guid == request.Guid, e => e.Region.Country,
+ cancellationToken);
+
+ _unitOfWork.Dispose();
+
+ if (entity == null)
+ {
+ throw new NotFoundException();
+ }
+
+ return _mapper.Map(entity);
+ }
+}
diff --git a/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs
new file mode 100644
index 0000000..1c7fbb5
--- /dev/null
+++ b/src/Application/Cities/Queries/GetCity/GetCityQueryValidator.cs
@@ -0,0 +1,14 @@
+using FluentValidation;
+using Microsoft.Extensions.Localization;
+
+namespace cuqmbr.TravelGuide.Application.Cities.Queries.GetCity;
+
+public class GetCityQueryValidator : AbstractValidator
+{
+ public GetCityQueryValidator(IStringLocalizer localizer)
+ {
+ RuleFor(v => v.Guid)
+ .NotEmpty()
+ .WithMessage(localizer["FluentValidation.NotEmpty"]);
+ }
+}
diff --git a/src/Application/Cities/ViewModels/AddCityViewModel.cs b/src/Application/Cities/ViewModels/AddCityViewModel.cs
new file mode 100644
index 0000000..ad60681
--- /dev/null
+++ b/src/Application/Cities/ViewModels/AddCityViewModel.cs
@@ -0,0 +1,8 @@
+namespace cuqmbr.TravelGuide.Application.Cities.ViewModels;
+
+public sealed class AddCityViewModel
+{
+ public string Name { get; set; }
+
+ public Guid RegionGuid { get; set; }
+}
diff --git a/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs
new file mode 100644
index 0000000..68ec4d9
--- /dev/null
+++ b/src/Application/Cities/ViewModels/GetCitiesPageFilterViewModel.cs
@@ -0,0 +1,8 @@
+namespace cuqmbr.TravelGuide.Application.Cities.ViewModels;
+
+public sealed class GetCitiesPageFilterViewModel
+{
+ public Guid? CountryUuid { get; set; }
+
+ public Guid? RegionUuid { get; set; }
+}
diff --git a/src/Application/Cities/ViewModels/UpdateCityViewModel.cs b/src/Application/Cities/ViewModels/UpdateCityViewModel.cs
new file mode 100644
index 0000000..05f7e11
--- /dev/null
+++ b/src/Application/Cities/ViewModels/UpdateCityViewModel.cs
@@ -0,0 +1,8 @@
+namespace cuqmbr.TravelGuide.Application.Cities.ViewModels;
+
+public sealed class UpdateCityViewModel
+{
+ public string Name { get; set; }
+
+ public Guid RegionUuid { get; set; }
+}
diff --git a/src/Application/Common/Authorization/AllowAllRequirement.cs b/src/Application/Common/Authorization/AllowAllRequirement.cs
new file mode 100644
index 0000000..05c7d78
--- /dev/null
+++ b/src/Application/Common/Authorization/AllowAllRequirement.cs
@@ -0,0 +1,17 @@
+using MediatR.Behaviors.Authorization;
+
+namespace cuqmbr.TravelGuide.Application.Common.Authorization;
+
+public class AllowAllRequirement : IAuthorizationRequirement
+{
+ class MustBeAuthenticatedRequirementHandler :
+ IAuthorizationHandler
+ {
+ public Task Handle(
+ AllowAllRequirement request,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult(AuthorizationResult.Succeed());
+ }
+ }
+}
diff --git a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs
index 809c638..685e587 100644
--- a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs
+++ b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs
@@ -1,4 +1,3 @@
-// using cuqmbr.TravelGuide.Application.Common.Exceptions;
using MediatR.Behaviors.Authorization;
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
@@ -17,8 +16,6 @@ public class MustBeAuthenticatedRequirement : IAuthorizationRequirement
if (!request.IsAuthenticated)
{
return Task.FromResult(AuthorizationResult.Fail());
- // TODO: Remove UnAuthorizedException, isn't used
- // throw new UnAuthorizedException();
}
return Task.FromResult(AuthorizationResult.Succeed());
diff --git a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs b/src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs
similarity index 54%
rename from src/Application/Common/Authorization/MustBeInRolesRequirement.cs
rename to src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs
index e1368d1..4a293fb 100644
--- a/src/Application/Common/Authorization/MustBeInRolesRequirement.cs
+++ b/src/Application/Common/Authorization/MustBeInAnyOfRolesRequirement.cs
@@ -1,31 +1,22 @@
using MediatR.Behaviors.Authorization;
-using Microsoft.Extensions.Localization;
-using cuqmbr.TravelGuide.Application.Common.Models;
+using cuqmbr.TravelGuide.Domain.Enums;
namespace cuqmbr.TravelGuide.Application.Common.Authorization;
-public class MustBeInRolesRequirement : IAuthorizationRequirement
+public class MustBeInAnyOfRolesRequirement : IAuthorizationRequirement
{
public ICollection UserRoles { get; init; }
public ICollection RequiredRoles { get; init; }
- class MustBeInRolesRequirementHandler :
- IAuthorizationHandler
+ class MustBeInAnyOfRolesRequirementHandler :
+ IAuthorizationHandler
{
- private readonly IStringLocalizer _localizer;
-
- public MustBeInRolesRequirementHandler(IStringLocalizer localizer)
- {
- _localizer = localizer;
- }
-
public Task Handle(
- MustBeInRolesRequirement request,
+ MustBeInAnyOfRolesRequirement request,
CancellationToken cancellationToken)
{
- var isUserInRequiredRoles =
- request.UserRoles?.Any(ur => request.RequiredRoles.Contains(ur))
- ?? false;
+ var isUserInRequiredRoles = request.UserRoles
+ .Any(ur => request.RequiredRoles.Contains(ur));
if (!isUserInRequiredRoles)
{
diff --git a/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs b/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs
new file mode 100644
index 0000000..d33ffc3
--- /dev/null
+++ b/src/Application/Common/Authorization/MustBeObjectOwnerOrAdminRequirement.cs
@@ -0,0 +1,42 @@
+using MediatR.Behaviors.Authorization;
+using cuqmbr.TravelGuide.Domain.Enums;
+
+namespace cuqmbr.TravelGuide.Application.Common.Authorization;
+
+public class MustBeObjectOwnerOrAdminRequirement : IAuthorizationRequirement
+{
+ public ICollection? UserRoles { get; init; }
+
+ public Guid? UserGuid { get; init; }
+ public Guid? RequiredGuid { get; init; }
+
+ class MustBeObjectOwnerOrAdminRequirementHandler :
+ IAuthorizationHandler
+ {
+ public Task Handle(
+ MustBeObjectOwnerOrAdminRequirement request,
+ CancellationToken cancellationToken)
+ {
+ var isAdmin = request?.UserRoles
+ ?.Any(ur => ur.Equals(IdentityRole.Administrator)) ??
+ false;
+
+ if (isAdmin)
+ {
+ return Task.FromResult(AuthorizationResult.Succeed());
+ }
+
+ if (request?.UserGuid == null || request?.RequiredGuid == null)
+ {
+ return Task.FromResult(AuthorizationResult.Fail());
+ }
+
+ if (request.UserGuid == request.RequiredGuid)
+ {
+ return Task.FromResult(AuthorizationResult.Succeed());
+ }
+
+ return Task.FromResult(AuthorizationResult.Fail());
+ }
+ }
+}
diff --git a/src/Application/Common/Exceptions/ValidationException.cs b/src/Application/Common/Exceptions/ValidationException.cs
index 7c1df67..0ed4b17 100644
--- a/src/Application/Common/Exceptions/ValidationException.cs
+++ b/src/Application/Common/Exceptions/ValidationException.cs
@@ -10,9 +10,24 @@ public class ValidationException : Exception
Errors = new Dictionary();
}
+ public ValidationException(string message) : base(message)
+ {
+ Errors = new Dictionary();
+ }
+
public ValidationException(IEnumerable failures)
: this()
{
+ // TODO: Make serialized dictionary look more like this
+ // "errors": {
+ // "viewModel": [
+ // "The viewModel field is required."
+ // ],
+ // "$.addresses[0].order": [
+ // "The JSON value could not be converted to System.Int16. Path: $.addresses[0].order | LineNumber: 5 | BytePositionInLine: 26."
+ // ]
+ // },
+
Errors = failures
.GroupBy(f => f.PropertyName, f => f.ErrorMessage)
.ToDictionary(fg => fg.Key, fg => fg.ToArray());
diff --git a/src/Application/Common/FluentValidation/CustomValidators.cs b/src/Application/Common/FluentValidation/CustomValidators.cs
new file mode 100644
index 0000000..aa7bb49
--- /dev/null
+++ b/src/Application/Common/FluentValidation/CustomValidators.cs
@@ -0,0 +1,63 @@
+using FluentValidation;
+
+namespace cuqmbr.TravelGuide.Application.Common.FluentValidation;
+
+public static class CustomValidators
+{
+ public static IRuleBuilderOptions IsUsername(
+ this IRuleBuilder ruleBuilder)
+ {
+ return
+ ruleBuilder
+ .Matches(@"^[a-z0-9-_\.]*$");
+ }
+
+ // According to RFC 5321.
+ public static IRuleBuilderOptions IsEmail(
+ this IRuleBuilder ruleBuilder)
+ {
+ return
+ ruleBuilder
+ .Matches(@"^[a-z0-9-_\.]{1,64}@[a-z0-9-_\.]{1,251}\.[a-z0-9-_]{2,4}$");
+ }
+
+ // According to ITU-T E.164, no spaces.
+ public static IRuleBuilderOptions IsPhoneNumber(
+ this IRuleBuilder ruleBuilder)
+ {
+ return
+ ruleBuilder
+ .Matches(@"^\+[0-9]{7,15}$");
+ }
+
+ public static IRuleBuilderOptions>
+ IsUnique(
+ this IRuleBuilder> ruleBuilder,
+ Func selector)
+ {
+ if (selector == null)
+ {
+ throw new ArgumentNullException(
+ nameof(selector),
+ "Cannot pass a null selector.");
+ }
+
+ return
+ ruleBuilder
+ .Must(x => x.IsDistinct(selector));
+ }
+
+ public static bool IsDistinct(
+ this IEnumerable elements, Func selector)
+ {
+ var hashSet = new HashSet();
+ foreach (var element in elements.Select(selector))
+ {
+ if (!hashSet.Contains(element))
+ hashSet.Add(element);
+ else
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs
deleted file mode 100644
index b98a962..0000000
--- a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories;
-
-namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence;
-
-public interface UnitOfWork : IDisposable
-{
- CountryRepository CountryRepository { get; }
- RegionRepository RegionRepository { get; }
-
- int Save();
- Task SaveAsync(CancellationToken cancellationToken);
-}
diff --git a/src/Application/Common/Interfaces/Services/AuthenticationService.cs b/src/Application/Common/Interfaces/Services/AuthenticationService.cs
deleted file mode 100644
index 6f8932b..0000000
--- a/src/Application/Common/Interfaces/Services/AuthenticationService.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using cuqmbr.TravelGuide.Application.Authenticaion;
-
-namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
-
-public interface AuthenticationService
-{
- Task RegisterAsync(string email, string password,
- CancellationToken cancellationToken);
-
- Task LoginAsync(string email, string password,
- CancellationToken cancellationToken);
-
- Task RenewAccessTokenAsync(string refreshToken,
- CancellationToken cancellationToken);
-
- Task RevokeRefreshTokenAsync(string refreshToken,
- CancellationToken cancellationToken);
-}
diff --git a/src/Application/Common/Interfaces/Services/CultureService.cs b/src/Application/Common/Interfaces/Services/CultureService.cs
deleted file mode 100644
index 3ee1ee8..0000000
--- a/src/Application/Common/Interfaces/Services/CultureService.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-using System.Globalization;
-
-namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
-
-public interface CultureService
-{
- public CultureInfo Culture { get; }
-}
diff --git a/src/Application/Common/Interfaces/Services/SessionUserService.cs b/src/Application/Common/Interfaces/Services/SessionUserService.cs
deleted file mode 100644
index 0ffb78d..0000000
--- a/src/Application/Common/Interfaces/Services/SessionUserService.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using cuqmbr.TravelGuide.Application.Common.Models;
-
-namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
-
-public interface SessionUserService
-{
- public int? Id { get; }
-
- public Guid? Uuid { get; }
-
- public string? Email { get; }
-
- public ICollection Roles { get; }
-
-
- public bool IsAuthenticated => Id != null;
-
-
- public string? AccessToken { get; }
-
- public string? RefreshToken { get; }
-}
diff --git a/src/Application/Common/Interfaces/Services/TimeZoneService.cs b/src/Application/Common/Interfaces/Services/TimeZoneService.cs
deleted file mode 100644
index 6ec775c..0000000
--- a/src/Application/Common/Interfaces/Services/TimeZoneService.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
-
-public interface TimeZoneService
-{
- public TimeZoneInfo TimeZone { get; }
-}
diff --git a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs
index 679eb28..8f7797f 100644
--- a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs
+++ b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs
@@ -1,14 +1,14 @@
using AutoMapper;
-using cuqmbr.TravelGuide.Application.Common.Interfaces.Services;
+using cuqmbr.TravelGuide.Application.Common.Services;
namespace cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers;
public class DateTimeOffsetToLocalResolver :
IMemberValueResolver