From d694573c199cb94272e65dc4f9a54b27e2d5d8ee Mon Sep 17 00:00:00 2001 From: cuqmbr Date: Tue, 29 Apr 2025 23:51:19 +0300 Subject: [PATCH] initial commit --- .gitignore | 484 +++++++ Dockerfile | 41 + TravelGuide.sln | 64 + global.json | 6 + src/Application/Application.csproj | 31 + .../Commands/Register/RegisterCommand.cs | 10 + .../Register/RegisterCommandHandler.cs | 21 + .../Register/RegisterCommandValidator.cs | 31 + .../RenewAccessTokenCommand.cs | 8 + .../RenewAccessTokenCommandAuthorizer.cs | 24 + .../RenewAccessTokenCommandHandler.cs | 22 + .../RenewAccessTokenCommandValidator.cs | 12 + .../RenewAccessTokenWithCookieCommand.cs | 6 + ...wAccessTokenWithCookieCommandAuthorizer.cs | 26 + ...enewAccessTokenWithCookieCommandHandler.cs | 28 + ...ewAccessTokenWithCookieCommandValidator.cs | 10 + .../RevokeRefreshTokenCommand.cs | 8 + .../RevokeRefreshTokenCommandAuthorizer.cs | 24 + .../RevokeRefreshTokenCommandHandler.cs | 22 + .../RevokeRefreshTokenCommandValidator.cs | 12 + .../RevokeRefreshTokenWithCookieCommand.cs | 6 + ...RefreshTokenWithCookieCommandAuthorizer.cs | 26 + ...okeRefreshTokenWithCookieCommandHandler.cs | 28 + ...eRefreshTokenWithCookieCommandValidator.cs | 10 + .../Queries/Login/LoginQuery.cs | 10 + .../Queries/Login/LoginQueryHandler.cs | 21 + .../Queries/Login/LoginQueryValidator.cs | 15 + src/Application/Authentication/TokensModel.cs | 14 + .../CustomUnauthorizedResultHandler.cs | 13 + .../MustBeAuthenticatedRequirement.cs | 27 + .../Authorization/MustBeInRolesRequirement.cs | 41 + .../Common/Behaviours/LoggingBehaviour.cs | 46 + .../Common/Behaviours/ValidationBehaviour.cs | 42 + .../Exceptions/AuthenticationException.cs | 11 + .../Exceptions/DuplicateEntityException.cs | 11 + .../Common/Exceptions/ForbiddenException.cs | 11 + .../Common/Exceptions/LoginException.cs | 11 + .../Common/Exceptions/NotFoundException.cs | 11 + .../Exceptions/RegistrationException.cs | 11 + .../Exceptions/UnAuthorizedException.cs | 10 + .../Common/Exceptions/ValidationException.cs | 23 + .../Common/Extensions/QueryableExtension.cs | 73 + .../Repositories/BaseRepository.cs | 51 + .../Repositories/CountryRepository.cs | 6 + .../Repositories/RegionRepository.cs | 6 + .../Interfaces/Persistence/UnitOfWork.cs | 12 + .../Services/AuthenticationService.cs | 18 + .../Interfaces/Services/CultureService.cs | 8 + .../Interfaces/Services/SessionUserService.cs | 22 + .../Interfaces/Services/TimeZoneService.cs | 6 + src/Application/Common/Mappings/IMapFrom.cs | 6 + .../Common/Mappings/MappingProfile.cs | 36 + .../Resolvers/DateTimeOffsetResolver.cs | 23 + src/Application/Common/Models/IdentityRole.cs | 21 + .../Common/Models/PaginatedList.cs | 23 + .../Common/ViewModels/PageQuery.cs | 8 + .../Common/ViewModels/SearchQuery.cs | 6 + .../Common/ViewModels/SortQuery.cs | 7 + src/Application/ConfigurationOptions.cs | 28 + .../Commands/AddCountry/AddCountryCommand.cs | 8 + .../AddCountry/AddCountryCommandAuthorizer.cs | 31 + .../AddCountry/AddCountryCommandHandler.cs | 49 + .../AddCountry/AddCountryCommandValidator.cs | 23 + .../DeleteCountry/DeleteCountryCommand.cs | 8 + .../DeleteCountryCommandAuthorizer.cs | 31 + .../DeleteCountryCommandHandler.cs | 34 + .../DeleteCountryCommandValidator.cs | 14 + .../UpdateCountry/UpdateCountryCommand.cs | 10 + .../UpdateCountryCommandAuthorizer.cs | 31 + .../UpdateCountryCommandHandler.cs | 52 + .../UpdateCountryCommandValidator.cs | 27 + src/Application/Countries/CountryDto.cs | 19 + .../GetCountriesPage/GetCountriesPageQuery.cs | 15 + .../GetCountriesPageQueryAuthorizer.cs | 31 + .../GetCountriesPageQueryHandler.cs | 44 + .../GetCountriesPageQueryValidator.cs | 44 + .../Queries/GetCountry/GetCountryQuery.cs | 8 + .../GetCountry/GetCountryQueryAuthorizer.cs | 31 + .../GetCountry/GetCountryQueryHandler.cs | 38 + .../GetCountry/GetCountryQueryValidator.cs | 14 + .../Commands/AddRegion/AddRegionCommand.cs | 10 + .../AddRegion/AddRegionCommandAuthorizer.cs | 31 + .../AddRegion/AddRegionCommandHandler.cs | 60 + .../AddRegion/AddRegionCommandValidator.cs | 27 + .../DeleteRegion/DeleteRegionCommand.cs | 8 + .../DeleteRegionCommandAuthorizer.cs | 31 + .../DeleteRegionCommandHandler.cs | 34 + .../DeleteRegionCommandValidator.cs | 14 + .../UpdateRegion/UpdateRegionCommand.cs | 12 + .../UpdateRegionCommandAuthorizer.cs | 31 + .../UpdateRegionCommandHandler.cs | 54 + .../UpdateRegionCommandValidator.cs | 31 + .../Queries/GetRegion/GetRegionQuery.cs | 8 + .../GetRegion/GetRegionQueryAuthorizer.cs | 31 + .../GetRegion/GetRegionQueryHandler.cs | 39 + .../GetRegion/GetRegionQueryValidator.cs | 14 + .../GetRegionsPage/GetRegionsPageQuery.cs | 17 + .../GetRegionsPageQueryAuthorizer.cs | 31 + .../GetRegionsPageQueryHandler.cs | 51 + .../GetRegionsPageQueryValidator.cs | 43 + src/Application/Regions/RegionDto.cs | 24 + .../GetRegionsPageFilterViewModel.cs | 6 + .../Resources/Localization/en-US.json | 46 + .../Resources/Localization/uk-UA.json | 46 + src/Application/packages.lock.json | 178 +++ .../Application/Configuration.cs | 94 ++ src/Configuration/Configuration.csproj | 38 + .../Configuration/Configuration.cs | 42 + src/Configuration/Identity/Configuration.cs | 96 ++ .../Infrastructure/Configuration.cs | 19 + src/Configuration/Logging/Configuration.cs | 72 + .../Persistence/Configuration.cs | 81 ++ src/Configuration/packages.lock.json | 851 +++++++++++ src/Domain/Domain.csproj | 9 + src/Domain/Entities/Address.cs | 24 + src/Domain/Entities/City.cs | 13 + src/Domain/Entities/Country.cs | 8 + src/Domain/Entities/EntityBase.cs | 8 + src/Domain/Entities/Region.cs | 13 + src/Domain/Entities/Route.cs | 14 + src/Domain/Entities/RouteAddress.cs | 16 + src/Domain/Enums/Enumeration.cs | 79 + src/Domain/Enums/VehicleType.cs | 28 + .../Controllers/AuthenticationController.cs | 87 ++ src/HttpApi/Controllers/ControllerBase.cs | 15 + .../Controllers/CountriesController.cs | 165 +++ src/HttpApi/Controllers/RegionsController.cs | 181 +++ src/HttpApi/Controllers/TestsController.cs | 26 + src/HttpApi/HttpApi.csproj | 38 + .../GlobalExceptionHandlerMiddleware.cs | 210 +++ .../ThreadCultureSetterMiddleware.cs | 22 + src/HttpApi/Program.cs | 120 ++ src/HttpApi/Properties/launchSettings.json | 11 + src/HttpApi/Services/AspNetCultureService.cs | 45 + .../Services/AspNetSessionUserService.cs | 53 + src/HttpApi/Services/AspNetTimeZoneService.cs | 30 + .../AcceptLanguageHeaderOperationFilter.cs | 41 + .../AcceptTimeZoneHeaderOperationFilter.cs | 40 + .../AuthorizationHeaderOperationFilter.cs | 26 + src/HttpApi/appsettings.Development.json | 40 + src/HttpApi/packages.lock.json | 1138 +++++++++++++++ src/Identity/ConfigurationOptions.cs | 34 + .../UnSupportedDatastoreException.cs | 11 + src/Identity/Identity.csproj | 27 + src/Identity/IdentitySeeder.cs | 85 ++ src/Identity/Models/IdentityRole.cs | 6 + src/Identity/Models/IdentityUser.cs | 8 + src/Identity/Models/RefreshToken.cs | 23 + .../IdentityRoleClaimConfiguration.cs | 30 + .../IdentityRoleConfiguration.cs | 34 + .../IdentityUserClaimConfiguration.cs | 30 + .../IdentityUserConfiguration.cs | 128 ++ .../IdentityUserLoginConfiguration.cs | 30 + .../IdentityUserRoleConfiguration.cs | 22 + .../IdentityUserTokenConfiguration.cs | 30 + ...250423194315_Initial_migration.Designer.cs | 355 +++++ .../20250423194315_Initial_migration.cs | 281 ++++ ...ostgreSqlIdentityDbContextModelSnapshot.cs | 352 +++++ .../PostgreSql/PostgreSqlIdentityDbContext.cs | 35 + .../PostgreSql/PostgreSqlInitializer.cs | 28 + .../Services/JwtAuthenticationService.cs | 210 +++ src/Identity/packages.lock.json | 600 ++++++++ src/Infrastructure/ConfigurationOptions.cs | 6 + src/Infrastructure/Infrastructure.csproj | 17 + src/Infrastructure/packages.lock.json | 184 +++ src/Persistence/ConfigurationOptions.cs | 16 + src/Persistence/DbSeeder.cs | 13 + .../UnSupportedDatastoreException.cs | 11 + .../Configurations/BaseConfiguration.cs | 55 + .../Configurations/CountryConfiguration.cs | 31 + .../Configurations/RegionConfiguration.cs | 40 + src/Persistence/InMemory/InMemoryDbContext.cs | 45 + .../InMemory/InMemoryUnitOfWork.cs | 46 + .../Repositories/InMemoryBaseRepository.cs | 132 ++ .../Repositories/InMemoryCountryRepository.cs | 11 + .../Repositories/InMemoryRegionRepository.cs | 11 + src/Persistence/Json/JsonDbContext.cs | 0 src/Persistence/Persistence.csproj | 25 + .../Configurations/AddressConfiguration.cs | 55 + .../Configurations/BaseConfiguration.cs | 55 + .../Configurations/CityConfiguration.cs | 48 + .../Configurations/CountryConfiguration.cs | 31 + .../Configurations/RegionConfiguration.cs | 40 + .../RouteAddressConfiguration.cs | 83 ++ .../Configurations/RouteConfiguration.cs | 30 + ...250427160059_Initial_migration.Designer.cs | 392 +++++ .../20250427160059_Initial_migration.cs | 348 +++++ .../PostgreSqlDbContextModelSnapshot.cs | 389 +++++ .../PostgreSql/PostgreSqlDbContext.cs | 55 + .../PostgreSql/PostgreSqlDbInitializer.cs | 21 + .../PostgreSql/PostgreSqlUnitOfWork.cs | 46 + .../Repositories/PostgreSqlBaseRepository.cs | 133 ++ .../PostgreSqlCountryRepository.cs | 11 + .../PostgreSqlRegionRepository.cs | 11 + src/Persistence/packages.lock.json | 340 +++++ .../Application.IntegrationTests.csproj | 39 + tst/Application.IntegrationTests/BaseTest.cs | 120 ++ .../CountriesTests.cs | 765 ++++++++++ .../RegionsTests.cs | 1290 +++++++++++++++++ .../packages.lock.json | 1024 +++++++++++++ .../xunit.runner.json | 3 + 201 files changed, 15211 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 TravelGuide.sln create mode 100644 global.json create mode 100644 src/Application/Application.csproj create mode 100644 src/Application/Authentication/Commands/Register/RegisterCommand.cs create mode 100644 src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs create mode 100644 src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommand.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandValidator.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs create mode 100644 src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommand.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandValidator.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs create mode 100644 src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs create mode 100644 src/Application/Authentication/Queries/Login/LoginQuery.cs create mode 100644 src/Application/Authentication/Queries/Login/LoginQueryHandler.cs create mode 100644 src/Application/Authentication/Queries/Login/LoginQueryValidator.cs create mode 100644 src/Application/Authentication/TokensModel.cs create mode 100644 src/Application/Common/Authorization/CustomUnauthorizedResultHandler.cs create mode 100644 src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs create mode 100644 src/Application/Common/Authorization/MustBeInRolesRequirement.cs create mode 100644 src/Application/Common/Behaviours/LoggingBehaviour.cs create mode 100644 src/Application/Common/Behaviours/ValidationBehaviour.cs create mode 100644 src/Application/Common/Exceptions/AuthenticationException.cs create mode 100644 src/Application/Common/Exceptions/DuplicateEntityException.cs create mode 100644 src/Application/Common/Exceptions/ForbiddenException.cs create mode 100644 src/Application/Common/Exceptions/LoginException.cs create mode 100644 src/Application/Common/Exceptions/NotFoundException.cs create mode 100644 src/Application/Common/Exceptions/RegistrationException.cs create mode 100644 src/Application/Common/Exceptions/UnAuthorizedException.cs create mode 100644 src/Application/Common/Exceptions/ValidationException.cs create mode 100644 src/Application/Common/Extensions/QueryableExtension.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs create mode 100644 src/Application/Common/Interfaces/Persistence/UnitOfWork.cs create mode 100644 src/Application/Common/Interfaces/Services/AuthenticationService.cs create mode 100644 src/Application/Common/Interfaces/Services/CultureService.cs create mode 100644 src/Application/Common/Interfaces/Services/SessionUserService.cs create mode 100644 src/Application/Common/Interfaces/Services/TimeZoneService.cs create mode 100644 src/Application/Common/Mappings/IMapFrom.cs create mode 100644 src/Application/Common/Mappings/MappingProfile.cs create mode 100644 src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs create mode 100644 src/Application/Common/Models/IdentityRole.cs create mode 100644 src/Application/Common/Models/PaginatedList.cs create mode 100644 src/Application/Common/ViewModels/PageQuery.cs create mode 100644 src/Application/Common/ViewModels/SearchQuery.cs create mode 100644 src/Application/Common/ViewModels/SortQuery.cs create mode 100644 src/Application/ConfigurationOptions.cs create mode 100644 src/Application/Countries/Commands/AddCountry/AddCountryCommand.cs create mode 100644 src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs create mode 100644 src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs create mode 100644 src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs create mode 100644 src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommand.cs create mode 100644 src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs create mode 100644 src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs create mode 100644 src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandValidator.cs create mode 100644 src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommand.cs create mode 100644 src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs create mode 100644 src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs create mode 100644 src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs create mode 100644 src/Application/Countries/CountryDto.cs create mode 100644 src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQuery.cs create mode 100644 src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs create mode 100644 src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs create mode 100644 src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs create mode 100644 src/Application/Countries/Queries/GetCountry/GetCountryQuery.cs create mode 100644 src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs create mode 100644 src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs create mode 100644 src/Application/Countries/Queries/GetCountry/GetCountryQueryValidator.cs create mode 100644 src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs create mode 100644 src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs create mode 100644 src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs create mode 100644 src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs create mode 100644 src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs create mode 100644 src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs create mode 100644 src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs create mode 100644 src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs create mode 100644 src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs create mode 100644 src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs create mode 100644 src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs create mode 100644 src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs create mode 100644 src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs create mode 100644 src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs create mode 100644 src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs create mode 100644 src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs create mode 100644 src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs create mode 100644 src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs create mode 100644 src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs create mode 100644 src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs create mode 100644 src/Application/Regions/RegionDto.cs create mode 100644 src/Application/Regions/ViewModels/GetRegionsPageFilterViewModel.cs create mode 100644 src/Application/Resources/Localization/en-US.json create mode 100644 src/Application/Resources/Localization/uk-UA.json create mode 100644 src/Application/packages.lock.json create mode 100644 src/Configuration/Application/Configuration.cs create mode 100644 src/Configuration/Configuration.csproj create mode 100644 src/Configuration/Configuration/Configuration.cs create mode 100644 src/Configuration/Identity/Configuration.cs create mode 100644 src/Configuration/Infrastructure/Configuration.cs create mode 100644 src/Configuration/Logging/Configuration.cs create mode 100644 src/Configuration/Persistence/Configuration.cs create mode 100644 src/Configuration/packages.lock.json create mode 100644 src/Domain/Domain.csproj create mode 100644 src/Domain/Entities/Address.cs create mode 100644 src/Domain/Entities/City.cs create mode 100644 src/Domain/Entities/Country.cs create mode 100644 src/Domain/Entities/EntityBase.cs create mode 100644 src/Domain/Entities/Region.cs create mode 100644 src/Domain/Entities/Route.cs create mode 100644 src/Domain/Entities/RouteAddress.cs create mode 100644 src/Domain/Enums/Enumeration.cs create mode 100644 src/Domain/Enums/VehicleType.cs create mode 100644 src/HttpApi/Controllers/AuthenticationController.cs create mode 100644 src/HttpApi/Controllers/ControllerBase.cs create mode 100644 src/HttpApi/Controllers/CountriesController.cs create mode 100644 src/HttpApi/Controllers/RegionsController.cs create mode 100644 src/HttpApi/Controllers/TestsController.cs create mode 100644 src/HttpApi/HttpApi.csproj create mode 100644 src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs create mode 100644 src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs create mode 100644 src/HttpApi/Program.cs create mode 100644 src/HttpApi/Properties/launchSettings.json create mode 100644 src/HttpApi/Services/AspNetCultureService.cs create mode 100644 src/HttpApi/Services/AspNetSessionUserService.cs create mode 100644 src/HttpApi/Services/AspNetTimeZoneService.cs create mode 100644 src/HttpApi/Swashbuckle/OperationFilters/AcceptLanguageHeaderOperationFilter.cs create mode 100644 src/HttpApi/Swashbuckle/OperationFilters/AcceptTimeZoneHeaderOperationFilter.cs create mode 100644 src/HttpApi/Swashbuckle/OperationFilters/AuthorizationHeaderOperationFilter.cs create mode 100644 src/HttpApi/appsettings.Development.json create mode 100644 src/HttpApi/packages.lock.json create mode 100644 src/Identity/ConfigurationOptions.cs create mode 100644 src/Identity/Exceptions/UnSupportedDatastoreException.cs create mode 100644 src/Identity/Identity.csproj create mode 100644 src/Identity/IdentitySeeder.cs create mode 100644 src/Identity/Models/IdentityRole.cs create mode 100644 src/Identity/Models/IdentityUser.cs create mode 100644 src/Identity/Models/RefreshToken.cs create mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs create mode 100644 src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs create mode 100644 src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs create mode 100644 src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs create mode 100644 src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs create mode 100644 src/Identity/Services/JwtAuthenticationService.cs create mode 100644 src/Identity/packages.lock.json create mode 100644 src/Infrastructure/ConfigurationOptions.cs create mode 100644 src/Infrastructure/Infrastructure.csproj create mode 100644 src/Infrastructure/packages.lock.json create mode 100644 src/Persistence/ConfigurationOptions.cs create mode 100644 src/Persistence/DbSeeder.cs create mode 100644 src/Persistence/Exceptions/UnSupportedDatastoreException.cs create mode 100644 src/Persistence/InMemory/Configurations/BaseConfiguration.cs create mode 100644 src/Persistence/InMemory/Configurations/CountryConfiguration.cs create mode 100644 src/Persistence/InMemory/Configurations/RegionConfiguration.cs create mode 100644 src/Persistence/InMemory/InMemoryDbContext.cs create mode 100644 src/Persistence/InMemory/InMemoryUnitOfWork.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs create mode 100644 src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs create mode 100644 src/Persistence/Json/JsonDbContext.cs create mode 100644 src/Persistence/Persistence.csproj create mode 100644 src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/CityConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/CountryConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs create mode 100644 src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs create mode 100644 src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs create mode 100644 src/Persistence/PostgreSql/PostgreSqlDbContext.cs create mode 100644 src/Persistence/PostgreSql/PostgreSqlDbInitializer.cs create mode 100644 src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs create mode 100644 src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs create mode 100644 src/Persistence/packages.lock.json create mode 100644 tst/Application.IntegrationTests/Application.IntegrationTests.csproj create mode 100644 tst/Application.IntegrationTests/BaseTest.cs create mode 100644 tst/Application.IntegrationTests/CountriesTests.cs create mode 100644 tst/Application.IntegrationTests/RegionsTests.cs create mode 100644 tst/Application.IntegrationTests/packages.lock.json create mode 100644 tst/Application.IntegrationTests/xunit.runner.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc78471 --- /dev/null +++ b/.gitignore @@ -0,0 +1,484 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from `dotnet new gitignore` + +# dotenv files +.env + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +.idea/ + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/main/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/main/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..65b9ad1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +ARG BUILD_PATH_ARG=/build + + +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build + + +ENV SOURCE_PATH=/source + +ARG BUILD_PATH_ARG +ENV BUILD_PATH=${BUILD_PATH_ARG} + + +ADD ./src ${SOURCE_PATH} +WORKDIR ${SOURCE_PATH} + +RUN dotnet publish HttpApi \ + --output ${BUILD_PATH} --no-self-contained \ + -p:PublishSingleFile=true -p:IsAotCompatible=true + +RUN rm -Rf ${BUILD_PATH}/*pdb + + + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime + + +ARG BUILD_PATH_ARG +ENV BUILD_PATH=${BUILD_PATH_ARG} + +ARG APP_PATH_ARG=/app +ENV APP_PATH=${APP_PATH_ARG} + + +WORKDIR ${APP_PATH} +COPY --from=build ${BUILD_PATH} . + +EXPOSE 8080/tcp + +HEALTHCHECK CMD curl --fail http://localhost:8080/health || exit + +ENTRYPOINT ${APP_PATH}/HttpApi diff --git a/TravelGuide.sln b/TravelGuide.sln new file mode 100644 index 0000000..f012d1c --- /dev/null +++ b/TravelGuide.sln @@ -0,0 +1,64 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "src\Domain\Domain.csproj", "{958720B9-290A-4C45-93D7-464547EC05FA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "src\Application\Application.csproj", "{6FA62100-F370-4985-8133-7DBB0EEB27AC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Persistence", "src\Persistence\Persistence.csproj", "{B9425855-6C99-4E4B-92F8-049B342F76B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{266B11B4-6838-4D17-A277-EDB40A9F1A5E}" +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}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {958720B9-290A-4C45-93D7-464547EC05FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {958720B9-290A-4C45-93D7-464547EC05FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {958720B9-290A-4C45-93D7-464547EC05FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {958720B9-290A-4C45-93D7-464547EC05FA}.Release|Any CPU.Build.0 = Release|Any CPU + {6FA62100-F370-4985-8133-7DBB0EEB27AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FA62100-F370-4985-8133-7DBB0EEB27AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FA62100-F370-4985-8133-7DBB0EEB27AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FA62100-F370-4985-8133-7DBB0EEB27AC}.Release|Any CPU.Build.0 = Release|Any CPU + {B9425855-6C99-4E4B-92F8-049B342F76B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9425855-6C99-4E4B-92F8-049B342F76B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9425855-6C99-4E4B-92F8-049B342F76B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9425855-6C99-4E4B-92F8-049B342F76B1}.Release|Any CPU.Build.0 = Release|Any CPU + {266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {266B11B4-6838-4D17-A277-EDB40A9F1A5E}.Release|Any CPU.Build.0 = Release|Any CPU + {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4431B3CB-A5F2-447A-8BC7-9DC3DA9E6A6D}.Release|Any CPU.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 + {1DCFA4EE-A545-42FE-A3BC-A606D2961298}.Release|Any CPU.Build.0 = Release|Any CPU + {B52B8651-10B8-488D-8ACF-9C4499F8A723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B52B8651-10B8-488D-8ACF-9C4499F8A723}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B52B8651-10B8-488D-8ACF-9C4499F8A723}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B52B8651-10B8-488D-8ACF-9C4499F8A723}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..6137348 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "9.0.104", + "rollForward": "latestMinor" + } +} diff --git a/src/Application/Application.csproj b/src/Application/Application.csproj new file mode 100644 index 0000000..e77ed7a --- /dev/null +++ b/src/Application/Application.csproj @@ -0,0 +1,31 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/Application/Authentication/Commands/Register/RegisterCommand.cs b/src/Application/Authentication/Commands/Register/RegisterCommand.cs new file mode 100644 index 0000000..51edec4 --- /dev/null +++ b/src/Application/Authentication/Commands/Register/RegisterCommand.cs @@ -0,0 +1,10 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; + +public record RegisterCommand : IRequest +{ + public string Email { get; set; } + + public string Password { get; set; } +} diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs new file mode 100644 index 0000000..99ca0f1 --- /dev/null +++ b/src/Application/Authentication/Commands/Register/RegisterCommandHandler.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; + +public class RegisterCommandHandler : IRequestHandler +{ + private readonly AuthenticationService _authenticationService; + + public RegisterCommandHandler(AuthenticationService authenticationService) + { + _authenticationService = authenticationService; + } + + public async Task Handle( + RegisterCommand request, CancellationToken cancellationToken) + { + await _authenticationService.RegisterAsync( + request.Email, request.Password, cancellationToken); + } +} diff --git a/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs new file mode 100644 index 0000000..13afc23 --- /dev/null +++ b/src/Application/Authentication/Commands/Register/RegisterCommandValidator.cs @@ -0,0 +1,31 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; + +public class RegisterCommandValidator : AbstractValidator +{ + public RegisterCommandValidator() + { + RuleFor(v => v.Email) + .NotEmpty() + .WithMessage("Email address is required.") + .Matches(@"\b[\w\.-]+@[\w\.-]+\.\w{2,4}\b") + .WithMessage("Email address is invalid."); + + RuleFor(v => v.Password) + .NotEmpty() + .WithMessage("Password is required.") + .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: !@#$%^&*()."); + } +} diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommand.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommand.cs new file mode 100644 index 0000000..812ae16 --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; + +public record RenewAccessTokenCommand : IRequest +{ + public string RefreshToken { get; set; } +} diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs new file mode 100644 index 0000000..e5237e1 --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandAuthorizer.cs @@ -0,0 +1,24 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using MediatR.Behaviors.Authorization; + +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 + }); + } +} diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs new file mode 100644 index 0000000..0cb9016 --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandHandler.cs @@ -0,0 +1,22 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; + +public class RenewAccessTokenCommandHandler : + IRequestHandler +{ + private readonly AuthenticationService _authenticationService; + + public RenewAccessTokenCommandHandler(AuthenticationService authenticationService) + { + _authenticationService = authenticationService; + } + + public async Task Handle( + RenewAccessTokenCommand request, CancellationToken cancellationToken) + { + return await _authenticationService.RenewAccessTokenAsync( + request.RefreshToken, cancellationToken); + } +} diff --git a/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandValidator.cs b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandValidator.cs new file mode 100644 index 0000000..a2841cf --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessToken/RenewAccessTokenCommandValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; + +public class RenewAccessTokenCommandValidator : + AbstractValidator +{ + public RenewAccessTokenCommandValidator() + { + RuleFor(v => v.RefreshToken).NotEmpty(); + } +} diff --git a/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs new file mode 100644 index 0000000..93f2031 --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommand.cs @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..481c916 --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandAuthorizer.cs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..797b289 --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandHandler.cs @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..a49e1ef --- /dev/null +++ b/src/Application/Authentication/Commands/RenewAccessTokenWithCookieWithCookie/RenewAccessTokenWithCookieCommandValidator.cs @@ -0,0 +1,10 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands + .RenewAccessTokenWithCookie; + +public class RenewAccessTokenWithCookieCommandValidator : + AbstractValidator +{ + public RenewAccessTokenWithCookieCommandValidator() { } +} diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommand.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommand.cs new file mode 100644 index 0000000..0054e41 --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; + +public record RevokeRefreshTokenCommand : IRequest +{ + public string RefreshToken { get; set; } +} diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs new file mode 100644 index 0000000..d298795 --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandAuthorizer.cs @@ -0,0 +1,24 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; + +public class RevokeRefreshTokenCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public RevokeRefreshTokenCommandAuthorizer(SessionUserService currentUserService) + { + _sessionUserService = currentUserService; + } + + public override void BuildPolicy(RevokeRefreshTokenCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated = _sessionUserService.IsAuthenticated + }); + } +} diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs new file mode 100644 index 0000000..1c0f322 --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandHandler.cs @@ -0,0 +1,22 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; + +public class RevokeRefreshTokenCommandHandler : + IRequestHandler +{ + private readonly AuthenticationService _authenticationService; + + public RevokeRefreshTokenCommandHandler(AuthenticationService authenticationService) + { + _authenticationService = authenticationService; + } + + public async Task Handle( + RevokeRefreshTokenCommand request, CancellationToken cancellationToken) + { + await _authenticationService.RevokeRefreshTokenAsync( + request.RefreshToken, cancellationToken); + } +} diff --git a/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandValidator.cs b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandValidator.cs new file mode 100644 index 0000000..9f539bf --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshToken/RevokeRefreshTokenCommandValidator.cs @@ -0,0 +1,12 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; + +public class RevokeRefreshTokenCommandValidator : + AbstractValidator +{ + public RevokeRefreshTokenCommandValidator() + { + RuleFor(v => v.RefreshToken).NotEmpty(); + } +} diff --git a/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs new file mode 100644 index 0000000..f277cc8 --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommand.cs @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..eb2f8e2 --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandAuthorizer.cs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..27a8339 --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandHandler.cs @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..c378206 --- /dev/null +++ b/src/Application/Authentication/Commands/RevokeRefreshTokenWithCookie/RevokeRefreshTokenWithCookieCommandValidator.cs @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..905347c --- /dev/null +++ b/src/Application/Authentication/Queries/Login/LoginQuery.cs @@ -0,0 +1,10 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; + +public record LoginQuery : IRequest +{ + public string Email { get; set; } + + public string Password { get; set; } +} diff --git a/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs new file mode 100644 index 0000000..693a5bb --- /dev/null +++ b/src/Application/Authentication/Queries/Login/LoginQueryHandler.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; + +public class LoginQueryHandler : IRequestHandler +{ + private readonly AuthenticationService _authenticationService; + + public LoginQueryHandler(AuthenticationService authenticationService) + { + _authenticationService = authenticationService; + } + + public async Task Handle( + LoginQuery request, CancellationToken cancellationToken) + { + return await _authenticationService.LoginAsync( + request.Email, request.Password, cancellationToken); + } +} diff --git a/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs new file mode 100644 index 0000000..9f3fb38 --- /dev/null +++ b/src/Application/Authentication/Queries/Login/LoginQueryValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; + +namespace cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; + +public class LoginQueryValidator : AbstractValidator +{ + public LoginQueryValidator() + { + RuleFor(v => v.Email) + .NotEmpty().WithMessage("Email address is required."); + + RuleFor(v => v.Password) + .NotEmpty().WithMessage("Password is required."); + } +} diff --git a/src/Application/Authentication/TokensModel.cs b/src/Application/Authentication/TokensModel.cs new file mode 100644 index 0000000..2f66f0f --- /dev/null +++ b/src/Application/Authentication/TokensModel.cs @@ -0,0 +1,14 @@ +namespace cuqmbr.TravelGuide.Application.Authenticaion; + +public sealed class TokensModel +{ + public TokensModel(string accessToken, string refreshToken) + { + AccessToken = accessToken; + RefreshToken = refreshToken; + } + + public string AccessToken { get; set; } + + public string RefreshToken { get; set; } +} diff --git a/src/Application/Common/Authorization/CustomUnauthorizedResultHandler.cs b/src/Application/Common/Authorization/CustomUnauthorizedResultHandler.cs new file mode 100644 index 0000000..90787e7 --- /dev/null +++ b/src/Application/Common/Authorization/CustomUnauthorizedResultHandler.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using MediatR.Behaviors.Authorization; +using MediatR.Behaviors.Authorization.Interfaces; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class ForbiddenUnauthorizedResultHandler : IUnauthorizedResultHandler +{ + public Task Invoke(AuthorizationResult result) + { + throw new ForbiddenException(result.FailureMessage); + } +} diff --git a/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs new file mode 100644 index 0000000..809c638 --- /dev/null +++ b/src/Application/Common/Authorization/MustBeAuthenticatedRequirement.cs @@ -0,0 +1,27 @@ +// using cuqmbr.TravelGuide.Application.Common.Exceptions; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class MustBeAuthenticatedRequirement : IAuthorizationRequirement +{ + public required bool IsAuthenticated { get; init; } = default!; + + class MustBeAuthenticatedRequirementHandler : + IAuthorizationHandler + { + public Task Handle( + MustBeAuthenticatedRequirement request, + CancellationToken cancellationToken) + { + 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/MustBeInRolesRequirement.cs new file mode 100644 index 0000000..e1368d1 --- /dev/null +++ b/src/Application/Common/Authorization/MustBeInRolesRequirement.cs @@ -0,0 +1,41 @@ +using MediatR.Behaviors.Authorization; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Application.Common.Models; + +namespace cuqmbr.TravelGuide.Application.Common.Authorization; + +public class MustBeInRolesRequirement : IAuthorizationRequirement +{ + public ICollection UserRoles { get; init; } + public ICollection RequiredRoles { get; init; } + + class MustBeInRolesRequirementHandler : + IAuthorizationHandler + { + private readonly IStringLocalizer _localizer; + + public MustBeInRolesRequirementHandler(IStringLocalizer localizer) + { + _localizer = localizer; + } + + public Task Handle( + MustBeInRolesRequirement request, + CancellationToken cancellationToken) + { + var isUserInRequiredRoles = + request.UserRoles?.Any(ur => request.RequiredRoles.Contains(ur)) + ?? false; + + if (!isUserInRequiredRoles) + { + // TODO: Make message visible in api response + return Task.FromResult(AuthorizationResult.Fail( + "You must be in the following roles: " + + $"{String.Join(", ", request.RequiredRoles)}.")); + } + + return Task.FromResult(AuthorizationResult.Succeed()); + } + } +} diff --git a/src/Application/Common/Behaviours/LoggingBehaviour.cs b/src/Application/Common/Behaviours/LoggingBehaviour.cs new file mode 100644 index 0000000..7e52a79 --- /dev/null +++ b/src/Application/Common/Behaviours/LoggingBehaviour.cs @@ -0,0 +1,46 @@ +using System.Diagnostics; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace cuqmbr.TravelGuide.Application.Common.Behaviours; + +public class LoggingBehaviour : + IPipelineBehavior + where TRequest : notnull +{ + private readonly ILogger _logger; + private readonly Stopwatch _stopWatch; + + public LoggingBehaviour(ILogger logger) + { + _logger = logger; + _stopWatch = new Stopwatch(); + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + _logger.LogDebug("Started handling MediatR request."); + + _stopWatch.Start(); + + var response = await next(); + + _stopWatch.Stop(); + + _logger.LogInformation( + "MediatR request handled in {Duration}ms.", + _stopWatch.ElapsedMilliseconds); + + if (_stopWatch.ElapsedMilliseconds > 500) + { + _logger.LogWarning( + "MediatR request was handled slowly ({Duration}ms).", + _stopWatch.ElapsedMilliseconds); + } + + return response; + } +} diff --git a/src/Application/Common/Behaviours/ValidationBehaviour.cs b/src/Application/Common/Behaviours/ValidationBehaviour.cs new file mode 100644 index 0000000..f9972ac --- /dev/null +++ b/src/Application/Common/Behaviours/ValidationBehaviour.cs @@ -0,0 +1,42 @@ +using FluentValidation; +using MediatR; +using ValidationException = + cuqmbr.TravelGuide.Application.Common.Exceptions.ValidationException; + +namespace cuqmbr.TravelGuide.Application.Common.Behaviours; + +public class ValidationBehaviour : + IPipelineBehavior + where TRequest : notnull +{ + private readonly IEnumerable> _validators; + + public ValidationBehaviour(IEnumerable> validators) + { + _validators = validators; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + if (_validators.Any()) + { + var context = new ValidationContext(request); + + var failures = _validators + .Select(v => v.Validate(context)) + .SelectMany(r => r.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Any()) + { + throw new ValidationException(failures); + } + } + + return await next(); + } +} diff --git a/src/Application/Common/Exceptions/AuthenticationException.cs b/src/Application/Common/Exceptions/AuthenticationException.cs new file mode 100644 index 0000000..60d216c --- /dev/null +++ b/src/Application/Common/Exceptions/AuthenticationException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class AuthenticationException : Exception +{ + public AuthenticationException() + : base() { } + + public AuthenticationException(string message) + : base(message) { } +} + diff --git a/src/Application/Common/Exceptions/DuplicateEntityException.cs b/src/Application/Common/Exceptions/DuplicateEntityException.cs new file mode 100644 index 0000000..54c0e14 --- /dev/null +++ b/src/Application/Common/Exceptions/DuplicateEntityException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class DuplicateEntityException : Exception +{ + public DuplicateEntityException() + : base() { } + + public DuplicateEntityException(string message) + : base(message) { } +} + diff --git a/src/Application/Common/Exceptions/ForbiddenException.cs b/src/Application/Common/Exceptions/ForbiddenException.cs new file mode 100644 index 0000000..2f57288 --- /dev/null +++ b/src/Application/Common/Exceptions/ForbiddenException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class ForbiddenException : Exception +{ + public ForbiddenException() + : base() { } + + public ForbiddenException(string message) + : base(message) { } +} + diff --git a/src/Application/Common/Exceptions/LoginException.cs b/src/Application/Common/Exceptions/LoginException.cs new file mode 100644 index 0000000..f30962d --- /dev/null +++ b/src/Application/Common/Exceptions/LoginException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class LoginException : Exception +{ + public LoginException() + : base() { } + + public LoginException(string message) + : base(message) { } +} + diff --git a/src/Application/Common/Exceptions/NotFoundException.cs b/src/Application/Common/Exceptions/NotFoundException.cs new file mode 100644 index 0000000..cf9a931 --- /dev/null +++ b/src/Application/Common/Exceptions/NotFoundException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class NotFoundException : Exception +{ + public NotFoundException() + : base() { } + + public NotFoundException(string message) + : base(message) { } +} + diff --git a/src/Application/Common/Exceptions/RegistrationException.cs b/src/Application/Common/Exceptions/RegistrationException.cs new file mode 100644 index 0000000..aed5762 --- /dev/null +++ b/src/Application/Common/Exceptions/RegistrationException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class RegistrationException : Exception +{ + public RegistrationException() + : base() { } + + public RegistrationException(string message) + : base(message) { } +} + diff --git a/src/Application/Common/Exceptions/UnAuthorizedException.cs b/src/Application/Common/Exceptions/UnAuthorizedException.cs new file mode 100644 index 0000000..ba1cdda --- /dev/null +++ b/src/Application/Common/Exceptions/UnAuthorizedException.cs @@ -0,0 +1,10 @@ +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class UnAuthorizedException : Exception +{ + public UnAuthorizedException() + : base() { } + + public UnAuthorizedException(string message) + : base(message) { } +} diff --git a/src/Application/Common/Exceptions/ValidationException.cs b/src/Application/Common/Exceptions/ValidationException.cs new file mode 100644 index 0000000..7c1df67 --- /dev/null +++ b/src/Application/Common/Exceptions/ValidationException.cs @@ -0,0 +1,23 @@ +using FluentValidation.Results; + +namespace cuqmbr.TravelGuide.Application.Common.Exceptions; + +public class ValidationException : Exception +{ + public ValidationException() + : base("One or more validation failures have occurred.") + { + Errors = new Dictionary(); + } + + public ValidationException(IEnumerable failures) + : this() + { + Errors = failures + .GroupBy(f => f.PropertyName, f => f.ErrorMessage) + .ToDictionary(fg => fg.Key, fg => fg.ToArray()); + } + + public IDictionary Errors { get; } +} + diff --git a/src/Application/Common/Extensions/QueryableExtension.cs b/src/Application/Common/Extensions/QueryableExtension.cs new file mode 100644 index 0000000..3deb3c5 --- /dev/null +++ b/src/Application/Common/Extensions/QueryableExtension.cs @@ -0,0 +1,73 @@ +using System.Linq.Dynamic.Core; +using System.Reflection; +using System.Text; + +namespace cuqmbr.TravelGuide.Application.Common.Extensions; + +public static class QueryableExtension +{ + public static IQueryable ApplySort( + IQueryable entities, string? orderQueryString) + { + if (!entities.Any() || String.IsNullOrWhiteSpace(orderQueryString)) + { + return entities; + } + + + var orderParameters = orderQueryString.Trim().Split(","); + + + var queryPropertyTypes = typeof(T) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .ToList(); + + var queryPropertyNames = queryPropertyTypes.ConvertAll(o => o.Name); + + + var orderQueryBuilder = new StringBuilder(); + + foreach (var parameter in orderParameters) + { + if (string.IsNullOrWhiteSpace(parameter)) + { + continue; + } + + var queryPropertyName = + parameter[0] == '-' || + parameter[0] == '+' + ? parameter.Substring(1) + : parameter; + + var propertyName = queryPropertyNames + .FirstOrDefault(pn => + pn.Equals( + queryPropertyName, + StringComparison.InvariantCultureIgnoreCase)); + + if (propertyName == null) + { + continue; + } + + var sortingOrder = + parameter[0] == '-' + ? "descending" + : "ascending"; + + orderQueryBuilder.Append($"{propertyName} {sortingOrder}, "); + } + + + var orderQuery = orderQueryBuilder.ToString() + .TrimEnd(',', ' '); + + if (String.IsNullOrWhiteSpace(orderQuery)) + { + return entities; + } + + return entities.OrderBy(orderQuery); + } +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs new file mode 100644 index 0000000..e3b78d5 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/BaseRepository.cs @@ -0,0 +1,51 @@ +using System.Linq.Expressions; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface BaseRepository + where TEntity : EntityBase +{ + Task AddOneAsync( + TEntity entity, + CancellationToken cancellationToken); + + Task GetOneAsync( + Expression> predicate, + CancellationToken cancellationToken); + + Task GetOneAsync( + Expression> predicate, + Expression> includeSelector, + CancellationToken cancellationToken); + + Task> GetPageAsync( + int pageNumber, int pageSize, + CancellationToken cancellationToken); + + Task> GetPageAsync( + Expression> includeSelector, + int pageNumber, int pageSize, + CancellationToken cancellationToken); + + Task> GetPageAsync( + Expression> predicate, + int pageNumber, int pageSize, + CancellationToken cancellationToken); + + Task> GetPageAsync( + Expression> predicate, + Expression> includeSelector, + int pageNumber, int pageSize, + CancellationToken cancellationToken); + + Task UpdateOneAsync( + TEntity entity, + CancellationToken cancellationToken); + + Task DeleteOneAsync( + TEntity entity, + CancellationToken cancellationToken); +} diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs new file mode 100644 index 0000000..2be573d --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/CountryRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface CountryRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs b/src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs new file mode 100644 index 0000000..29ecdfe --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/Repositories/RegionRepository.cs @@ -0,0 +1,6 @@ +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Common.Interfaces + .Persistence.Repositories; + +public interface RegionRepository : BaseRepository { } diff --git a/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs new file mode 100644 index 0000000..b98a962 --- /dev/null +++ b/src/Application/Common/Interfaces/Persistence/UnitOfWork.cs @@ -0,0 +1,12 @@ +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 new file mode 100644 index 0000000..6f8932b --- /dev/null +++ b/src/Application/Common/Interfaces/Services/AuthenticationService.cs @@ -0,0 +1,18 @@ +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 new file mode 100644 index 0000000..3ee1ee8 --- /dev/null +++ b/src/Application/Common/Interfaces/Services/CultureService.cs @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..0ffb78d --- /dev/null +++ b/src/Application/Common/Interfaces/Services/SessionUserService.cs @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..6ec775c --- /dev/null +++ b/src/Application/Common/Interfaces/Services/TimeZoneService.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +public interface TimeZoneService +{ + public TimeZoneInfo TimeZone { get; } +} diff --git a/src/Application/Common/Mappings/IMapFrom.cs b/src/Application/Common/Mappings/IMapFrom.cs new file mode 100644 index 0000000..7f95949 --- /dev/null +++ b/src/Application/Common/Mappings/IMapFrom.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Common.Mappings; + +public interface IMapFrom +{ + void Mapping(MappingProfile profile) => profile.CreateMap(typeof(T), GetType()); +} diff --git a/src/Application/Common/Mappings/MappingProfile.cs b/src/Application/Common/Mappings/MappingProfile.cs new file mode 100644 index 0000000..08c1f33 --- /dev/null +++ b/src/Application/Common/Mappings/MappingProfile.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Common.Mappings; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly()); + } + + private void ApplyMappingsFromAssembly(Assembly assembly) + { + var types = assembly.GetExportedTypes() + .Where(t => t.GetInterfaces() + .Any(i => + i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(IMapFrom<>) + ) + ) + .ToList(); + + foreach (var type in types) + { + var instance = Activator.CreateInstance(type); + + var methodInfo = + type.GetMethod("Mapping") ?? + type.GetInterface("IMapFrom`1")?.GetMethod("Mapping"); + + methodInfo?.Invoke(instance, new object[] { this }); + } + } +} + diff --git a/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs new file mode 100644 index 0000000..679eb28 --- /dev/null +++ b/src/Application/Common/Mappings/Resolvers/DateTimeOffsetResolver.cs @@ -0,0 +1,23 @@ +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +namespace cuqmbr.TravelGuide.Application.Common.Mappings.Resolvers; + +public class DateTimeOffsetToLocalResolver : + IMemberValueResolver +{ + private readonly TimeZoneService _timeZoneService; + + public DateTimeOffsetToLocalResolver(TimeZoneService timeZoneService) + { + _timeZoneService = timeZoneService; + } + + public DateTimeOffset Resolve( + object source, object destination, + DateTimeOffset sourceMember, DateTimeOffset destinationMember, + ResolutionContext context) + { + return TimeZoneInfo.ConvertTime(sourceMember, _timeZoneService.TimeZone); + } +} diff --git a/src/Application/Common/Models/IdentityRole.cs b/src/Application/Common/Models/IdentityRole.cs new file mode 100644 index 0000000..5636e3b --- /dev/null +++ b/src/Application/Common/Models/IdentityRole.cs @@ -0,0 +1,21 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Application.Common.Models; + +public abstract class IdentityRole : Enumeration +{ + public static readonly IdentityRole Administrator = new AdministratorRole(); + public static readonly IdentityRole User = new UserRole(); + + protected IdentityRole(int value, string name) : base(value, name) { } + + private sealed class AdministratorRole : IdentityRole + { + public AdministratorRole() : base(0, "administrator") { } + } + + private sealed class UserRole : IdentityRole + { + public UserRole() : base(1, "user") { } + } +} diff --git a/src/Application/Common/Models/PaginatedList.cs b/src/Application/Common/Models/PaginatedList.cs new file mode 100644 index 0000000..595fce1 --- /dev/null +++ b/src/Application/Common/Models/PaginatedList.cs @@ -0,0 +1,23 @@ +namespace cuqmbr.TravelGuide.Application.Common.Models; + +public class PaginatedList +{ + public IReadOnlyCollection Items { get; } + public int PageNumber { get; } + public int TotalPages { get; } + public int TotalCount { get; } + + public PaginatedList( + IReadOnlyCollection items, int count, + int pageNumber, int pageSize) + { + PageNumber = pageNumber; + TotalPages = (int) Math.Ceiling(count / (double) pageSize); + TotalCount = count; + Items = items; + } + + public bool HasPreviousPage => PageNumber > 1; + + public bool HasNextPage => PageNumber < TotalPages; +} diff --git a/src/Application/Common/ViewModels/PageQuery.cs b/src/Application/Common/ViewModels/PageQuery.cs new file mode 100644 index 0000000..fd54fd9 --- /dev/null +++ b/src/Application/Common/ViewModels/PageQuery.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Application.Common.ViewModels; + +public sealed class PageQuery +{ + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 10; +} diff --git a/src/Application/Common/ViewModels/SearchQuery.cs b/src/Application/Common/ViewModels/SearchQuery.cs new file mode 100644 index 0000000..b6e12c1 --- /dev/null +++ b/src/Application/Common/ViewModels/SearchQuery.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Common.ViewModels; + +public sealed class SearchQuery +{ + public string Search { get; set; } = String.Empty; +} diff --git a/src/Application/Common/ViewModels/SortQuery.cs b/src/Application/Common/ViewModels/SortQuery.cs new file mode 100644 index 0000000..56a9c68 --- /dev/null +++ b/src/Application/Common/ViewModels/SortQuery.cs @@ -0,0 +1,7 @@ +namespace cuqmbr.TravelGuide.Application.Common.ViewModels; + +public sealed class SortQuery +{ + public string Sort { get; set; } = String.Empty; +} + diff --git a/src/Application/ConfigurationOptions.cs b/src/Application/ConfigurationOptions.cs new file mode 100644 index 0000000..e80d048 --- /dev/null +++ b/src/Application/ConfigurationOptions.cs @@ -0,0 +1,28 @@ +namespace cuqmbr.TravelGuide.Application; + +public sealed class ConfigurationOptions +{ + public static string SectionName { get; } = "Application"; + + public LocalizationConfigurationOptions Localization { get; set; } = new(); + + public LoggingConfigurationOptions Logging { get; set; } = new(); +} + +public sealed class LocalizationConfigurationOptions +{ + public string DefaultCultureName { get; set; } = "en-US"; + + public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(30); +} + +public sealed class LoggingConfigurationOptions +{ + public string Type { get; set; } = "SimpleConsole"; + + public string LogLevel { get; set; } = "Information"; + + public string TimestampFormat { get; set; } = "yyyy-MM-ddTHH:mm:ss.fffK"; + + public bool UseUtcTimestamp { get; set; } = true; +} diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommand.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommand.cs new file mode 100644 index 0000000..fa01636 --- /dev/null +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; + +public record AddCountryCommand : IRequest +{ + public string Name { get; set; } +} diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs new file mode 100644 index 0000000..3ba50fd --- /dev/null +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; + +public class AddCountryCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddCountryCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddCountryCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs new file mode 100644 index 0000000..91ad72c --- /dev/null +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandHandler.cs @@ -0,0 +1,49 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; + +public class AddCountryCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddCountryCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddCountryCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CountryRepository.GetOneAsync( + e => e.Name == request.Name, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Country with given name already exists."); + } + + entity = new Country() + { + Name = request.Name + }; + + entity = await _unitOfWork.CountryRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs new file mode 100644 index 0000000..51e03ce --- /dev/null +++ b/src/Application/Countries/Commands/AddCountry/AddCountryCommandValidator.cs @@ -0,0 +1,23 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; + +public class AddCountryCommandValidator : AbstractValidator +{ + public AddCountryCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + } +} diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommand.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommand.cs new file mode 100644 index 0000000..4b9c621 --- /dev/null +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; + +public record DeleteCountryCommand : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs new file mode 100644 index 0000000..73b8422 --- /dev/null +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; + +public class DeleteCountryCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteCountryCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteCountryCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs new file mode 100644 index 0000000..8f3b55a --- /dev/null +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; + +public class DeleteCountryCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteCountryCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteCountryCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CountryRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.CountryRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandValidator.cs b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandValidator.cs new file mode 100644 index 0000000..153ec30 --- /dev/null +++ b/src/Application/Countries/Commands/DeleteCountry/DeleteCountryCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; + +public class DeleteCountryCommandValidator : AbstractValidator +{ + public DeleteCountryCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommand.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommand.cs new file mode 100644 index 0000000..be28db9 --- /dev/null +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommand.cs @@ -0,0 +1,10 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; + +public record UpdateCountryCommand : IRequest +{ + public Guid Guid { get; set; } + + public string Name { get; set; } +} diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs new file mode 100644 index 0000000..dc4994c --- /dev/null +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; + +public class UpdateCountryCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateCountryCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateCountryCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs new file mode 100644 index 0000000..7d3d822 --- /dev/null +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandHandler.cs @@ -0,0 +1,52 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; + +public class UpdateCountryCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateCountryCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateCountryCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CountryRepository.GetOneAsync( + e => e.Name == request.Name, cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException(); + } + + entity = await _unitOfWork.CountryRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + entity.Name = request.Name; + + entity = await _unitOfWork.CountryRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs new file mode 100644 index 0000000..d2d0612 --- /dev/null +++ b/src/Application/Countries/Commands/UpdateCountry/UpdateCountryCommandValidator.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; + +public class UpdateCountryCommandValidator : AbstractValidator +{ + public UpdateCountryCommandValidator( + IStringLocalizer localizer, + CultureService 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)); + } +} diff --git a/src/Application/Countries/CountryDto.cs b/src/Application/Countries/CountryDto.cs new file mode 100644 index 0000000..3f26d7d --- /dev/null +++ b/src/Application/Countries/CountryDto.cs @@ -0,0 +1,19 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Countries; + +public sealed class CountryDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public void Mapping(MappingProfile profile) + { + profile.CreateMap() + .ForMember( + d => d.Uuid, + opt => opt.MapFrom(s => s.Guid)); + } +} diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQuery.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQuery.cs new file mode 100644 index 0000000..0d00952 --- /dev/null +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQuery.cs @@ -0,0 +1,15 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; + +public record GetCountriesPageQuery : 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; +} diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs new file mode 100644 index 0000000..67796d8 --- /dev/null +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; + +public class GetCountriesPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCountriesPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCountriesPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs new file mode 100644 index 0000000..2ab565d --- /dev/null +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; + +public class GetCountriesPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCountriesPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetCountriesPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.CountryRepository.GetPageAsync( + e => e.Name.ToLower().Contains(request.Search.ToLower()), + 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/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs new file mode 100644 index 0000000..59d2866 --- /dev/null +++ b/src/Application/Countries/Queries/GetCountriesPage/GetCountriesPageQueryValidator.cs @@ -0,0 +1,44 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; + +public class GetCountriesPageQueryValidator : + AbstractValidator +{ + public GetCountriesPageQueryValidator( + IStringLocalizer localizer, + CultureService 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/Countries/Queries/GetCountry/GetCountryQuery.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQuery.cs new file mode 100644 index 0000000..2f13ccb --- /dev/null +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; + +public record GetCountryQuery : IRequest +{ + public Guid Guid { get; set; } +} diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs new file mode 100644 index 0000000..699f958 --- /dev/null +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; + +public class GetCountryQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetCountryQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetCountryQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs new file mode 100644 index 0000000..49851a9 --- /dev/null +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryHandler.cs @@ -0,0 +1,38 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; + +public class GetCountryQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetCountryQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetCountryQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.CountryRepository.GetOneAsync( + e => e.Guid == request.Guid, cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Countries/Queries/GetCountry/GetCountryQueryValidator.cs b/src/Application/Countries/Queries/GetCountry/GetCountryQueryValidator.cs new file mode 100644 index 0000000..02d1efb --- /dev/null +++ b/src/Application/Countries/Queries/GetCountry/GetCountryQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; + +public class GetCountryQueryValidator : AbstractValidator +{ + public GetCountryQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Guid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs new file mode 100644 index 0000000..cc9c48f --- /dev/null +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommand.cs @@ -0,0 +1,10 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; + +public record AddRegionCommand : IRequest +{ + public string Name { get; set; } + + public Guid CountryUuid { get; set; } +} diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs new file mode 100644 index 0000000..05be471 --- /dev/null +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; + +public class AddRegionCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public AddRegionCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(AddRegionCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs new file mode 100644 index 0000000..87357bd --- /dev/null +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandHandler.cs @@ -0,0 +1,60 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Domain.Entities; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; + +public class AddRegionCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public AddRegionCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + AddRegionCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Name == request.Name && e.Country.Guid == request.CountryUuid, + cancellationToken); + + if (entity != null) + { + throw new DuplicateEntityException( + "Region with given name already exists."); + } + + var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync( + e => e.Guid == request.CountryUuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CountryUuid} not found."); + } + + entity = new Region() + { + Name = request.Name, + CountryId = parentEntity.Id + }; + + entity = await _unitOfWork.RegionRepository.AddOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs new file mode 100644 index 0000000..6cbc97d --- /dev/null +++ b/src/Application/Regions/Commands/AddRegion/AddRegionCommandValidator.cs @@ -0,0 +1,27 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; + +public class AddRegionCommandValidator : AbstractValidator +{ + public AddRegionCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Name) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]) + .MaximumLength(64) + .WithMessage( + String.Format( + cultureService.Culture, + localizer["FluentValidation.MaximumLength"], + 64)); + + RuleFor(v => v.CountryUuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs new file mode 100644 index 0000000..b6839cd --- /dev/null +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommand.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; + +public record DeleteRegionCommand : IRequest +{ + public Guid Uuid { get; set; } +} diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs new file mode 100644 index 0000000..593eb3b --- /dev/null +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; + +public class DeleteRegionCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public DeleteRegionCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(DeleteRegionCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs new file mode 100644 index 0000000..b52384b --- /dev/null +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandHandler.cs @@ -0,0 +1,34 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; + +public class DeleteRegionCommandHandler : IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + + public DeleteRegionCommandHandler(UnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task Handle( + DeleteRegionCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.Uuid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + await _unitOfWork.RegionRepository.DeleteOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + } +} diff --git a/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs new file mode 100644 index 0000000..5bfaf0b --- /dev/null +++ b/src/Application/Regions/Commands/DeleteRegion/DeleteRegionCommandValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; + +public class DeleteRegionCommandValidator : AbstractValidator +{ + public DeleteRegionCommandValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs new file mode 100644 index 0000000..1ff6fb2 --- /dev/null +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommand.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; + +public record UpdateRegionCommand : IRequest +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public Guid CountryUuid { get; set; } +} diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs new file mode 100644 index 0000000..d36cd2d --- /dev/null +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; + +public class UpdateRegionCommandAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public UpdateRegionCommandAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(UpdateRegionCommand request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs new file mode 100644 index 0000000..f5fd163 --- /dev/null +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandHandler.cs @@ -0,0 +1,54 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; + +public class UpdateRegionCommandHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public UpdateRegionCommandHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + UpdateRegionCommand request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.Uuid, cancellationToken); + + if (entity == null) + { + throw new NotFoundException(); + } + + var parentEntity = await _unitOfWork.CountryRepository.GetOneAsync( + e => e.Guid == request.CountryUuid, cancellationToken); + + if (parentEntity == null) + { + throw new NotFoundException( + $"Parent entity with Guid: {request.CountryUuid} not found."); + } + + entity.Name = request.Name; + entity.CountryId = parentEntity.Id; + + entity = await _unitOfWork.RegionRepository.UpdateOneAsync( + entity, cancellationToken); + + await _unitOfWork.SaveAsync(cancellationToken); + _unitOfWork.Dispose(); + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs new file mode 100644 index 0000000..a259cd5 --- /dev/null +++ b/src/Application/Regions/Commands/UpdateRegion/UpdateRegionCommandValidator.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; + +public class UpdateRegionCommandValidator : AbstractValidator +{ + public UpdateRegionCommandValidator( + IStringLocalizer localizer, + CultureService cultureService) + { + RuleFor(v => v.Uuid) + .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.CountryUuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs new file mode 100644 index 0000000..32b5f5d --- /dev/null +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQuery.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; + +public record GetRegionQuery : IRequest +{ + public Guid Uuid { get; set; } +} diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs new file mode 100644 index 0000000..55ebab2 --- /dev/null +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; + +public class GetRegionQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRegionQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRegionQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs new file mode 100644 index 0000000..1af903e --- /dev/null +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryHandler.cs @@ -0,0 +1,39 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using AutoMapper; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; + +public class GetRegionQueryHandler : + IRequestHandler +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetRegionQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task Handle( + GetRegionQuery request, + CancellationToken cancellationToken) + { + var entity = await _unitOfWork.RegionRepository.GetOneAsync( + e => e.Guid == request.Uuid, e => e.Country, + cancellationToken); + + _unitOfWork.Dispose(); + + if (entity == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(entity); + } +} diff --git a/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs b/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs new file mode 100644 index 0000000..90e7f09 --- /dev/null +++ b/src/Application/Regions/Queries/GetRegion/GetRegionQueryValidator.cs @@ -0,0 +1,14 @@ +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; + +public class GetRegionQueryValidator : AbstractValidator +{ + public GetRegionQueryValidator(IStringLocalizer localizer) + { + RuleFor(v => v.Uuid) + .NotEmpty() + .WithMessage(localizer["FluentValidation.NotEmpty"]); + } +} diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs new file mode 100644 index 0000000..d618dd6 --- /dev/null +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQuery.cs @@ -0,0 +1,17 @@ +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; + +public record GetRegionsPageQuery : 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? CountryUuid { get; set; } +} diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs new file mode 100644 index 0000000..e742d58 --- /dev/null +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryAuthorizer.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Application.Common.Authorization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; +using MediatR.Behaviors.Authorization; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; + +public class GetRegionsPageQueryAuthorizer : + AbstractRequestAuthorizer +{ + private readonly SessionUserService _sessionUserService; + + public GetRegionsPageQueryAuthorizer(SessionUserService sessionUserService) + { + _sessionUserService = sessionUserService; + } + + public override void BuildPolicy(GetRegionsPageQuery request) + { + UseRequirement(new MustBeAuthenticatedRequirement + { + IsAuthenticated= _sessionUserService.IsAuthenticated + }); + + UseRequirement(new MustBeInRolesRequirement + { + RequiredRoles = [IdentityRole.Administrator], + UserRoles = _sessionUserService.Roles + }); + } +} diff --git a/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs new file mode 100644 index 0000000..1e40663 --- /dev/null +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryHandler.cs @@ -0,0 +1,51 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using AutoMapper; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Extensions; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; + +public class GetRegionsPageQueryHandler : + IRequestHandler> +{ + private readonly UnitOfWork _unitOfWork; + private readonly IMapper _mapper; + + public GetRegionsPageQueryHandler( + UnitOfWork unitOfWork, + IMapper mapper) + { + _unitOfWork = unitOfWork; + _mapper = mapper; + } + + public async Task> Handle( + GetRegionsPageQuery request, + CancellationToken cancellationToken) + { + var paginatedList = await _unitOfWork.RegionRepository.GetPageAsync( + e => + (e.Name.ToLower().Contains(request.Search.ToLower()) || + e.Country.Name.ToLower().Contains(request.Search.ToLower())) && + (request.CountryUuid != null + ? e.Country.Guid == request.CountryUuid + : true), + e => e.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/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs new file mode 100644 index 0000000..e075ef5 --- /dev/null +++ b/src/Application/Regions/Queries/GetRegionsPage/GetRegionsPageQueryValidator.cs @@ -0,0 +1,43 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using FluentValidation; +using Microsoft.Extensions.Localization; + +namespace cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; + +public class GetRegionsPageQueryValidator : AbstractValidator +{ + public GetRegionsPageQueryValidator( + IStringLocalizer localizer, + CultureService 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/Regions/RegionDto.cs b/src/Application/Regions/RegionDto.cs new file mode 100644 index 0000000..c4ce975 --- /dev/null +++ b/src/Application/Regions/RegionDto.cs @@ -0,0 +1,24 @@ +using cuqmbr.TravelGuide.Application.Common.Mappings; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Application.Regions; + +public sealed class RegionDto : IMapFrom +{ + public Guid Uuid { get; set; } + + public string Name { get; set; } + + public Guid CountryUuid { 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.Country.Guid)); + } +} diff --git a/src/Application/Regions/ViewModels/GetRegionsPageFilterViewModel.cs b/src/Application/Regions/ViewModels/GetRegionsPageFilterViewModel.cs new file mode 100644 index 0000000..974c7ab --- /dev/null +++ b/src/Application/Regions/ViewModels/GetRegionsPageFilterViewModel.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Application.Regions.ViewModels; + +public sealed class GetRegionsPageFilterViewModel +{ + public Guid? CountryUuid { get; set; } +} diff --git a/src/Application/Resources/Localization/en-US.json b/src/Application/Resources/Localization/en-US.json new file mode 100644 index 0000000..481cb0f --- /dev/null +++ b/src/Application/Resources/Localization/en-US.json @@ -0,0 +1,46 @@ +{ + "FluentValidation": { + "MaximumLength": "Must less than {0:G} characters.", + "NotEmpty": "Must not be empty.", + "GreaterThanOrEqualTo": "Must be greater than or equal to {0:G}.", + "LessThanOrEqualTo": "Must be less than or equal to {0:G}." + }, + "ExceptionHandling": { + "ValidationException": { + "Title": "One or more validation errors occurred.", + "Detail": "Provided data doesn't satisfy validation requirements." + }, + "RegistrationException": { + "Title": "Registration failed.", + "Detail": "Email already registered." + }, + "UnAuthorizedException": { + "Title": "Unauthenticated access prevented.", + "Detail": "Request lacks valid authentication credentials for the target resource." + }, + "AithenticationException": { + "Title": "Authentication failed.", + "Detail": "Check provided credentials validity." + }, + "LoginException": { + "Title": "Login failed.", + "Detail": "Provided email and/or password are invalid." + }, + "ForbiddenException": { + "Title": "Unauthorized access prevented.", + "Detail": "You have not enough privileges to perform the request." + }, + "DuplicateEntityException": { + "Title": "Entity already exists.", + "Detail": "Duplicates not allowed." + }, + "NotFoundException": { + "Title": "One or more resources was not found.", + "Detail": "Check validity of input data." + }, + "UnhandledException": { + "Title": "One or more internal server errors occurred.", + "Detail": "Report this error to service's support team." + } + } +} diff --git a/src/Application/Resources/Localization/uk-UA.json b/src/Application/Resources/Localization/uk-UA.json new file mode 100644 index 0000000..740082d --- /dev/null +++ b/src/Application/Resources/Localization/uk-UA.json @@ -0,0 +1,46 @@ +{ + "FluentValidation": { + "MaximumLength": "Повинно бути менше ніж {0:G} символів.", + "NotEmpty": "Не повинно бути порожнім.", + "GreaterThanOrEqualTo": "Повинно бути більше або дорівнювати {0:G}.", + "LessThanOrEqualTo": "Повинно бути менше або дорівнювати {0:G}." + }, + "ExceptionHandling": { + "ValidationException": { + "Title": "Виникла одна або декілька помилок валідації.", + "Detail": "Надані дані не задовольняють вимогам валідації." + }, + "RegistrationException": { + "Title": "Реєстрація не вдалася.", + "Detail": "Електронна пошта вже зареєстрована." + }, + "UnAuthorizedException": { + "Title": "Доступ без автентифікації заблоковано.", + "Detail": "Запит не містить дійсних автентифікаційних даних для цільового ресурсу." + }, + "AithenticationException": { + "Title": "Автентифікація не вдалася.", + "Detail": "Перевірте правильність наданих облікових даних." + }, + "LoginException": { + "Title": "Вхід не вдалий.", + "Detail": "Надані електронна пошта та/або пароль недійсні." + }, + "ForbiddenException": { + "Title": "Доступ заборонено.", + "Detail": "У вас недостатньо прав для виконання запиту." + }, + "DuplicateEntityException": { + "Title": "Об’єкт вже існує.", + "Detail": "Дублювання не дозволяється." + }, + "NotFoundException": { + "Title": "Один або декілька ресурсів не знайдено.", + "Detail": "Перевірте правильність вхідних даних." + }, + "UnhandledException": { + "Title": "Виникла одна або декілька внутрішніх помилок сервера.", + "Detail": "Повідомте про цю помилку службі підтримки сервісу." + } + } +} diff --git a/src/Application/packages.lock.json b/src/Application/packages.lock.json new file mode 100644 index 0000000..f56b518 --- /dev/null +++ b/src/Application/packages.lock.json @@ -0,0 +1,178 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "AspNetCore.Localizer.Json": { + "type": "Direct", + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", + "dependencies": { + "Microsoft.AspNetCore.Components": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Localization": "9.0.0" + } + }, + "AutoMapper": { + "type": "Direct", + "requested": "[14.0.0, )", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "FluentValidation": { + "type": "Direct", + "requested": "[11.11.0, )", + "resolved": "11.11.0", + "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" + }, + "MediatR": { + "type": "Direct", + "requested": "[12.4.1, )", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "MediatR.Behaviors.Authorization": { + "type": "Direct", + "requested": "[12.2.0, )", + "resolved": "12.2.0", + "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", + "dependencies": { + "MediatR": "12.4.1", + "MediatR.Contracts": "2.0.1" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "System.Linq.Dynamic.Core": { + "type": "Direct", + "requested": "[1.6.2, )", + "resolved": "1.6.2", + "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "9.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" + }, + "Microsoft.Extensions.Localization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" + }, + "domain": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/Configuration/Application/Configuration.cs b/src/Configuration/Application/Configuration.cs new file mode 100644 index 0000000..cfbd70c --- /dev/null +++ b/src/Configuration/Application/Configuration.cs @@ -0,0 +1,94 @@ +using System.Globalization; +using System.Text; +using AspNetCore.Localizer.Json.Commons; +using AspNetCore.Localizer.Json.Extensions; +using cuqmbr.TravelGuide.Application; +using cuqmbr.TravelGuide.Application.Common.Behaviours; +using FluentValidation; +using MediatR; +using MediatR.Behaviors.Authorization.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using cuqmbr.TravelGuide.Application.Common.Authorization; + +namespace cuqmbr.TravelGuide.Configuration.Application; + +public static class Configuration +{ + public static IServiceCollection ConfigureApplication( + this IServiceCollection services) + { + var configurationOptions = services.BuildServiceProvider().GetService< + IOptions>() + .Value; + + return services + .AddLocalization(configurationOptions.Localization) + .AddFluentValidation() + .AddAutoMapper() + .AddMediatR(); + } + + private static IServiceCollection AddFluentValidation( + this IServiceCollection services) + { + return services.AddValidatorsFromAssembly( + typeof(cuqmbr.TravelGuide.Application.ConfigurationOptions).Assembly); + } + + private static IServiceCollection AddAutoMapper( + this IServiceCollection services) + { + return services.AddAutoMapper( + typeof(cuqmbr.TravelGuide.Application.ConfigurationOptions).Assembly); + } + + private static IServiceCollection AddMediatR( + this IServiceCollection services) + { + services.AddMediatorAuthorization( + typeof(cuqmbr.TravelGuide.Application.ConfigurationOptions).Assembly, + configuration => + configuration.UseUnauthorizedResultHandlerStrategy( + new ForbiddenUnauthorizedResultHandler())); + + services.AddAuthorizersFromAssembly( + typeof(cuqmbr.TravelGuide.Application.ConfigurationOptions).Assembly); + + services.AddMediatR(configuration => + { + configuration.RegisterServicesFromAssembly( + typeof(cuqmbr.TravelGuide.Application.ConfigurationOptions) + .Assembly); + configuration.AddBehavior( + typeof(IPipelineBehavior<,>), + typeof(LoggingBehaviour<,>)); + configuration.AddBehavior( + typeof(IPipelineBehavior<,>), + typeof(ValidationBehaviour<,>)); + }); + + return services; + } + + private static IServiceCollection AddLocalization( + this IServiceCollection services, + LocalizationConfigurationOptions configuration) + { + return services.AddJsonLocalization(options => + { + options.ResourcesPath = "Resources"; + options.FileEncoding = Encoding.UTF8; + options.DefaultCulture = + CultureInfo.GetCultureInfo(configuration.DefaultCultureName); + options.DefaultUICulture = + CultureInfo.GetCultureInfo(configuration.DefaultCultureName); + options.LocalizationMode = + AspNetCore.Localizer.Json.JsonOptions.LocalizationMode.I18n; + options.CacheDuration = configuration.CacheDuration; + options.AssemblyHelper = new AssemblyHelper( + typeof(cuqmbr.TravelGuide.Application.ConfigurationOptions) + .Assembly); + }); + } +} diff --git a/src/Configuration/Configuration.csproj b/src/Configuration/Configuration.csproj new file mode 100644 index 0000000..7afac40 --- /dev/null +++ b/src/Configuration/Configuration.csproj @@ -0,0 +1,38 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/Configuration/Configuration/Configuration.cs b/src/Configuration/Configuration/Configuration.cs new file mode 100644 index 0000000..0c6eaa9 --- /dev/null +++ b/src/Configuration/Configuration/Configuration.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using PersistenceConfigurationOptions = + cuqmbr.TravelGuide.Persistence.ConfigurationOptions; +using ApplicationConfigurationOptions = + cuqmbr.TravelGuide.Application.ConfigurationOptions; +using IdentityConfigurationOptions = + cuqmbr.TravelGuide.Identity.ConfigurationOptions; + +namespace cuqmbr.TravelGuide.Configuration.Configuration; + +public static class Configuration +{ + public static IServiceCollection ConfigureConfiguration( + this IServiceCollection services, + string[] args) + { + var environment = + Environment.GetEnvironmentVariable("TravelGuide_Environment"); + + var configuration = new ConfigurationBuilder() + .AddJsonFile($"./appsettings.{environment}.json", optional: true) + .AddJsonFile($"./appsettings.json", optional: true) + .AddEnvironmentVariables(prefix: "TravelGuide_") + .AddCommandLine(args) + .Build(); + + services.AddOptions().Bind( + configuration.GetSection( + PersistenceConfigurationOptions.SectionName)); + + services.AddOptions().Bind( + configuration.GetSection( + ApplicationConfigurationOptions.SectionName)); + + services.AddOptions().Bind( + configuration.GetSection( + IdentityConfigurationOptions.SectionName)); + + return services; + } +} diff --git a/src/Configuration/Identity/Configuration.cs b/src/Configuration/Identity/Configuration.cs new file mode 100644 index 0000000..d9c1d2a --- /dev/null +++ b/src/Configuration/Identity/Configuration.cs @@ -0,0 +1,96 @@ +using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; +using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; +using Microsoft.EntityFrameworkCore; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Identity.Services; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using cuqmbr.TravelGuide.Identity.Exceptions; + +namespace cuqmbr.TravelGuide.Configuration.Identity; + +public static class Configuration +{ + public static IServiceCollection ConfigureIdentity( + this IServiceCollection services) + { + using var configurationServiceProvider = services.BuildServiceProvider(); + var configuration = configurationServiceProvider.GetService< + IOptions>() + .Value; + + // TODO: Make enum from available datastore types + + if (configuration.Datastore.Type.ToLower().Equals("postgresql")) + { + services.AddDbContext(options => + { + options.UseNpgsql( + configuration.Datastore.ConnectionString, + options => + { + options.MigrationsHistoryTable( + "ef_migrations_history", + configuration.Datastore.PartitionName); + }); + }); + + services + .AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + if (configuration.Datastore.Initialize) + { + using var dbContextServiceProvider = services.BuildServiceProvider(); + PostgreSqlInitializer.Initialize(dbContextServiceProvider); + } + } + else + { + throw new UnSupportedDatastoreException( + $"{configuration.Datastore.Type} datastore is not supported."); + } + + services + .AddScoped(); + + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = + JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = + JwtBearerDefaults.AuthenticationScheme; + options.DefaultScheme = + JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.IncludeErrorDetails = true; + options.SaveToken = true; + options.RequireHttpsMetadata = false; + options.TokenValidationParameters = + new TokenValidationParameters() + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidAudience = configuration.JsonWebToken.Audience, + ValidIssuer = configuration.JsonWebToken.Issuer, + ClockSkew = TimeSpan.Zero, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes( + configuration.JsonWebToken.IssuerSigningKey)) + }; + }); + + return services; + } +} diff --git a/src/Configuration/Infrastructure/Configuration.cs b/src/Configuration/Infrastructure/Configuration.cs new file mode 100644 index 0000000..ebbe6ce --- /dev/null +++ b/src/Configuration/Infrastructure/Configuration.cs @@ -0,0 +1,19 @@ +// using Microsoft.Extensions.Configuration; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace cuqmbr.TravelGuide.Configuration.Infrastructure; +// +// public static class Configuration +// { +// public static IServiceCollection ConfigureInfrastructure( +// this IServiceCollection services, +// IConfiguration configuration) +// { +// services +// .AddIdentity(configuration) +// .AddAuthenticationWithJwt(configuration) +// .AddServices(); +// +// return services; +// } +// } diff --git a/src/Configuration/Logging/Configuration.cs b/src/Configuration/Logging/Configuration.cs new file mode 100644 index 0000000..b89dddb --- /dev/null +++ b/src/Configuration/Logging/Configuration.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace cuqmbr.TravelGuide.Configuration.Logging; + +public static class Configuration +{ + public static IServiceCollection ConfigureLogging( + this IServiceCollection services) + { + var configurationOptions = services.BuildServiceProvider().GetService< + IOptions>() + .Value.Logging; + + // TODO: Make enum for available logger types + + services.AddLogging(configuration => + { + configuration.SetMinimumLevel( + Enum.Parse(configurationOptions.LogLevel)); + + if (configurationOptions.Type.Equals("JsonConsole")) + { + configuration.AddJsonConsole(configuration => + { + if (!String.IsNullOrEmpty( + configurationOptions.TimestampFormat)) + { + configuration.TimestampFormat = + configurationOptions.TimestampFormat; + configuration.UseUtcTimestamp = + configurationOptions.UseUtcTimestamp; + } + configuration.IncludeScopes = true; + }); + } + else if (configurationOptions.Type.Equals("SystemdConsole")) + { + configuration.AddSystemdConsole(configuration => + { + if (!String.IsNullOrEmpty( + configurationOptions.TimestampFormat)) + { + configuration.TimestampFormat = + $"[{configurationOptions.TimestampFormat}] "; + configuration.UseUtcTimestamp = true; + } + configuration.IncludeScopes = true; + }); + } + else if (configurationOptions.Type.Equals("SimpleConsole")) + { + configuration.AddSimpleConsole(configuration => + { + // TODO: Uncomment + // configuration.SingleLine = true; + if (!String.IsNullOrEmpty( + configurationOptions.TimestampFormat)) + { + configuration.TimestampFormat = + $"[{configurationOptions.TimestampFormat}] "; + configuration.UseUtcTimestamp = true; + } + configuration.IncludeScopes = true; + }); + } + }); + + return services; + } +} diff --git a/src/Configuration/Persistence/Configuration.cs b/src/Configuration/Persistence/Configuration.cs new file mode 100644 index 0000000..5aacbf3 --- /dev/null +++ b/src/Configuration/Persistence/Configuration.cs @@ -0,0 +1,81 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Persistence; +using cuqmbr.TravelGuide.Persistence.Exceptions; +using Microsoft.EntityFrameworkCore; +using cuqmbr.TravelGuide.Persistence.PostgreSql; +using cuqmbr.TravelGuide.Persistence.InMemory; + +namespace cuqmbr.TravelGuide.Configuration.Persistence; + +public static class Configuration +{ + public static IServiceCollection ConfigurePersistence( + this IServiceCollection services) + { + using var configurationServiceProvider = services.BuildServiceProvider(); + var configuration = configurationServiceProvider.GetService< + IOptions>() + .Value; + + // TODO: Make enum from available datastore types + + if (configuration.Type.ToLower().Equals("postgresql")) + { + services.AddDbContext(options => + { + options.UseNpgsql( + configuration.ConnectionString, + options => + { + options.MigrationsHistoryTable( + "ef_migrations_history", + configuration.PartitionName); + }); + }); + + services + .AddScoped() + .AddScoped(); + + if (configuration.Migrate) + { + using var dbContextServiceProvider = + services.BuildServiceProvider(); + var dbContext = + dbContextServiceProvider.GetService(); + PostgreSqlDbInitializer.Initialize(dbContext); + } + } + else if (configuration.Type.ToLower().Equals("inmemory")) + { + services.AddDbContext(options => + { + // Generate new name every time DI container is constructed. + // Allows separate integration tests to connect to + // different EventHandler databases. + // https://learn.microsoft.com/ef/core/testing/testing-without-the-database#in-memory-database-naming + var connectionString = $"InMemory{services.GetHashCode()}"; + options.UseInMemoryDatabase(connectionString); + }); + + services + .AddTransient() + .AddTransient(); + + // Initialization is not required. + } + else + { + throw new UnSupportedDatastoreException( + $"{configuration.Type} datastore is not supported."); + } + + // using var serviceProvider = services.BuildServiceProvider(); + // var unitOfWork = serviceProvider.GetService(); + // DbSeeder.Seed(unitOfWork); + + return services; + } +} diff --git a/src/Configuration/packages.lock.json b/src/Configuration/packages.lock.json new file mode 100644 index 0000000..7bd3fb8 --- /dev/null +++ b/src/Configuration/packages.lock.json @@ -0,0 +1,851 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "AspNetCore.Localizer.Json": { + "type": "Direct", + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", + "dependencies": { + "Microsoft.AspNetCore.Components": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Localization": "9.0.0" + } + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "Direct", + "requested": "[11.11.0, )", + "resolved": "11.11.0", + "contentHash": "viTKWaMbL3yJYgGI0DiCeavNbE9UPMWFVK2XS9nYXGbm3NDMd0/L5ER4wBzmTtW3BYh3SrlSXm9RACiKZ6stlA==", + "dependencies": { + "FluentValidation": "11.11.0", + "Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0" + } + }, + "Microsoft.AspNetCore.Identity": { + "type": "Direct", + "requested": "[2.3.1, )", + "resolved": "2.3.1", + "contentHash": "JcQ4pNXg+IISfcR95jeO2ZRt38N67MrUEj28HBmwfqD96BUyw4S54tQhrBmCOyPlf2vgNvSz/tsGAG7EgC0yRg==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Cookies": "2.3.0", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.Identity.Core": "2.3.0" + } + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "VHjUwjvN8UKjb8xtKJ/o+dc9tTeHOW3QzlfkzX3JrUspLkPIjwMdZCcw6eS4gsFjby0NFkcXBjHtrgTjVOfO5Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "YruNASPuiCjLOVxO09lpQT4e2OYvpsoD0e5NGEQKOcPCu143RDzWTNlpzcxhArBgAS0FPwQ+OEGZOWhwgWHvOA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.10", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "TbM2HElARG7z1gxwakdppmOkm1SykPqDcu3EF97daEwSb/+TXnRrFfJtF+5FWWxcsNhbRrmLfS2WszYcab7u1A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "2IGiG3FtVnD83IA6HYGuNei8dOw455C09yEhGl8bjcY6aGZgoC6yhYvDnozw8wlTowfoG9bxVrdTsr2ACZOYHg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "vVXI70CgT/dmXV3MM+n/BR2rLXEoAyoK0hQT+8MrbCMuJBiLRxnTtSrksNiASWCwOtxo/Tyy7CO8AGthbsYxnw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.FileExtensions": "9.0.4", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "cI0lQe0js65INCTCtAgnlVJWKgzgoRHVAW1B1zwCbmcliO4IZoTf92f1SYbLeLk7FzMJ/GlCvjLvJegJ6kltmQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Configuration": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "System.Text.Json": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g==" + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" + }, + "MediatR": { + "type": "Transitive", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "MediatR.Behaviors.Authorization": { + "type": "Transitive", + "resolved": "12.2.0", + "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", + "dependencies": { + "MediatR": "12.4.1", + "MediatR.Contracts": "2.0.1" + } + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "Microsoft.AspNetCore.Authentication": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "Tq6bxTOe65Ikh9dWVTEOqpvNqBGIQueO0J+zl2rQba0yP0YV66iYDkSz9MqTdRZftvJ2I5kMeRUm9Z2mjEAbUQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Core": "2.3.0", + "Microsoft.AspNetCore.DataProtection": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Extensions.WebEncoders": "8.0.11" + } + }, + "Microsoft.AspNetCore.Authentication.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "ve6uvLwKNRkfnO/QeN9M8eUJ49lCnWv/6/9p6iTEuiI6Rtsz+myaBAjdMzLuTViQY032xbTF5AdZF5BJzJJyXQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.AspNetCore.Authentication.Cookies": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "w3JPWHreXJ/Uv9CLkQtGCLwTbxZKY+94QPVi1RxcMuBTyRp+C9SdynznHEjnHWnw6QFNEHnBuHmWW3OYrvbpEQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authentication.Core": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "gnLnKGawBjqBnU9fEuel3VcYAARkjyONAliaGDfMc8o8HBtfh+HrOPEoR8Xx4b2RnMb7uxdBDOvEAC7sul79ig==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "9.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + } + }, + "Microsoft.AspNetCore.DataProtection": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "C+FhGaA8ekrfes0Ujhtkhk74Bpkt6Zt+NrMaGrCWBqW1LFzqw/pXDbMbpcAyI9hbYgZfC6+t01As4LGXbdxG4A==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0", + "Microsoft.AspNetCore.DataProtection.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Win32.Registry": "4.5.0", + "System.Security.Cryptography.Xml": "8.0.2", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Microsoft.AspNetCore.DataProtection.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "71GdtUkVDagLsBt+YatfzUItnbT2vIjHxWySNE2MkgIDhqT3g4sNNxOj/0PlPTpc1+mG3ZwfUoZ61jIt1wPw7g==" + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4ivq53W2k6Nj4eez9wc81ytfGj6HR1NaZJCpOrvghJo9zHuQF57PLhPoQH5ItyCpHXnrN/y7yJDUm+TGYzrx0w==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "F5iHx7odAbFKBV1DNPDkFFcVmD5Tk7rk+tYm3LMQxHEFFdjlg5QcYb5XhHAefl5YaaPeG6ad+/ck8kSG3/D6kw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "I9azEG2tZ4DDHAFgv+N38e6Yhttvf+QjE2j2UYyCACE7Swm5/0uoihCMWZ87oOZYeqiEFSxbsfpT71OYHe2tpw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.AspNetCore.WebUtilities": "2.3.0", + "Microsoft.Extensions.ObjectPool": "8.0.11", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Net.Http.Headers": "2.3.0" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "EY2u/wFF5jsYwGXXswfQWrSsFPmiXsniAlUWo3rv/MGYf99ZFsENDnZcQP6W3c/+xQmQXq0NauzQ7jyy+o1LDQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Identity.Stores": "9.0.4" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", + "dependencies": { + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "rnVGier1R0w9YEAzxOlUl8koFwq4QLwuYKiJN6NFqbCNCPrRLGW3f7x0OtL/Sp1KBMVhgffUIP6jWPppzhpa2Q==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "+5IAX0aicQYCRfN4pAjad+JPwdEYoVEM3Z1Cl8/EiEv3FVHQHdd8TJQpQIslQDDQS/UsUMb0MsOXwqOh+TJtRw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.4", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E0pkWzI0liqu2ogqJ1kohk2eGkYRhf5tI75HGF6IQDARsshY/0w+prGyLvNuUeV7B8I7vYQZ4CzAKYKxw7b9gQ==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cMsm1O7g9X5qbB2wjHf3BVVvGwkG+zeXQ+M91I1Bm6RfylFMImqBPzs0+vmuef7fPxr2yOzPhIfJ2wQJfmtaSw==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "OjJ+xh/wQff5b0wiC3SPvoQqTA2boZeJQf+15+3+OJPtjBKzvxuwr25QRIu1p1t+K8ryQ8pzaoZ7eOpXfNzVGA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "96NFbmjcZsO9HkSdWAwh5tn/7LKIu7cLW+zubyGV1BR1w8xpcqPXZcTW4S/0eA0d9BxyFnH8tSDRjUerWGoU/Q==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "9.0.4", + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UY864WQ3AS2Fkc8fYLombWnjrXwYt+BEHHps0hY4sxlgqaVW06AxbpgRZjfYf8PyRbplJqruzZDB/nSLT+7RLQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4", + "Microsoft.Extensions.FileProviders.Physical": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "gQN2o/KnBfVk6Bd71E2YsvO5lsqrqHmaepDGk+FB/C4aiQY9B0XKKNKfl5/TqcNOs9OEithm4opiMHAErMFyEw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "qkQ9V7KFZdTWNThT7ke7E/Jad38s46atSs3QUYZB8f3thBTrcrousdY4Y/tyCtcH5YjsPSiByjuN+L8W/ThMQg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4", + "Microsoft.Extensions.FileSystemGlobbing": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "05Lh2ItSk4mzTdDWATW9nEcSybwprN8Tz42Fs5B+jwdXUpauktdAQUI1Am4sUQi2C63E5hvQp8gXvfwfg9mQGQ==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.Identity.Core": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.Extensions.Localization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "/kF+rSnoo3/nIwGzWsR4RgBnoTOdZ3lzz2qFRyp/GgaNid4j6hOAQrs/O+QHXhlcAdZxjg37MvtIE+pAvIgi9g==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" + }, + "Microsoft.Extensions.WebEncoders": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "EwF+KaQzTa/MoIm8gciABL6xeeiGKowqyam+lPYWukTppwch1P3QeL8CpgtLs8kIWuEowpAAUrVfP1kyZsZgqg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.8.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.8.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.IdentityModel.Logging": "8.8.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "/M0wVg6tJUOHutWD3BMOUVZAioJVXe0tCpFiovzv0T9T12TBf4MnaHP0efO8TCr1a6O9RZgQeZ9Gdark8L9XdA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", + "dependencies": { + "System.Security.AccessControl": "4.5.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.10", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.10" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "System.Linq.Dynamic.Core": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.0.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" + }, + "application": { + "type": "Project", + "dependencies": { + "AspNetCore.Localizer.Json": "[1.0.1, )", + "AutoMapper": "[14.0.0, )", + "Domain": "[1.0.0, )", + "FluentValidation": "[11.11.0, )", + "MediatR": "[12.4.1, )", + "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "System.Linq.Dynamic.Core": "[1.6.2, )" + } + }, + "domain": { + "type": "Project" + }, + "identity": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", + "Microsoft.AspNetCore.Identity": "[2.3.1, )", + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", + "Microsoft.Extensions.Options": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", + "Microsoft.IdentityModel.Tokens": "[8.8.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } + }, + "infrastructure": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )" + } + }, + "persistence": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[9.0.4, )", + "Microsoft.Extensions.Options": "[9.0.4, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/Domain/Domain.csproj b/src/Domain/Domain.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/src/Domain/Domain.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/src/Domain/Entities/Address.cs b/src/Domain/Entities/Address.cs new file mode 100644 index 0000000..81e017b --- /dev/null +++ b/src/Domain/Entities/Address.cs @@ -0,0 +1,24 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Address : EntityBase +{ + public string Name { get; set; } + + // TODO: Implement coordinates using NetTopologySuite + // public double Longitude { get; set; } + // + // public double Latitude { get; set; } + + // public VehicleType VehicleType { get; set; } + + + public long CityId { get; set; } + + public City City { get; set; } + + + public ICollection AddressRoutes { get; set; } +} + diff --git a/src/Domain/Entities/City.cs b/src/Domain/Entities/City.cs new file mode 100644 index 0000000..670739f --- /dev/null +++ b/src/Domain/Entities/City.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class City : EntityBase +{ + public string Name { get; set; } + + + public long RegionId { get; set; } + + public Region Region { get; set; } + + public ICollection
Addresses { get; set; } +} diff --git a/src/Domain/Entities/Country.cs b/src/Domain/Entities/Country.cs new file mode 100644 index 0000000..45fdabe --- /dev/null +++ b/src/Domain/Entities/Country.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Country : EntityBase +{ + public string Name { get; set; } + + public ICollection Regions { get; set; } +} diff --git a/src/Domain/Entities/EntityBase.cs b/src/Domain/Entities/EntityBase.cs new file mode 100644 index 0000000..f0020f0 --- /dev/null +++ b/src/Domain/Entities/EntityBase.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public abstract class EntityBase +{ + public long Id { get; set; } + + public Guid Guid { get; set; } = Guid.NewGuid(); +} diff --git a/src/Domain/Entities/Region.cs b/src/Domain/Entities/Region.cs new file mode 100644 index 0000000..2d90ec2 --- /dev/null +++ b/src/Domain/Entities/Region.cs @@ -0,0 +1,13 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Region : EntityBase +{ + public string Name { get; set; } + + + public long CountryId { get; set; } + + public Country Country { get; set; } + + public ICollection Cities { get; set; } +} diff --git a/src/Domain/Entities/Route.cs b/src/Domain/Entities/Route.cs new file mode 100644 index 0000000..8199c35 --- /dev/null +++ b/src/Domain/Entities/Route.cs @@ -0,0 +1,14 @@ +using cuqmbr.TravelGuide.Domain.Enums; + +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class Route : EntityBase +{ + public string Name { get; set; } + + // public VehicleType VehicleType { get; set; } + + + public ICollection RouteAddresses { get; set; } +} + diff --git a/src/Domain/Entities/RouteAddress.cs b/src/Domain/Entities/RouteAddress.cs new file mode 100644 index 0000000..f216cca --- /dev/null +++ b/src/Domain/Entities/RouteAddress.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Domain.Entities; + +public sealed class RouteAddress : EntityBase +{ + public short Order { get; set; } + + + public long AddressId { get; set; } + + public Address Address { get; set; } + + + public long RouteId { get; set; } + + public Route Route { get; set; } +} diff --git a/src/Domain/Enums/Enumeration.cs b/src/Domain/Enums/Enumeration.cs new file mode 100644 index 0000000..a7067d8 --- /dev/null +++ b/src/Domain/Enums/Enumeration.cs @@ -0,0 +1,79 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +using System.Reflection; + +public abstract class Enumeration : IEquatable> + where TEnum : Enumeration +{ + public static Dictionary Enumerations { get; private set; } = + CreateEnumerations(); + + protected Enumeration(int value, string name) + { + Value = value; + Name = name; + } + + public int Value { get; protected init; } + + public string Name { get; protected init; } = string.Empty; + + public static TEnum? FromValue(int value) + { + return + Enumerations.TryGetValue(value, out TEnum? enumeration) ? + enumeration : + default; + } + + public static TEnum? FromName(string name) + { + return Enumerations.Values.SingleOrDefault(v => v.Name == name); + } + + public bool Equals(Enumeration? other) + { + if (other is null) + { + return false; + } + + return + GetType() == other.GetType() && + Value == other.Value; + } + + public override bool Equals(object? obj) + { + return + obj is Enumeration other && + Equals(other); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public override string ToString() + { + return Name; + } + + private static Dictionary CreateEnumerations() + { + var enumerationType = typeof(TEnum); + + var fieldsForType = enumerationType + .GetFields( + BindingFlags.Public | + BindingFlags.Static | + BindingFlags.FlattenHierarchy) + .Where(fieldInfo => + enumerationType.IsAssignableFrom(fieldInfo.FieldType)) + .Select(fieldInfo => + (TEnum)fieldInfo.GetValue(default)!); + + return fieldsForType.ToDictionary(x => x.Value); + } +} diff --git a/src/Domain/Enums/VehicleType.cs b/src/Domain/Enums/VehicleType.cs new file mode 100644 index 0000000..a107370 --- /dev/null +++ b/src/Domain/Enums/VehicleType.cs @@ -0,0 +1,28 @@ +namespace cuqmbr.TravelGuide.Domain.Enums; + +// Do not forget to update the schema of your database when changing +// this class (if you use it with a database) + +public abstract class VehicleType : Enumeration +{ + public static readonly VehicleType Bus = new BusVehicleType(); + public static readonly VehicleType Train = new TrainVehicleType(); + public static readonly VehicleType Aircraft = new AircraftVehicleType(); + + protected VehicleType(int value, string name) : base(value, name) { } + + private sealed class BusVehicleType : VehicleType + { + public BusVehicleType() : base(0, "bus") { } + } + + private sealed class TrainVehicleType : VehicleType + { + public TrainVehicleType() : base(1, "train") { } + } + + private sealed class AircraftVehicleType : VehicleType + { + public AircraftVehicleType() : base(2, "aircraft") { } + } +} diff --git a/src/HttpApi/Controllers/AuthenticationController.cs b/src/HttpApi/Controllers/AuthenticationController.cs new file mode 100644 index 0000000..ecb2a62 --- /dev/null +++ b/src/HttpApi/Controllers/AuthenticationController.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Mvc; +using cuqmbr.TravelGuide.Application.Authenticaion; +using cuqmbr.TravelGuide.Application.Authenticaion.Queries.Login; +using cuqmbr.TravelGuide.Application.Authenticaion.Commands.Register; +using cuqmbr.TravelGuide.Application.Authenticaion.Commands.RenewAccessToken; +using cuqmbr.TravelGuide.Application.Authenticaion.Commands.RevokeRefreshToken; +using cuqmbr.TravelGuide.Application.Authenticaion.Commands + .RenewAccessTokenWithCookie; +using cuqmbr.TravelGuide.Application.Authenticaion.Commands + .RevokeRefreshTokenWithCookie; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("authentication")] +public class AuthenticationController : ControllerBase +{ + [HttpPost("register")] + public async Task Register( + [FromBody] RegisterCommand command, + CancellationToken cancellationToken) + { + await Mediator.Send(command, cancellationToken); + } + + [HttpPost("login")] + public async Task Login( + [FromBody] LoginQuery query, + CancellationToken cancellationToken) + { + return await Mediator.Send(query, cancellationToken); + } + + // [HttpPost("loginWithCookie")] + // public async Task LoginWithCookie( + // [FromBody] LoginQuery query, + // CancellationToken cancellationToken) + // { + // var tokens = await Mediator.Send(query, cancellationToken); + // + // HttpContext.Response.Cookies.Delete("refreshToken"); + // + // var cookieOptions = new CookieOptions() + // { + // Path = "/", + // Expires = DateTimeOffset.MaxValue, + // HttpOnly = true + // }; + // + // HttpContext.Response.Cookies.Append( + // "refreshToken", tokens.RefreshToken, cookieOptions); + // + // return tokens; + // } + + [HttpPost("renewAccessToken")] + public async Task RenewAccessToken( + [FromBody] RenewAccessTokenCommand command, + CancellationToken cancellationToken) + { + return await Mediator.Send(command, cancellationToken); + } + + // [HttpPost("renewAccessTokenWithCookie")] + // public async Task RenewAccessTokenWithCookie( + // [FromBody] RenewAccessTokenWithCookieCommand command, + // CancellationToken cancellationToken) + // { + // return await Mediator.Send(command, cancellationToken); + // } + + [HttpPost("revokeRefreshToken")] + public async Task RevokeRefreshToken( + [FromBody] RevokeRefreshTokenCommand command, + CancellationToken cancellationToken) + { + await Mediator.Send(command, cancellationToken); + } + + // [HttpPost("revokeRefreshTokenWithCookie")] + // public async Task RevokeRefreshTokenWithCookie( + // [FromBody] RevokeRefreshTokenWithCookieCommand command, + // CancellationToken cancellationToken) + // { + // await Mediator.Send(command, cancellationToken); + // HttpContext.Response.Cookies.Delete("refreshToken"); + // } +} diff --git a/src/HttpApi/Controllers/ControllerBase.cs b/src/HttpApi/Controllers/ControllerBase.cs new file mode 100644 index 0000000..569894c --- /dev/null +++ b/src/HttpApi/Controllers/ControllerBase.cs @@ -0,0 +1,15 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[ApiController] +[Produces("application/json")] +[Consumes("application/json")] +public abstract class ControllerBase : Microsoft.AspNetCore.Mvc.ControllerBase +{ + private IMediator _mediator; + protected IMediator Mediator => + _mediator ?? + HttpContext.RequestServices.GetService(); +} diff --git a/src/HttpApi/Controllers/CountriesController.cs b/src/HttpApi/Controllers/CountriesController.cs new file mode 100644 index 0000000..d40fa01 --- /dev/null +++ b/src/HttpApi/Controllers/CountriesController.cs @@ -0,0 +1,165 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Countries; +using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; +using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; +using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; +using cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; +using cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("countries")] +public class CountriesController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Create a country")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(CountryDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddCountryCommand command, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send(command, cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all countries")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get country by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CountryDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CountryDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetCountryQuery() { Guid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update country")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(CountryDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CountryDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateCountryCommand command, + CancellationToken cancellationToken) + { + command.Guid = uuid; + return await Mediator.Send(command, cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete country")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(CountryDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteCountryCommand() { Guid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/RegionsController.cs b/src/HttpApi/Controllers/RegionsController.cs new file mode 100644 index 0000000..1306916 --- /dev/null +++ b/src/HttpApi/Controllers/RegionsController.cs @@ -0,0 +1,181 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.ViewModels; +using cuqmbr.TravelGuide.Application.Regions; +using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; +using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; +using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; +using cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; +using cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; +using cuqmbr.TravelGuide.Application.Regions.ViewModels; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("regions")] +public class RegionsController : ControllerBase +{ + [HttpPost] + [SwaggerOperation("Create a region")] + [SwaggerResponse( + StatusCodes.Status201Created, "Object successfuly created", + typeof(RegionDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> Add( + [FromBody] AddRegionCommand command, + CancellationToken cancellationToken) + { + return StatusCode( + StatusCodes.Status201Created, + await Mediator.Send(command, cancellationToken)); + } + + [HttpGet] + [SwaggerOperation("Get a list of all regions")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", + typeof(PaginatedList))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task> GetPage( + [FromQuery] PageQuery pageQuery, [FromQuery] SearchQuery searchQuery, + [FromQuery] SortQuery sortQuery, + [FromQuery] GetRegionsPageFilterViewModel filterQuery, + CancellationToken cancellationToken) + { + return await Mediator.Send( + new GetRegionsPageQuery() + { + PageNumber = pageQuery.PageNumber, + PageSize = pageQuery.PageSize, + Search = searchQuery.Search, + Sort = sortQuery.Sort, + CountryUuid = filterQuery.CountryUuid + }, + cancellationToken); + } + + [HttpGet("{uuid:guid}")] + [SwaggerOperation("Get region by uuid")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(RegionDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Get( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + return await Mediator.Send(new GetRegionQuery() { Uuid = uuid }, + cancellationToken); + } + + [HttpPut("{uuid:guid}")] + [SwaggerOperation("Update region")] + [SwaggerResponse( + StatusCodes.Status200OK, "Request successful", typeof(RegionDto))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Object already exists", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(RegionDto))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Parent object not found", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Update( + [FromRoute] Guid uuid, + [FromBody] UpdateRegionCommand command, + CancellationToken cancellationToken) + { + command.Uuid = uuid; + return await Mediator.Send(command, cancellationToken); + } + + [HttpDelete("{uuid:guid}")] + [SwaggerOperation("Delete region")] + [SwaggerResponse(StatusCodes.Status204NoContent, "Request successful")] + [SwaggerResponse( + StatusCodes.Status400BadRequest, "Input data validation error", + typeof(HttpValidationProblemDetails))] + [SwaggerResponse( + StatusCodes.Status401Unauthorized, "Unauthorized to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status403Forbidden, + "Not enough privileges to perform an action", + typeof(ProblemDetails))] + [SwaggerResponse( + StatusCodes.Status404NotFound, "Object not found", typeof(RegionDto))] + [SwaggerResponse( + StatusCodes.Status500InternalServerError, "Internal server error", + typeof(ProblemDetails))] + public async Task Delete( + [FromRoute] Guid uuid, + CancellationToken cancellationToken) + { + await Mediator.Send( + new DeleteRegionCommand() { Uuid = uuid }, + cancellationToken); + return StatusCode(StatusCodes.Status204NoContent); + } +} diff --git a/src/HttpApi/Controllers/TestsController.cs b/src/HttpApi/Controllers/TestsController.cs new file mode 100644 index 0000000..4533d4e --- /dev/null +++ b/src/HttpApi/Controllers/TestsController.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +namespace cuqmbr.TravelGuide.HttpApi.Controllers; + +[Route("tests")] +public class TestsController : ControllerBase +{ + private readonly IStringLocalizer _localizer; + + public TestsController( + CultureService cultureService, + IStringLocalizer localizer) + { + _localizer = localizer; + } + + [HttpGet("getLocalizedString/{inputString}")] + public Task getLocalizedString( + [FromRoute] string inputString, + CancellationToken cancellationToken) + { + return Task.FromResult(_localizer[inputString]); + } +} diff --git a/src/HttpApi/HttpApi.csproj b/src/HttpApi/HttpApi.csproj new file mode 100644 index 0000000..8afd0fb --- /dev/null +++ b/src/HttpApi/HttpApi.csproj @@ -0,0 +1,38 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + true + + + + true + + + diff --git a/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs b/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs new file mode 100644 index 0000000..39c0018 --- /dev/null +++ b/src/HttpApi/Middlewares/GlobalExceptionHandlerMiddleware.cs @@ -0,0 +1,210 @@ +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; +using System.Reflection; + +namespace cuqmbr.TravelGuide.HttpApi.Middlewares; + +public class GlobalExceptionHandlerMiddleware : IMiddleware +{ + private readonly Dictionary> _exceptionHandlers; + private readonly ILogger _logger; + private readonly IStringLocalizer _localizer; + + public GlobalExceptionHandlerMiddleware( + ILogger logger, + IStringLocalizer localizer) + { + _exceptionHandlers = new() + { + // Request object validation + { typeof(ValidationException), HandleValidationException }, + // Authentication + // TODO: Remove UnAuthorizedException, isnt used + { typeof(UnAuthorizedException), HandleUnAuthorizedException }, + { typeof(AuthenticationException), HandleAuthenticationException }, + { typeof(RegistrationException), HandleRegistrationException }, + { typeof(LoginException), HandleLoginException }, + // Authorization + { typeof(ForbiddenException), HandleForbiddenException }, + // Data access + { typeof(DuplicateEntityException), HandleDuplicateEntityException }, + { typeof(NotFoundException), HandleNotFoundException }, + }; + + _logger = logger; + _localizer = localizer; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (Exception exception) + { + var exceptionType = exception.GetType(); + + if (_exceptionHandlers.ContainsKey(exceptionType)) + { + _logger.LogInformation( + "Interrupted with planned exception {ExceptionType}.", + exception.GetType()); + + context.Response.ContentType = "application/problem+json"; + + await _exceptionHandlers[exceptionType].Invoke(context, exception); + return; + } + + _logger.LogError( + exception, + "Interrupted with unhandled exception {ExceptionType}.", + exception.GetType()); + + await HandleUnhandledException(context, exception); + } + } + + private async Task HandleValidationException( + HttpContext context, + Exception exception) + { + var ex = (ValidationException)exception; + + context.Response.StatusCode = StatusCodes.Status400BadRequest; + + await context.Response.WriteAsJsonAsync(new HttpValidationProblemDetails(ex.Errors) + { + Status = StatusCodes.Status400BadRequest, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", + Title = _localizer["ExceptionHandling.ValidationException.Title"], + Detail = _localizer["ExceptionHandling.ValidationException.Detail"] + }); + } + + private async Task HandleUnAuthorizedException( + HttpContext context, + Exception exception) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status401Unauthorized, + Type = "https://datatracker.ietf.org/doc/html/rfc7235#section-3.1", + Title = _localizer["ExceptionHandling.UnAuthorizedException.Title"], + Detail = _localizer["ExceptionHandling.UnAuthorizedException.Detail"] + }); + } + + private async Task HandleAuthenticationException( + HttpContext context, + Exception exception) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status400BadRequest, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", + Title = _localizer["ExceptionHandling.AuthenticationException.Title"], + Detail = _localizer["ExceptionHandling.AuthenticationException.Detail"] + }); + } + + private async Task HandleRegistrationException( + HttpContext context, + Exception exception) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status400BadRequest, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", + Title = _localizer["ExceptionHandling.RegistrationException.Title"], + Detail = _localizer["ExceptionHandling.RegistrationException.Detail"] + }); + } + + private async Task HandleLoginException( + HttpContext context, + Exception exception) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status400BadRequest, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", + Title = _localizer["ExceptionHandling.LoginException.Title"], + Detail = _localizer["ExceptionHandling.LoginException.Detail"] + }); + } + + private async Task HandleForbiddenException( + HttpContext context, + Exception exception) + { + context.Response.StatusCode = StatusCodes.Status403Forbidden; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status403Forbidden, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.3", + Title = _localizer["ExceptionHandling.ForbiddenException.Title"], + Detail = _localizer["ExceptionHandling.ForbiddenException.Detail"] + }); + } + + private async Task HandleDuplicateEntityException( + HttpContext context, + Exception exception) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status400BadRequest, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.1", + Title = _localizer["ExceptionHandling.DuplicateEntityException.Title"], + Detail = _localizer["ExceptionHandling.DuplicateEntityException.Detail"] + }); + } + + private async Task HandleNotFoundException( + HttpContext context, + Exception exception) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status404NotFound, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4", + Title = _localizer["ExceptionHandling.NotFoundException.Title"], + Detail = _localizer["ExceptionHandling.NotFoundException.Detail"] + }); + } + + private async Task HandleUnhandledException(HttpContext context, Exception exception) + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + + await context.Response.WriteAsJsonAsync(new ProblemDetails() + { + Status = StatusCodes.Status500InternalServerError, + Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", + Title = _localizer["ExceptionHandling.UnhandledException.Title"], + Detail = _localizer["ExceptionHandling.UnhandledException.Detail"] + }); + } + + // class ProblemDetailsWithTraceId : ProblemDetails + // { + // public string TraceId { get; init; } = Activity.Current?.TraceId.ToString(); + // } +} + diff --git a/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs new file mode 100644 index 0000000..8b02796 --- /dev/null +++ b/src/HttpApi/Middlewares/ThreadCultureSetterMiddleware.cs @@ -0,0 +1,22 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.Globalization; + +namespace cuqmbr.TravelGuide.HttpApi.Middlewares; + +public class ThreadCultureSetterMiddleware : IMiddleware +{ + private readonly CultureService _cultureService; + + public ThreadCultureSetterMiddleware(CultureService cultureService) + { + _cultureService = cultureService; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + CultureInfo.DefaultThreadCurrentCulture = _cultureService.Culture; + CultureInfo.DefaultThreadCurrentUICulture = _cultureService.Culture; + + await next(context); + } +} diff --git a/src/HttpApi/Program.cs b/src/HttpApi/Program.cs new file mode 100644 index 0000000..cb281b3 --- /dev/null +++ b/src/HttpApi/Program.cs @@ -0,0 +1,120 @@ +using cuqmbr.TravelGuide.Configuration.Persistence; +using cuqmbr.TravelGuide.Configuration.Application; +using cuqmbr.TravelGuide.Configuration.Identity; +using cuqmbr.TravelGuide.Configuration.Configuration; +using cuqmbr.TravelGuide.Configuration.Logging; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.HttpApi.Services; +using cuqmbr.TravelGuide.HttpApi.Middlewares; +using cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; +using Swashbuckle.AspNetCore.SwaggerUI; +using MicroElements.Swashbuckle.FluentValidation.AspNetCore; +using Microsoft.OpenApi.Models; +using System.Reflection; + + +var builder = WebApplication.CreateBuilder(args); + +var services = builder.Services; + + +services.ConfigureConfiguration(args); + +services.ConfigureLogging(); + +services.ConfigurePersistence(); +services.ConfigureIdentity(); +// services.AddInfrastructure(); +services.ConfigureApplication(); + +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); + +services.AddControllers(); + + +services.AddEndpointsApiExplorer(); +services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo() + { + Version = "v1", + Title = "TravelGuide API", + Description = "This document describes TravelGuide API" + }); + + options.EnableAnnotations(); + + var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); + + options.OperationFilter(); + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Name = "Authorization", + Description = "Bearer Authorization with Json Web Token " + + "(https://datatracker.ietf.org/doc/html/rfc7519)", + Type = SecuritySchemeType.ApiKey + }); + + // Set Accept-Language header in Authorize window + options.OperationFilter(); + options.AddSecurityDefinition("Accept-Language", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Name = "Accept-Language", + Description = "Format: -. " + + "(https://datatracker.ietf.org/doc/html/rfc4646)", + Type = SecuritySchemeType.ApiKey + }); + + // Set Accept-TimeZone header in Authorize window + options.OperationFilter(); + options.AddSecurityDefinition("Accept-TimeZone", new OpenApiSecurityScheme + { + In = ParameterLocation.Header, + Name = "Accept-TimeZone", + Description = "Format: \\/\\. Time Zone name " + + "from IANA tz database (https://www.iana.org/time-zones).", + Type = SecuritySchemeType.ApiKey + }); +}); +services.AddFluentValidationRulesToSwagger(); + + +services.AddScoped(); +services.AddScoped(); + +services.AddHealthChecks(); + + + +var app = builder.Build(); + +app.UseCors(builder => builder + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader()); + +app.MapControllers(); + +app.UseSwagger(); +app.UseSwaggerUI(options => +{ + options.DocExpansion(DocExpansion.None); + options.DefaultModelRendering(ModelRendering.Model); + options.DefaultModelExpandDepth(0); + options.EnablePersistAuthorization(); +}); + +app.UseMiddleware(); +app.UseMiddleware(); + +app.MapHealthChecks("/health").WithOpenApi(); + + + +app.Run(); diff --git a/src/HttpApi/Properties/launchSettings.json b/src/HttpApi/Properties/launchSettings.json new file mode 100644 index 0000000..97084c0 --- /dev/null +++ b/src/HttpApi/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:8080" + } + } +} diff --git a/src/HttpApi/Services/AspNetCultureService.cs b/src/HttpApi/Services/AspNetCultureService.cs new file mode 100644 index 0000000..03388ab --- /dev/null +++ b/src/HttpApi/Services/AspNetCultureService.cs @@ -0,0 +1,45 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using System.Globalization; + +namespace cuqmbr.TravelGuide.HttpApi.Services; + +public sealed class AspNetCultureService : CultureService +{ + private readonly HttpContext _httpContext; + private const string DefaultCultureId = "en-US"; + + public AspNetCultureService(IHttpContextAccessor httpContextAccessor) + { + _httpContext = httpContextAccessor.HttpContext!; + } + + public CultureInfo Culture + { + get + { + string? cultureId = _httpContext.Request.Headers["Accept-Language"]; + + if (cultureId != null) + { + cultureId = cultureId.Split(',')[0]; + } + else + { + cultureId = DefaultCultureId; + } + + CultureInfo result = CultureInfo.GetCultureInfo(DefaultCultureId); + + try + { + return CultureInfo.GetCultureInfo(cultureId, true); + } + catch (CultureNotFoundException) { } + + CultureInfo.DefaultThreadCurrentCulture = result; + CultureInfo.DefaultThreadCurrentUICulture = result; + + return result; + } + } +} diff --git a/src/HttpApi/Services/AspNetSessionUserService.cs b/src/HttpApi/Services/AspNetSessionUserService.cs new file mode 100644 index 0000000..eed5afc --- /dev/null +++ b/src/HttpApi/Services/AspNetSessionUserService.cs @@ -0,0 +1,53 @@ +using System.IdentityModel.Tokens.Jwt; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; + +namespace cuqmbr.TravelGuide.HttpApi.Services; + +public sealed class AspNetSessionUserService : SessionUserService +{ + private readonly HttpContext _httpContext; + + public AspNetSessionUserService(IHttpContextAccessor httpContextAccessor) + { + _httpContext = httpContextAccessor.HttpContext; + } + + public int? Id + { + get + { + var claimValue = _httpContext.User.Claims + .FirstOrDefault(c => c.Properties + .Any(p => p.Value == JwtRegisteredClaimNames.Sub)) + ?.Value; + + var parsed = int.TryParse(claimValue, out var id); + + return parsed ? id : null; + } + } + + public Guid? Uuid => Guid.Parse(_httpContext.User.Claims + .FirstOrDefault(c => c.Properties + .Any(p => p.Value.Equals("uuid"))) + ?.Value); + + public string? Email => _httpContext.User.Claims + .FirstOrDefault(c => c.Properties + .Any(p => p.Value == JwtRegisteredClaimNames.Email)) + ?.Value; + + public ICollection Roles => _httpContext.User.Claims + .Where(c => c.Properties + .Any(p => p.Value == "roles")) + .Select(c => IdentityRole.FromName(c.Value)) + .ToArray() ?? default!; + + public string? AccessToken => + _httpContext.Request.Cookies["accessToken"] ?? + _httpContext.Request.Headers["Authorization"] + .ToString()?.Replace("Bearer ", ""); + public string? RefreshToken => + _httpContext.Request.Cookies["refreshToken"]; +} diff --git a/src/HttpApi/Services/AspNetTimeZoneService.cs b/src/HttpApi/Services/AspNetTimeZoneService.cs new file mode 100644 index 0000000..eddcfb9 --- /dev/null +++ b/src/HttpApi/Services/AspNetTimeZoneService.cs @@ -0,0 +1,30 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; + +namespace cuqmbr.TravelGuide.HttpApi.Services; + +public sealed class AspNetTimeZoneService : TimeZoneService +{ + private readonly HttpContext _httpContext; + + public AspNetTimeZoneService(IHttpContextAccessor httpContextAccessor) + { + _httpContext = httpContextAccessor.HttpContext!; + } + + public TimeZoneInfo TimeZone + { + get + { + string? tzId = _httpContext.Request.Headers["Accept-TimeZone"]; + + if (tzId == null) + { + tzId = "UTC"; + } + + var tzFound = TimeZoneInfo.TryFindSystemTimeZoneById(tzId, out var tz); + + return tzFound ? tz! : TimeZoneInfo.Utc; + } + } +} diff --git a/src/HttpApi/Swashbuckle/OperationFilters/AcceptLanguageHeaderOperationFilter.cs b/src/HttpApi/Swashbuckle/OperationFilters/AcceptLanguageHeaderOperationFilter.cs new file mode 100644 index 0000000..0aee357 --- /dev/null +++ b/src/HttpApi/Swashbuckle/OperationFilters/AcceptLanguageHeaderOperationFilter.cs @@ -0,0 +1,41 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; + +public class AcceptLanguageHeaderOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // TODO: Remove security requirements + operation.Security ??= new List(); + + var acceptLanguage = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Accept-Language" + } + }; + + operation.Security.Add(new OpenApiSecurityRequirement + { + [acceptLanguage] = new List() + }); + + if (operation.Parameters == null) + operation.Parameters = new List(); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Accept-Language", + Description = "Format: -. " + + "(https://datatracker.ietf.org/doc/html/rfc4646)", + In = ParameterLocation.Header, + Schema = new OpenApiSchema { Type = "String" }, + Required = false + }); + } +} diff --git a/src/HttpApi/Swashbuckle/OperationFilters/AcceptTimeZoneHeaderOperationFilter.cs b/src/HttpApi/Swashbuckle/OperationFilters/AcceptTimeZoneHeaderOperationFilter.cs new file mode 100644 index 0000000..a23cfd0 --- /dev/null +++ b/src/HttpApi/Swashbuckle/OperationFilters/AcceptTimeZoneHeaderOperationFilter.cs @@ -0,0 +1,40 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; + +public class AcceptTimeZoneHeaderOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // TODO: Remove security requirements + operation.Security ??= new List(); + + var acceptTimeZone = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Accept-TimeZone" + } + }; + + operation.Security.Add(new OpenApiSecurityRequirement + { + [acceptTimeZone] = new List() + }); + + if (operation.Parameters == null) + operation.Parameters = new List(); + + operation.Parameters.Add(new OpenApiParameter + { + Name = "Accept-TimeZone", + Description = "Format: \\/\\. Time Zone name " + + "from IANA tz database (https://www.iana.org/time-zones).", + In = ParameterLocation.Header, + Schema = new OpenApiSchema { Type = "String" }, + Required = false + }); + } +} diff --git a/src/HttpApi/Swashbuckle/OperationFilters/AuthorizationHeaderOperationFilter.cs b/src/HttpApi/Swashbuckle/OperationFilters/AuthorizationHeaderOperationFilter.cs new file mode 100644 index 0000000..28419f1 --- /dev/null +++ b/src/HttpApi/Swashbuckle/OperationFilters/AuthorizationHeaderOperationFilter.cs @@ -0,0 +1,26 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace cuqmbr.TravelGuide.HttpApi.Swashbuckle.OperationFilters; + +public class AuthorizationHeaderOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + operation.Security ??= new List(); + + var authorization = new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }; + + operation.Security.Add(new OpenApiSecurityRequirement + { + [authorization] = new List() + }); + } +} diff --git a/src/HttpApi/appsettings.Development.json b/src/HttpApi/appsettings.Development.json new file mode 100644 index 0000000..982fe9d --- /dev/null +++ b/src/HttpApi/appsettings.Development.json @@ -0,0 +1,40 @@ +{ + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://localhost:4300" + } + } + }, + "Application": { + "Logging": { + "Type": "SimpleConsole", + "LogLevel": "Information", + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss.fffK", + "UseUtcTimestamp": true + }, + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000", + "PartitionName": "application" + }, + "Localization": { + "DefaultCultureName": "en-US", + "CacheDuration": "00:30:00" + } + }, + "Identity": { + "Datastore": { + "Type": "postgresql", + "ConnectionString": "Host=127.0.0.1:5432;Database=travel_guide;Username=postgres;Password=0000", + "PartitionName": "identity" + }, + "JsonWebToken": { + "Issuer": "https://api.travel-guide.cuqmbr.xyz", + "Audience": "https://travel-guide.cuqmbr.xyz", + "IssuerSigningKey": "a2c98dec80787a4e85ffb5bcbc24f7e4cc014d8a4fe43e9520480a50759164bc", + "AccessTokenValidity": "24:00:00", + "RefreshTokenValidity": "72:00:00" + } + } +} diff --git a/src/HttpApi/packages.lock.json b/src/HttpApi/packages.lock.json new file mode 100644 index 0000000..f7cac22 --- /dev/null +++ b/src/HttpApi/packages.lock.json @@ -0,0 +1,1138 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "AspNetCore.Localizer.Json": { + "type": "Direct", + "requested": "[1.0.1, )", + "resolved": "1.0.1", + "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", + "dependencies": { + "Microsoft.AspNetCore.Components": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Localization": "9.0.0" + } + }, + "MicroElements.Swashbuckle.FluentValidation": { + "type": "Direct", + "requested": "[6.1.0, )", + "resolved": "6.1.0", + "contentHash": "VzqApLPY8xIqXDvWqRuvoDYEoCHII42c4LgvLO3BikKoIVcECD+ZSG727I7yPZ/J07VEoa8aJddoqUtSm4E4gw==", + "dependencies": { + "FluentValidation": "[10.0.0, 12.0.0)", + "MicroElements.OpenApi.FluentValidation": "6.1.0", + "Swashbuckle.AspNetCore.SwaggerGen": "[6.3.0, 8.0.0)" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "GfZWPbZz1aAtEO3wGCkpeyRc0gzr/+VRHnUgY/YjqVPDlHbeKWCXw3IxKarQdo9myC2O1QBf652Mo50QqbXYRg==", + "dependencies": { + "Microsoft.OpenApi": "1.6.17" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "0NdtmsbYfMr2HyF+W6L+kPaHJl1nAmFjWj0MfI5G+CFeWZxDwltQxzzwSmZQ4QhS5z8zjczGXwHZ8e3iFaoiXA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.8.3", + "Microsoft.Build.Locator": "1.7.8", + "Microsoft.CodeAnalysis.CSharp": "4.8.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "4.8.0", + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Mono.TextTemplating": "3.0.0", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.Extensions.Options": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Swashbuckle.AspNetCore": { + "type": "Direct", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "HJHexmU0PiYevgTLvKjYkxEtclF2w4O7iTd3Ef3p6KeT0kcYLpkFVgCw6glpGS57h8769anv8G+NFi9Kge+/yw==", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "6.0.5", + "Swashbuckle.AspNetCore.Swagger": "8.1.1", + "Swashbuckle.AspNetCore.SwaggerGen": "8.1.1", + "Swashbuckle.AspNetCore.SwaggerUI": "8.1.1" + } + }, + "Swashbuckle.AspNetCore.Annotations": { + "type": "Direct", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "7HPeJGAs2VikmUOUHjmXo657FhUEuwajjgUmLTVrzGHo4tS1Io29cyMMfMDp4eAnXnY88jMa4MwG00xhWRgIDg==", + "dependencies": { + "Swashbuckle.AspNetCore.SwaggerGen": "8.1.1" + } + }, + "Swashbuckle.AspNetCore.Swagger": { + "type": "Direct", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "h+8D5jQtnl6X4f2hJQwf0Khj0SnCQANzirCELjXJ6quJ4C1aNNCvJrAsQ+4fOKAMqJkvW48cKj79ftG+YoGcRg==", + "dependencies": { + "Microsoft.OpenApi": "1.6.23" + } + }, + "Swashbuckle.AspNetCore.SwaggerGen": { + "type": "Direct", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "2EuPzXSNleOOzYvziERWRLnk1Oz9i0Z1PimaUFy1SasBqeV/rG+eMfwFAMtTaf4W6gvVOzRcUCNRHvpBIIzr+A==", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "8.1.1" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Direct", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "GDLX/MpK4oa2nYC1N/zN2UidQTtVKLPF6gkdEmGb0RITEwpJG9Gu8olKqPYnKqVeFn44JZoCS0M2LGRKXP8B/A==" + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "viTKWaMbL3yJYgGI0DiCeavNbE9UPMWFVK2XS9nYXGbm3NDMd0/L5ER4wBzmTtW3BYh3SrlSXm9RACiKZ6stlA==", + "dependencies": { + "FluentValidation": "11.11.0", + "Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "MediatR": { + "type": "Transitive", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "MediatR.Behaviors.Authorization": { + "type": "Transitive", + "resolved": "12.2.0", + "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", + "dependencies": { + "MediatR": "12.4.1", + "MediatR.Contracts": "2.0.1" + } + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "MicroElements.OpenApi.FluentValidation": { + "type": "Transitive", + "resolved": "6.1.0", + "contentHash": "qJPAI3bL70ND6fIi4bGqQf/lpV9wUod23R7JVhVVYRonoT/ZGmPBjMbO//IPPy3yP2F21iMX05brYjyU4/WqwQ==", + "dependencies": { + "FluentValidation": "[10.0.0, 12.0.0)", + "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.AspNetCore.Authentication": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "Tq6bxTOe65Ikh9dWVTEOqpvNqBGIQueO0J+zl2rQba0yP0YV66iYDkSz9MqTdRZftvJ2I5kMeRUm9Z2mjEAbUQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Core": "2.3.0", + "Microsoft.AspNetCore.DataProtection": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Extensions.WebEncoders": "8.0.11" + } + }, + "Microsoft.AspNetCore.Authentication.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "ve6uvLwKNRkfnO/QeN9M8eUJ49lCnWv/6/9p6iTEuiI6Rtsz+myaBAjdMzLuTViQY032xbTF5AdZF5BJzJJyXQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.AspNetCore.Authentication.Cookies": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "w3JPWHreXJ/Uv9CLkQtGCLwTbxZKY+94QPVi1RxcMuBTyRp+C9SdynznHEjnHWnw6QFNEHnBuHmWW3OYrvbpEQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authentication.Core": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "gnLnKGawBjqBnU9fEuel3VcYAARkjyONAliaGDfMc8o8HBtfh+HrOPEoR8Xx4b2RnMb7uxdBDOvEAC7sul79ig==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "9.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + } + }, + "Microsoft.AspNetCore.DataProtection": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "C+FhGaA8ekrfes0Ujhtkhk74Bpkt6Zt+NrMaGrCWBqW1LFzqw/pXDbMbpcAyI9hbYgZfC6+t01As4LGXbdxG4A==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0", + "Microsoft.AspNetCore.DataProtection.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Win32.Registry": "4.5.0", + "System.Security.Cryptography.Xml": "8.0.2", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Microsoft.AspNetCore.DataProtection.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "71GdtUkVDagLsBt+YatfzUItnbT2vIjHxWySNE2MkgIDhqT3g4sNNxOj/0PlPTpc1+mG3ZwfUoZ61jIt1wPw7g==" + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4ivq53W2k6Nj4eez9wc81ytfGj6HR1NaZJCpOrvghJo9zHuQF57PLhPoQH5ItyCpHXnrN/y7yJDUm+TGYzrx0w==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "F5iHx7odAbFKBV1DNPDkFFcVmD5Tk7rk+tYm3LMQxHEFFdjlg5QcYb5XhHAefl5YaaPeG6ad+/ck8kSG3/D6kw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "I9azEG2tZ4DDHAFgv+N38e6Yhttvf+QjE2j2UYyCACE7Swm5/0uoihCMWZ87oOZYeqiEFSxbsfpT71OYHe2tpw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.AspNetCore.WebUtilities": "2.3.0", + "Microsoft.Extensions.ObjectPool": "8.0.11", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Net.Http.Headers": "2.3.0" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "EY2u/wFF5jsYwGXXswfQWrSsFPmiXsniAlUWo3rv/MGYf99ZFsENDnZcQP6W3c/+xQmQXq0NauzQ7jyy+o1LDQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.AspNetCore.Identity": { + "type": "Transitive", + "resolved": "2.3.1", + "contentHash": "JcQ4pNXg+IISfcR95jeO2ZRt38N67MrUEj28HBmwfqD96BUyw4S54tQhrBmCOyPlf2vgNvSz/tsGAG7EgC0yRg==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Cookies": "2.3.0", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.Identity.Core": "2.3.0" + } + }, + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Identity.Stores": "9.0.4" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", + "dependencies": { + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" + }, + "Microsoft.Build.Framework": { + "type": "Transitive", + "resolved": "17.8.3", + "contentHash": "NrQZJW8TlKVPx72yltGb8SVz3P5mNRk9fNiD/ao8jRSk48WqIIdCn99q4IjlVmPcruuQ+yLdjNQLL8Rb4c916g==" + }, + "Microsoft.Build.Locator": { + "type": "Transitive", + "resolved": "1.7.8", + "contentHash": "sPy10x527Ph16S2u0yGME4S6ohBKJ69WfjeGG/bvELYeZVmJdKjxgnlL8cJJJLGV/cZIRqSfB12UDB8ICakOog==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.4", + "System.Collections.Immutable": "7.0.0", + "System.Reflection.Metadata": "7.0.0", + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Bcl.AsyncInterfaces": "7.0.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "System.Composition": "7.0.0", + "System.IO.Pipelines": "7.0.0", + "System.Threading.Channels": "7.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "4.8.0", + "contentHash": "IEYreI82QZKklp54yPHxZNG9EKSK6nHEkeuf+0Asie9llgS1gp0V1hw7ODG+QyoB7MuAnNQHmeV1Per/ECpv6A==", + "dependencies": { + "Microsoft.Build.Framework": "16.10.0", + "Microsoft.CodeAnalysis.Common": "[4.8.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", + "System.Text.Json": "7.0.3" + } + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "rnVGier1R0w9YEAzxOlUl8koFwq4QLwuYKiJN6NFqbCNCPrRLGW3f7x0OtL/Sp1KBMVhgffUIP6jWPppzhpa2Q==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "+5IAX0aicQYCRfN4pAjad+JPwdEYoVEM3Z1Cl8/EiEv3FVHQHdd8TJQpQIslQDDQS/UsUMb0MsOXwqOh+TJtRw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.4", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E0pkWzI0liqu2ogqJ1kohk2eGkYRhf5tI75HGF6IQDARsshY/0w+prGyLvNuUeV7B8I7vYQZ4CzAKYKxw7b9gQ==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cMsm1O7g9X5qbB2wjHf3BVVvGwkG+zeXQ+M91I1Bm6RfylFMImqBPzs0+vmuef7fPxr2yOzPhIfJ2wQJfmtaSw==" + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "VHjUwjvN8UKjb8xtKJ/o+dc9tTeHOW3QzlfkzX3JrUspLkPIjwMdZCcw6eS4gsFjby0NFkcXBjHtrgTjVOfO5Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "OjJ+xh/wQff5b0wiC3SPvoQqTA2boZeJQf+15+3+OJPtjBKzvxuwr25QRIu1p1t+K8ryQ8pzaoZ7eOpXfNzVGA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "YruNASPuiCjLOVxO09lpQT4e2OYvpsoD0e5NGEQKOcPCu143RDzWTNlpzcxhArBgAS0FPwQ+OEGZOWhwgWHvOA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.10", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "96NFbmjcZsO9HkSdWAwh5tn/7LKIu7cLW+zubyGV1BR1w8xpcqPXZcTW4S/0eA0d9BxyFnH8tSDRjUerWGoU/Q==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "9.0.4", + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "6.0.5", + "contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "TbM2HElARG7z1gxwakdppmOkm1SykPqDcu3EF97daEwSb/+TXnRrFfJtF+5FWWxcsNhbRrmLfS2WszYcab7u1A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "2IGiG3FtVnD83IA6HYGuNei8dOw455C09yEhGl8bjcY6aGZgoC6yhYvDnozw8wlTowfoG9bxVrdTsr2ACZOYHg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UY864WQ3AS2Fkc8fYLombWnjrXwYt+BEHHps0hY4sxlgqaVW06AxbpgRZjfYf8PyRbplJqruzZDB/nSLT+7RLQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4", + "Microsoft.Extensions.FileProviders.Physical": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "vVXI70CgT/dmXV3MM+n/BR2rLXEoAyoK0hQT+8MrbCMuJBiLRxnTtSrksNiASWCwOtxo/Tyy7CO8AGthbsYxnw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.FileExtensions": "9.0.4", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "gQN2o/KnBfVk6Bd71E2YsvO5lsqrqHmaepDGk+FB/C4aiQY9B0XKKNKfl5/TqcNOs9OEithm4opiMHAErMFyEw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "qkQ9V7KFZdTWNThT7ke7E/Jad38s46atSs3QUYZB8f3thBTrcrousdY4Y/tyCtcH5YjsPSiByjuN+L8W/ThMQg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4", + "Microsoft.Extensions.FileSystemGlobbing": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "05Lh2ItSk4mzTdDWATW9nEcSybwprN8Tz42Fs5B+jwdXUpauktdAQUI1Am4sUQi2C63E5hvQp8gXvfwfg9mQGQ==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.Identity.Core": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.Extensions.Localization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "/kF+rSnoo3/nIwGzWsR4RgBnoTOdZ3lzz2qFRyp/GgaNid4j6hOAQrs/O+QHXhlcAdZxjg37MvtIE+pAvIgi9g==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cI0lQe0js65INCTCtAgnlVJWKgzgoRHVAW1B1zwCbmcliO4IZoTf92f1SYbLeLk7FzMJ/GlCvjLvJegJ6kltmQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Configuration": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" + }, + "Microsoft.Extensions.WebEncoders": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "EwF+KaQzTa/MoIm8gciABL6xeeiGKowqyam+lPYWukTppwch1P3QeL8CpgtLs8kIWuEowpAAUrVfP1kyZsZgqg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.8.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.8.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.IdentityModel.Logging": "8.8.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "/M0wVg6tJUOHutWD3BMOUVZAioJVXe0tCpFiovzv0T9T12TBf4MnaHP0efO8TCr1a6O9RZgQeZ9Gdark8L9XdA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==" + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "1.6.23", + "contentHash": "tZ1I0KXnn98CWuV8cpI247A17jaY+ILS9vvF7yhI0uPPEqF4P1d7BWL5Uwtel10w9NucllHB3nTkfYTAcHAh8g==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", + "dependencies": { + "System.Security.AccessControl": "4.5.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.10", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.10" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Convention": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0", + "System.Composition.TypedParts": "7.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", + "dependencies": { + "System.Composition.Runtime": "7.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", + "dependencies": { + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" + }, + "System.Linq.Dynamic.Core": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", + "dependencies": { + "System.Collections.Immutable": "7.0.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.0.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g==" + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" + }, + "application": { + "type": "Project", + "dependencies": { + "AspNetCore.Localizer.Json": "[1.0.1, )", + "AutoMapper": "[14.0.0, )", + "Domain": "[1.0.0, )", + "FluentValidation": "[11.11.0, )", + "MediatR": "[12.4.1, )", + "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "System.Linq.Dynamic.Core": "[1.6.2, )" + } + }, + "configuration": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "AspNetCore.Localizer.Json": "[1.0.1, )", + "Domain": "[1.0.0, )", + "FluentValidation.DependencyInjectionExtensions": "[11.11.0, )", + "Identity": "[1.0.0, )", + "Infrastructure": "[1.0.0, )", + "Microsoft.AspNetCore.Identity": "[2.3.1, )", + "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[9.0.4, )", + "Microsoft.Extensions.Configuration": "[9.0.4, )", + "Microsoft.Extensions.Configuration.CommandLine": "[9.0.4, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[9.0.4, )", + "Microsoft.Extensions.Configuration.Json": "[9.0.4, )", + "Microsoft.Extensions.DependencyInjection": "[9.0.4, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.Extensions.Logging.Console": "[9.0.4, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[9.0.4, )", + "Persistence": "[1.0.0, )", + "System.Text.Json": "[9.0.4, )" + } + }, + "domain": { + "type": "Project" + }, + "identity": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", + "Microsoft.AspNetCore.Identity": "[2.3.1, )", + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", + "Microsoft.Extensions.Options": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", + "Microsoft.IdentityModel.Tokens": "[8.8.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } + }, + "infrastructure": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )" + } + }, + "persistence": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[9.0.4, )", + "Microsoft.Extensions.Options": "[9.0.4, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } + } + } + } +} \ No newline at end of file diff --git a/src/Identity/ConfigurationOptions.cs b/src/Identity/ConfigurationOptions.cs new file mode 100644 index 0000000..bf10582 --- /dev/null +++ b/src/Identity/ConfigurationOptions.cs @@ -0,0 +1,34 @@ +namespace cuqmbr.TravelGuide.Identity; + +public sealed class ConfigurationOptions +{ + public static string SectionName { get; } = "Identity"; + + public Datastore Datastore { get; set; } = new(); + + public JsonWebToken JsonWebToken { get; set; } = new(); +} + +public sealed class Datastore +{ + public string Type { get; set; } = "inmemory"; + + public string ConnectionString { get; set; } = "InMemory"; + + public string PartitionName { get; set; } = "identity"; + + public bool Initialize { get; set; } = true; +} + +public sealed class JsonWebToken +{ + public string Issuer { get; set; } = "localhost"; + + public string Audience { get; set; } = "localhost"; + + public string IssuerSigningKey { get; set; } = "change-me"; + + public TimeSpan AccessTokenValidity { get; set; } = TimeSpan.FromMinutes(15); + + public TimeSpan RefreshTokenValidity { get; set; } = TimeSpan.FromDays(3); +} diff --git a/src/Identity/Exceptions/UnSupportedDatastoreException.cs b/src/Identity/Exceptions/UnSupportedDatastoreException.cs new file mode 100644 index 0000000..7366911 --- /dev/null +++ b/src/Identity/Exceptions/UnSupportedDatastoreException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Identity.Exceptions; + +public class UnSupportedDatastoreException : Exception +{ + public UnSupportedDatastoreException() + : base() { } + + public UnSupportedDatastoreException(string message) + : base(message) { } +} + diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj new file mode 100644 index 0000000..53c2cb1 --- /dev/null +++ b/src/Identity/Identity.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/Identity/IdentitySeeder.cs b/src/Identity/IdentitySeeder.cs new file mode 100644 index 0000000..15e7647 --- /dev/null +++ b/src/Identity/IdentitySeeder.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.Identity; +using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; +using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; +using Microsoft.Extensions.DependencyInjection; +using IdentityRoleEnum = cuqmbr.TravelGuide.Application.Common.Models + .IdentityRole; + +namespace cuqmbr.TravelGuide.Identity; + +public static class IdentitySeeder +{ + public static void Seed(IServiceProvider serviceProvider) + { + using var userManager = serviceProvider + .GetService>(); + userManager.UserValidators.Clear(); + userManager.PasswordValidators.Clear(); + + using var roleManager = serviceProvider + .GetService>(); + roleManager.RoleValidators.Clear(); + + // Seed Roles + foreach (var role in IdentityRoleEnum.Enumerations) + { + var roleString = role.Value.Name; + + var roleExists = roleManager.RoleExistsAsync(roleString).Result; + + if (roleExists) + { + continue; + } + + roleManager.CreateAsync(new IdentityRole() + { + Name = roleString, + ConcurrencyStamp = Guid.NewGuid().ToString("D") + }).Wait(); + } + + // Seed Identity + var users = new (string Email, string Password, IdentityRoleEnum[] Roles)[] + { + ("admin", "admin", new [] { IdentityRoleEnum.Administrator }), + }; + + foreach (var user in users) + { + + var userExists = userManager + .FindByEmailAsync(user.Email).Result is not null; + + if (userExists) + { + continue; + } + + var newUser = new IdentityUser + { + Email = user.Email, + NormalizedEmail = user.Email.ToUpper(), + EmailConfirmed = true, + SecurityStamp = Guid.NewGuid().ToString("D"), + RefreshTokens = default! + }; + + var hashedPassword + = userManager.PasswordHasher.HashPassword(newUser, user.Password); + newUser.PasswordHash = hashedPassword; + + userManager + .CreateAsync(newUser) + .Wait(); + + var userRoles = user.Roles.Select(x => x.Name); + + userManager + .AddToRolesAsync(newUser, userRoles) + .Wait(); + } + + } +} + diff --git a/src/Identity/Models/IdentityRole.cs b/src/Identity/Models/IdentityRole.cs new file mode 100644 index 0000000..b1d9ee4 --- /dev/null +++ b/src/Identity/Models/IdentityRole.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Identity.Models; + +public class IdentityRole : Microsoft.AspNetCore.Identity.IdentityRole +{ + public string Uuid { get; set; } = Guid.NewGuid().ToString(); +} diff --git a/src/Identity/Models/IdentityUser.cs b/src/Identity/Models/IdentityUser.cs new file mode 100644 index 0000000..ce9aad1 --- /dev/null +++ b/src/Identity/Models/IdentityUser.cs @@ -0,0 +1,8 @@ +namespace cuqmbr.TravelGuide.Identity.Models; + +public class IdentityUser : Microsoft.AspNetCore.Identity.IdentityUser +{ + public string Uuid { get; set; } = Guid.NewGuid().ToString(); + + public ICollection RefreshTokens { get; set; } +} diff --git a/src/Identity/Models/RefreshToken.cs b/src/Identity/Models/RefreshToken.cs new file mode 100644 index 0000000..a751096 --- /dev/null +++ b/src/Identity/Models/RefreshToken.cs @@ -0,0 +1,23 @@ +namespace cuqmbr.TravelGuide.Identity.Models; + +public class RefreshToken +{ + public int Id { get; set; } + + public string Value { get; set; } = null!; + + public DateTimeOffset CreationTimestamp { get; set; } + + public DateTimeOffset ExpirationTimestamp { get; set; } + + public DateTimeOffset? RevokationTimestamp { get; set; } + + public bool IsExpired => DateTimeOffset.UtcNow >= ExpirationTimestamp; + + public bool IsActive => RevokationTimestamp is null && !IsExpired; + + public int IdentityUserId { get; set; } + + public IdentityUser IdentityUser { get; set; } +} + diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs new file mode 100644 index 0000000..91c8229 --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleClaimConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; + +public class IdentityRoleClaimConfiguration : IEntityTypeConfiguration> +{ + public void Configure(EntityTypeBuilder> builder) + { + builder + .ToTable("role_claims"); + + builder + .Property(rc => rc.Id) + .HasColumnName("id"); + + builder + .Property(rc => rc.RoleId) + .HasColumnName("role_id"); + + builder + .Property(rc => rc.ClaimType) + .HasColumnName("claim_type"); + + builder + .Property(rc => rc.ClaimValue) + .HasColumnName("claim_value"); + } +} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs new file mode 100644 index 0000000..e8ee518 --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Configurations/IdentityRoleConfiguration.cs @@ -0,0 +1,34 @@ +using cuqmbr.TravelGuide.Identity.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; + +public class IdentityRoleConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("roles"); + + builder + .Property(r => r.Id) + .HasColumnName("id"); + + builder + .Property(r => r.Uuid) + .HasColumnName("uuid"); + + builder + .Property(r => r.Name) + .HasColumnName("name"); + + builder + .Property(r => r.NormalizedName) + .HasColumnName("normalized_name"); + + builder + .Property(r => r.ConcurrencyStamp) + .HasColumnName("concurrency_stamp"); + } +} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs new file mode 100644 index 0000000..863092f --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserClaimConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; + +public class IdentityUserClaimConfiguration : IEntityTypeConfiguration> +{ + public void Configure(EntityTypeBuilder> builder) + { + builder + .ToTable("user_claims"); + + builder + .Property(uc => uc.Id) + .HasColumnName("id"); + + builder + .Property(uc => uc.UserId) + .HasColumnName("user_id"); + + builder + .Property(uc => uc.ClaimType) + .HasColumnName("claim_type"); + + builder + .Property(uc => uc.ClaimValue) + .HasColumnName("claim_value"); + } +} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs new file mode 100644 index 0000000..e99835c --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserConfiguration.cs @@ -0,0 +1,128 @@ +using cuqmbr.TravelGuide.Identity.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; + +public class IdentityUserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("users"); + + builder + .Property(u => u.Id) + .HasColumnName("id"); + + builder + .Property(u => u.Uuid) + .HasColumnName("uuid"); + + builder + .Ignore(u => u.UserName); + + builder + .Ignore(u => u.NormalizedUserName); + + builder + .Property(u => u.Email) + .HasColumnName("email"); + + builder + .Property(u => u.NormalizedEmail) + .HasColumnName("normalized_email"); + + builder + .Property(u => u.EmailConfirmed) + .HasColumnName("email_confirmed"); + + builder + .Property(u => u.PasswordHash) + .HasColumnName("password_hash"); + + builder + .Property(u => u.SecurityStamp) + .HasColumnName("security_stamp"); + + builder + .Property(u => u.ConcurrencyStamp) + .HasColumnName("concurrency_stamp"); + + builder + .Ignore(u => u.PhoneNumber); + + builder + .Ignore(u => u.PhoneNumberConfirmed); + + builder + .Property(u => u.TwoFactorEnabled) + .HasColumnName("two_factor_enabled"); + + builder + .Property(u => u.LockoutEnabled) + .HasColumnName("lockout_enabled"); + + builder + .Property(u => u.LockoutEnd) + .HasColumnName("lockout_end"); + + builder + .Property(u => u.AccessFailedCount) + .HasColumnName("access_failed_count"); + + builder + .OwnsMany(u => u.RefreshTokens, + refreshToken => + { + refreshToken + .ToTable("user_refresh_tokens"); + + refreshToken + .HasKey(rt => rt.Id) + .HasName("id"); + + refreshToken + .WithOwner(rt => rt.IdentityUser) + .HasForeignKey(rt => rt.IdentityUserId) + .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); + + refreshToken + .Property(rt => rt.Id) + .HasColumnName("id") + .HasColumnType("int") + .IsRequired(); + + refreshToken + .Property(rt => rt.IdentityUserId) + .HasColumnName("user_id") + .HasColumnType("int") + .IsRequired(); + + refreshToken + .Property(rt => rt.Value) + .HasColumnName("value") + .HasColumnType("varchar(256)") + .IsRequired(); + + refreshToken + .Property(rt => rt.CreationTimestamp) + .HasColumnName("creation_timestamp_utc") + .HasColumnType("timestamptz") + .IsRequired(); + + refreshToken + .Property(rt => rt.ExpirationTimestamp) + .HasColumnName("expiration_timestamp_utc") + .HasColumnType("timestamptz") + .IsRequired(); + + refreshToken + .Property(rt => rt.RevokationTimestamp) + .HasColumnName("revokation_timestamp_utc") + .HasColumnType("timestamptz") + .IsRequired(false); + } + ); + } +} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs new file mode 100644 index 0000000..3464a1c --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserLoginConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; + +public class IdentityUserLoginConfiguration : IEntityTypeConfiguration> +{ + public void Configure(EntityTypeBuilder> builder) + { + builder + .ToTable("user_logins"); + + builder + .Property(ul => ul.LoginProvider) + .HasColumnName("login_provider"); + + builder + .Property(ul => ul.ProviderKey) + .HasColumnName("provider_key"); + + builder + .Property(ul => ul.ProviderDisplayName) + .HasColumnName("provider_display_name"); + + builder + .Property(ul => ul.UserId) + .HasColumnName("user_id"); + } +} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs new file mode 100644 index 0000000..70fee96 --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserRoleConfiguration.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; + +public class IdentityUserRoleConfiguration : IEntityTypeConfiguration> +{ + public void Configure(EntityTypeBuilder> builder) + { + builder + .ToTable("user_roles"); + + builder + .Property(ur => ur.UserId) + .HasColumnName("user_id"); + + builder + .Property(ur => ur.RoleId) + .HasColumnName("role_id"); + } +} diff --git a/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs new file mode 100644 index 0000000..6b91d44 --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Configurations/IdentityUserTokenConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations; + +public class IdentityUserTokenConfiguration : IEntityTypeConfiguration> +{ + public void Configure(EntityTypeBuilder> builder) + { + builder + .ToTable("user_tokens"); + + builder + .Property(ut => ut.UserId) + .HasColumnName("user_id"); + + builder + .Property(ut => ut.LoginProvider) + .HasColumnName("login_provider"); + + builder + .Property(ut => ut.Name) + .HasColumnName("name"); + + builder + .Property(ut => ut.Value) + .HasColumnName("value"); + } +} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs new file mode 100644 index 0000000..5fd201a --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.Designer.cs @@ -0,0 +1,355 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; + +#nullable disable + +namespace Identity.Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlIdentityDbContext))] + [Migration("20250423194315_Initial_migration")] + partial class Initial_migration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("identity") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_logins", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_roles", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_tokens", "identity"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.Property("Uuid") + .IsRequired() + .HasColumnType("text") + .HasColumnName("uuid"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "identity"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("Uuid") + .IsRequired() + .HasColumnType("text") + .HasColumnName("uuid"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.ToTable("users", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => + { + b.OwnsMany("cuqmbr.TravelGuide.Identity.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CreationTimestamp") + .HasColumnType("timestamptz") + .HasColumnName("creation_timestamp_utc"); + + b1.Property("ExpirationTimestamp") + .HasColumnType("timestamptz") + .HasColumnName("expiration_timestamp_utc"); + + b1.Property("IdentityUserId") + .HasColumnType("int") + .HasColumnName("user_id"); + + b1.Property("RevokationTimestamp") + .HasColumnType("timestamptz") + .HasColumnName("revokation_timestamp_utc"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("value"); + + b1.HasKey("Id") + .HasName("id"); + + b1.HasIndex("IdentityUserId"); + + b1.ToTable("user_refresh_tokens", "identity"); + + b1.WithOwner("IdentityUser") + .HasForeignKey("IdentityUserId") + .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); + + b1.Navigation("IdentityUser"); + }); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs new file mode 100644 index 0000000..035edce --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Migrations/20250423194315_Initial_migration.cs @@ -0,0 +1,281 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Identity.Persistence.PostgreSql.Migrations +{ + /// + public partial class Initial_migration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "identity"); + + migrationBuilder.CreateTable( + name: "roles", + schema: "identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + uuid = table.Column(type: "text", nullable: false), + name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + concurrency_stamp = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_roles", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "users", + schema: "identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + uuid = table.Column(type: "text", nullable: false), + email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + normalized_email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + email_confirmed = table.Column(type: "boolean", nullable: false), + password_hash = table.Column(type: "text", nullable: true), + security_stamp = table.Column(type: "text", nullable: true), + concurrency_stamp = table.Column(type: "text", nullable: true), + two_factor_enabled = table.Column(type: "boolean", nullable: false), + lockout_end = table.Column(type: "timestamp with time zone", nullable: true), + lockout_enabled = table.Column(type: "boolean", nullable: false), + access_failed_count = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "role_claims", + schema: "identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + role_id = table.Column(type: "integer", nullable: false), + claim_type = table.Column(type: "text", nullable: true), + claim_value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_role_claims", x => x.id); + table.ForeignKey( + name: "FK_role_claims_roles_role_id", + column: x => x.role_id, + principalSchema: "identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_claims", + schema: "identity", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "integer", nullable: false), + claim_type = table.Column(type: "text", nullable: true), + claim_value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user_claims", x => x.id); + table.ForeignKey( + name: "FK_user_claims_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_logins", + schema: "identity", + columns: table => new + { + login_provider = table.Column(type: "text", nullable: false), + provider_key = table.Column(type: "text", nullable: false), + provider_display_name = table.Column(type: "text", nullable: true), + user_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_logins", x => new { x.login_provider, x.provider_key }); + table.ForeignKey( + name: "FK_user_logins_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_refresh_tokens", + schema: "identity", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + value = table.Column(type: "varchar(256)", nullable: false), + creation_timestamp_utc = table.Column(type: "timestamptz", nullable: false), + expiration_timestamp_utc = table.Column(type: "timestamptz", nullable: false), + revokation_timestamp_utc = table.Column(type: "timestamptz", nullable: true), + user_id = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("id", x => x.id); + table.ForeignKey( + name: "fk_identityUserRefreshTokens_identityUser_userId", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_roles", + schema: "identity", + columns: table => new + { + user_id = table.Column(type: "integer", nullable: false), + role_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_roles", x => new { x.user_id, x.role_id }); + table.ForeignKey( + name: "FK_user_roles_roles_role_id", + column: x => x.role_id, + principalSchema: "identity", + principalTable: "roles", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_user_roles_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_tokens", + schema: "identity", + columns: table => new + { + user_id = table.Column(type: "integer", nullable: false), + login_provider = table.Column(type: "text", nullable: false), + name = table.Column(type: "text", nullable: false), + value = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user_tokens", x => new { x.user_id, x.login_provider, x.name }); + table.ForeignKey( + name: "FK_user_tokens_users_user_id", + column: x => x.user_id, + principalSchema: "identity", + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_role_claims_role_id", + schema: "identity", + table: "role_claims", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + schema: "identity", + table: "roles", + column: "normalized_name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_user_claims_user_id", + schema: "identity", + table: "user_claims", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_user_logins_user_id", + schema: "identity", + table: "user_logins", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_user_refresh_tokens_user_id", + schema: "identity", + table: "user_refresh_tokens", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "IX_user_roles_role_id", + schema: "identity", + table: "user_roles", + column: "role_id"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + schema: "identity", + table: "users", + column: "normalized_email"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "role_claims", + schema: "identity"); + + migrationBuilder.DropTable( + name: "user_claims", + schema: "identity"); + + migrationBuilder.DropTable( + name: "user_logins", + schema: "identity"); + + migrationBuilder.DropTable( + name: "user_refresh_tokens", + schema: "identity"); + + migrationBuilder.DropTable( + name: "user_roles", + schema: "identity"); + + migrationBuilder.DropTable( + name: "user_tokens", + schema: "identity"); + + migrationBuilder.DropTable( + name: "roles", + schema: "identity"); + + migrationBuilder.DropTable( + name: "users", + schema: "identity"); + } + } +} diff --git a/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs b/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs new file mode 100644 index 0000000..6a55357 --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/Migrations/PostgreSqlIdentityDbContextModelSnapshot.cs @@ -0,0 +1,352 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; + +#nullable disable + +namespace Identity.Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlIdentityDbContext))] + partial class PostgreSqlIdentityDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("identity") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("role_claims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text") + .HasColumnName("claim_type"); + + b.Property("ClaimValue") + .HasColumnType("text") + .HasColumnName("claim_value"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_claims", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("ProviderKey") + .HasColumnType("text") + .HasColumnName("provider_key"); + + b.Property("ProviderDisplayName") + .HasColumnType("text") + .HasColumnName("provider_display_name"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("user_logins", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("RoleId") + .HasColumnType("integer") + .HasColumnName("role_id"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("user_roles", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.Property("LoginProvider") + .HasColumnType("text") + .HasColumnName("login_provider"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Value") + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("user_tokens", "identity"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("name"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_name"); + + b.Property("Uuid") + .IsRequired() + .HasColumnType("text") + .HasColumnName("uuid"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("roles", "identity"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccessFailedCount") + .HasColumnType("integer") + .HasColumnName("access_failed_count"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text") + .HasColumnName("concurrency_stamp"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("email"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean") + .HasColumnName("email_confirmed"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean") + .HasColumnName("lockout_enabled"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone") + .HasColumnName("lockout_end"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasColumnName("normalized_email"); + + b.Property("PasswordHash") + .HasColumnType("text") + .HasColumnName("password_hash"); + + b.Property("SecurityStamp") + .HasColumnType("text") + .HasColumnName("security_stamp"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean") + .HasColumnName("two_factor_enabled"); + + b.Property("Uuid") + .IsRequired() + .HasColumnType("text") + .HasColumnName("uuid"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.ToTable("users", "identity"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("cuqmbr.TravelGuide.Identity.Models.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Identity.Models.IdentityUser", b => + { + b.OwnsMany("cuqmbr.TravelGuide.Identity.Models.RefreshToken", "RefreshTokens", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CreationTimestamp") + .HasColumnType("timestamptz") + .HasColumnName("creation_timestamp_utc"); + + b1.Property("ExpirationTimestamp") + .HasColumnType("timestamptz") + .HasColumnName("expiration_timestamp_utc"); + + b1.Property("IdentityUserId") + .HasColumnType("int") + .HasColumnName("user_id"); + + b1.Property("RevokationTimestamp") + .HasColumnType("timestamptz") + .HasColumnName("revokation_timestamp_utc"); + + b1.Property("Value") + .IsRequired() + .HasColumnType("varchar(256)") + .HasColumnName("value"); + + b1.HasKey("Id") + .HasName("id"); + + b1.HasIndex("IdentityUserId"); + + b1.ToTable("user_refresh_tokens", "identity"); + + b1.WithOwner("IdentityUser") + .HasForeignKey("IdentityUserId") + .HasConstraintName("fk_identityUserRefreshTokens_identityUser_userId"); + + b1.Navigation("IdentityUser"); + }); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs b/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs new file mode 100644 index 0000000..faca79d --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/PostgreSqlIdentityDbContext.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; +using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; +using Microsoft.EntityFrameworkCore; +using System.Reflection; +using Microsoft.Extensions.Options; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; + +public class PostgreSqlIdentityDbContext + : IdentityDbContext +{ + private readonly ConfigurationOptions _configuration; + + public PostgreSqlIdentityDbContext( + DbContextOptions options, + IOptions configurationOptions) + : base(options) + { + _configuration = configurationOptions.Value; + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.HasDefaultSchema(_configuration.Datastore.PartitionName); + + builder.ApplyConfigurationsFromAssembly( + Assembly.GetExecutingAssembly(), + t => t.Namespace == + "cuqmbr.TravelGuide.Identity.Persistence.PostgreSql.Configurations" + ); + } +} diff --git a/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs b/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs new file mode 100644 index 0000000..cabb6d7 --- /dev/null +++ b/src/Identity/Persistence/PostgreSql/PostgreSqlInitializer.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace cuqmbr.TravelGuide.Identity.Persistence.PostgreSql; + +public static class PostgreSqlInitializer +{ + public static void Initialize(IServiceProvider serviceProvider) + { + using var dbContext = serviceProvider + .GetService(); + + var totalMigrationsCount = + dbContext.Database.GetMigrations().Count(); + var appliedMigrationCount = + dbContext.Database.GetAppliedMigrations().Count(); + + if (totalMigrationsCount - appliedMigrationCount > 0) + { + dbContext.Database.Migrate(); + } + + if (appliedMigrationCount == 0) + { + IdentitySeeder.Seed(serviceProvider); + } + } +} diff --git a/src/Identity/Services/JwtAuthenticationService.cs b/src/Identity/Services/JwtAuthenticationService.cs new file mode 100644 index 0000000..f2a8c5d --- /dev/null +++ b/src/Identity/Services/JwtAuthenticationService.cs @@ -0,0 +1,210 @@ +using System.Security.Cryptography; +using cuqmbr.TravelGuide.Application.Authenticaion; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using IdentityUser = cuqmbr.TravelGuide.Identity.Models.IdentityUser; +using IdentityRole = cuqmbr.TravelGuide.Identity.Models.IdentityRole; +using cuqmbr.TravelGuide.Identity.Models; +using System.Security.Claims; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using cuqmbr.TravelGuide.Application.Common.Models; +using System.IdentityModel.Tokens.Jwt; +using cuqmbr.TravelGuide.Application.Common.Exceptions; + +namespace cuqmbr.TravelGuide.Identity.Services; + +public class JwtAuthenticationService : AuthenticationService +{ + private readonly ConfigurationOptions _configuration; + + private readonly UserManager _userManager; + private readonly RoleManager _roleManager; + + public JwtAuthenticationService( + UserManager userManager, + RoleManager roleManager, + IOptions configuration) + { + _userManager = userManager; + _userManager.UserValidators.Clear(); + _userManager.PasswordValidators.Clear(); + + _roleManager = roleManager; + + _configuration = configuration.Value; + } + + public async Task RegisterAsync( + string email, + string password, + CancellationToken cancellationToken) + { + var userWithSameEmail = await _userManager.FindByEmailAsync(email); + if (userWithSameEmail is not null) + { + throw new RegistrationException("User with given email already registered."); + } + + var newUser = new IdentityUser + { + // Id = Guid.NewGuid().ToString(), + Email = email, + RefreshTokens = default! + }; + + var createUserResult = await _userManager.CreateAsync(newUser, password); + + if (createUserResult.Errors.Any()) + { + var errorMessage = String.Join("\n", + createUserResult.Errors.Select(e => e.Description)); + throw new AuthenticationException(errorMessage); + } + + var addToRoleResult = + await _userManager.AddToRoleAsync(newUser, Application.Common.Models.IdentityRole.User.Name); + + if (addToRoleResult.Errors.Any()) + { + var errorMessage = String.Join("\n", + addToRoleResult.Errors.Select(e => e.Description)); + throw new AuthenticationException(errorMessage); + } + } + + public async Task LoginAsync( + string email, + string password, + CancellationToken cancellationToken) + { + var user = await _userManager.FindByEmailAsync(email); + if (user is null) + { + throw new LoginException("No users registered with given email."); + } + + var isPasswordCorrect = await _userManager.CheckPasswordAsync(user, password); + if (!isPasswordCorrect) + { + throw new LoginException("Given password is incorrect."); + } + + var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + var refreshToken = user.RefreshTokens.FirstOrDefault(t => t.IsActive); + if (refreshToken is null) + { + refreshToken = CreateRefreshToken(); + refreshToken.IdentityUserId = user.Id; + user.RefreshTokens.Add(refreshToken); + var result = await _userManager.UpdateAsync(user); + } + + return new TokensModel(accessToken, refreshToken.Value); + } + + public async Task RenewAccessTokenAsync( + string refreshToken, + CancellationToken cancellationToken) + { + var user = _userManager.Users.SingleOrDefault(u => + u.RefreshTokens.Any(rt => rt.Value == refreshToken)); + if (user is null) + { + throw new AuthenticationException($"Refresh token was not found."); + } + + var refreshTokenObject = user.RefreshTokens.Single(rt => + rt.Value == refreshToken); + if (!refreshTokenObject.IsActive) + { + throw new AuthenticationException("Refresh token is inactive."); + } + + var jwtSecurityToken = await CreateJwtAsync(user, cancellationToken); + var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + return new TokensModel(accessToken, refreshToken); + } + + public async Task RevokeRefreshTokenAsync( + string refreshToken, + CancellationToken cancellationToken) + { + var user = _userManager.Users.SingleOrDefault(u => + u.RefreshTokens.Any(t => t.Value == refreshToken)); + if (user is null) + { + throw new AuthenticationException("Invalid refreshToken"); + } + + var refreshTokenObject = user.RefreshTokens.Single(x => + x.Value == refreshToken); + if (!refreshTokenObject.IsActive) + { + throw new AuthenticationException("RefreshToken already revoked"); + } + + refreshTokenObject.RevokationTimestamp = DateTimeOffset.UtcNow; + await _userManager.UpdateAsync(user); + } + + private async Task CreateJwtAsync( + IdentityUser user, + CancellationToken cancellationToken) + { + var userClaims = await _userManager.GetClaimsAsync(user); + + var roles = await _userManager.GetRolesAsync(user); + var roleClaims = new List(); + foreach (var role in roles) + { + roleClaims.Add(new Claim("roles", role)); + } + + var claims = new List() + { + new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new Claim(JwtRegisteredClaimNames.Email, user.Email) + } + .Union(userClaims) + .Union(roleClaims); + + var expirationDateTimeUtc = DateTime.UtcNow.Add( + _configuration.JsonWebToken.AccessTokenValidity); + + var symmetricSecurityKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(_configuration.JsonWebToken.IssuerSigningKey)); + var signingCredentials = new SigningCredentials( + symmetricSecurityKey, SecurityAlgorithms.HmacSha256); + + var jwtSecurityToken = new JwtSecurityToken( + issuer: _configuration.JsonWebToken.Issuer, + audience: _configuration.JsonWebToken.Audience, + claims: claims, + expires: expirationDateTimeUtc, + signingCredentials: signingCredentials); + + return jwtSecurityToken; + } + + private RefreshToken CreateRefreshToken() + { + var randomNumber = new byte[32]; + + using var rng = RandomNumberGenerator.Create(); + rng.GetNonZeroBytes(randomNumber); + + return new RefreshToken + { + // Id = Guid.NewGuid().ToString(), + Value = Convert.ToBase64String(randomNumber), + CreationTimestamp = DateTimeOffset.UtcNow, + ExpirationTimestamp = DateTimeOffset.UtcNow.Add( + _configuration.JsonWebToken.RefreshTokenValidity) + }; + } +} diff --git a/src/Identity/packages.lock.json b/src/Identity/packages.lock.json new file mode 100644 index 0000000..aa4f436 --- /dev/null +++ b/src/Identity/packages.lock.json @@ -0,0 +1,600 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Identity": { + "type": "Direct", + "requested": "[2.3.1, )", + "resolved": "2.3.1", + "contentHash": "JcQ4pNXg+IISfcR95jeO2ZRt38N67MrUEj28HBmwfqD96BUyw4S54tQhrBmCOyPlf2vgNvSz/tsGAG7EgC0yRg==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Cookies": "2.3.0", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.Identity.Core": "2.3.0" + } + }, + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Identity.Stores": "9.0.4" + } + }, + "Microsoft.Extensions.Options": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Direct", + "requested": "[8.8.0, )", + "resolved": "8.8.0", + "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.8.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Direct", + "requested": "[8.8.0, )", + "resolved": "8.8.0", + "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.IdentityModel.Logging": "8.8.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, + "AspNetCore.Localizer.Json": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", + "dependencies": { + "Microsoft.AspNetCore.Components": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Localization": "9.0.0" + } + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" + }, + "MediatR": { + "type": "Transitive", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "MediatR.Behaviors.Authorization": { + "type": "Transitive", + "resolved": "12.2.0", + "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", + "dependencies": { + "MediatR": "12.4.1", + "MediatR.Contracts": "2.0.1" + } + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "Microsoft.AspNetCore.Authentication": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "Tq6bxTOe65Ikh9dWVTEOqpvNqBGIQueO0J+zl2rQba0yP0YV66iYDkSz9MqTdRZftvJ2I5kMeRUm9Z2mjEAbUQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Core": "2.3.0", + "Microsoft.AspNetCore.DataProtection": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Extensions.WebEncoders": "8.0.11" + } + }, + "Microsoft.AspNetCore.Authentication.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "ve6uvLwKNRkfnO/QeN9M8eUJ49lCnWv/6/9p6iTEuiI6Rtsz+myaBAjdMzLuTViQY032xbTF5AdZF5BJzJJyXQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.AspNetCore.Authentication.Cookies": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "w3JPWHreXJ/Uv9CLkQtGCLwTbxZKY+94QPVi1RxcMuBTyRp+C9SdynznHEjnHWnw6QFNEHnBuHmWW3OYrvbpEQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authentication.Core": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "gnLnKGawBjqBnU9fEuel3VcYAARkjyONAliaGDfMc8o8HBtfh+HrOPEoR8Xx4b2RnMb7uxdBDOvEAC7sul79ig==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "9.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + } + }, + "Microsoft.AspNetCore.DataProtection": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "C+FhGaA8ekrfes0Ujhtkhk74Bpkt6Zt+NrMaGrCWBqW1LFzqw/pXDbMbpcAyI9hbYgZfC6+t01As4LGXbdxG4A==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0", + "Microsoft.AspNetCore.DataProtection.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Win32.Registry": "4.5.0", + "System.Security.Cryptography.Xml": "8.0.2", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Microsoft.AspNetCore.DataProtection.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "71GdtUkVDagLsBt+YatfzUItnbT2vIjHxWySNE2MkgIDhqT3g4sNNxOj/0PlPTpc1+mG3ZwfUoZ61jIt1wPw7g==" + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4ivq53W2k6Nj4eez9wc81ytfGj6HR1NaZJCpOrvghJo9zHuQF57PLhPoQH5ItyCpHXnrN/y7yJDUm+TGYzrx0w==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "F5iHx7odAbFKBV1DNPDkFFcVmD5Tk7rk+tYm3LMQxHEFFdjlg5QcYb5XhHAefl5YaaPeG6ad+/ck8kSG3/D6kw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "I9azEG2tZ4DDHAFgv+N38e6Yhttvf+QjE2j2UYyCACE7Swm5/0uoihCMWZ87oOZYeqiEFSxbsfpT71OYHe2tpw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.AspNetCore.WebUtilities": "2.3.0", + "Microsoft.Extensions.ObjectPool": "8.0.11", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Net.Http.Headers": "2.3.0" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "EY2u/wFF5jsYwGXXswfQWrSsFPmiXsniAlUWo3rv/MGYf99ZFsENDnZcQP6W3c/+xQmQXq0NauzQ7jyy+o1LDQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", + "dependencies": { + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "+5IAX0aicQYCRfN4pAjad+JPwdEYoVEM3Z1Cl8/EiEv3FVHQHdd8TJQpQIslQDDQS/UsUMb0MsOXwqOh+TJtRw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.4", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E0pkWzI0liqu2ogqJ1kohk2eGkYRhf5tI75HGF6IQDARsshY/0w+prGyLvNuUeV7B8I7vYQZ4CzAKYKxw7b9gQ==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cMsm1O7g9X5qbB2wjHf3BVVvGwkG+zeXQ+M91I1Bm6RfylFMImqBPzs0+vmuef7fPxr2yOzPhIfJ2wQJfmtaSw==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "OjJ+xh/wQff5b0wiC3SPvoQqTA2boZeJQf+15+3+OJPtjBKzvxuwr25QRIu1p1t+K8ryQ8pzaoZ7eOpXfNzVGA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.Identity.Core": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.Extensions.Localization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" + }, + "Microsoft.Extensions.WebEncoders": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "EwF+KaQzTa/MoIm8gciABL6xeeiGKowqyam+lPYWukTppwch1P3QeL8CpgtLs8kIWuEowpAAUrVfP1kyZsZgqg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.8.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "/M0wVg6tJUOHutWD3BMOUVZAioJVXe0tCpFiovzv0T9T12TBf4MnaHP0efO8TCr1a6O9RZgQeZ9Gdark8L9XdA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", + "dependencies": { + "System.Security.AccessControl": "4.5.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "System.Linq.Dynamic.Core": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.0.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" + }, + "application": { + "type": "Project", + "dependencies": { + "AspNetCore.Localizer.Json": "[1.0.1, )", + "AutoMapper": "[14.0.0, )", + "Domain": "[1.0.0, )", + "FluentValidation": "[11.11.0, )", + "MediatR": "[12.4.1, )", + "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "System.Linq.Dynamic.Core": "[1.6.2, )" + } + }, + "domain": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/ConfigurationOptions.cs b/src/Infrastructure/ConfigurationOptions.cs new file mode 100644 index 0000000..1d4b8a4 --- /dev/null +++ b/src/Infrastructure/ConfigurationOptions.cs @@ -0,0 +1,6 @@ +namespace cuqmbr.TravelGuide.Infrastructure; + +public sealed class ConfigurationOptions +{ + public static string SectionName { get; } = "Infrastructure"; +} diff --git a/src/Infrastructure/Infrastructure.csproj b/src/Infrastructure/Infrastructure.csproj new file mode 100644 index 0000000..d704cae --- /dev/null +++ b/src/Infrastructure/Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + net9.0 + enable + enable + + + + + + + + true + + + diff --git a/src/Infrastructure/packages.lock.json b/src/Infrastructure/packages.lock.json new file mode 100644 index 0000000..83c6755 --- /dev/null +++ b/src/Infrastructure/packages.lock.json @@ -0,0 +1,184 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "AspNetCore.Localizer.Json": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", + "dependencies": { + "Microsoft.AspNetCore.Components": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Localization": "9.0.0" + } + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" + }, + "MediatR": { + "type": "Transitive", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "MediatR.Behaviors.Authorization": { + "type": "Transitive", + "resolved": "12.2.0", + "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", + "dependencies": { + "MediatR": "12.4.1", + "MediatR.Contracts": "2.0.1" + } + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "9.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" + }, + "Microsoft.Extensions.Localization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" + }, + "System.Linq.Dynamic.Core": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" + }, + "application": { + "type": "Project", + "dependencies": { + "AspNetCore.Localizer.Json": "[1.0.1, )", + "AutoMapper": "[14.0.0, )", + "Domain": "[1.0.0, )", + "FluentValidation": "[11.11.0, )", + "MediatR": "[12.4.1, )", + "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "System.Linq.Dynamic.Core": "[1.6.2, )" + } + }, + "domain": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/src/Persistence/ConfigurationOptions.cs b/src/Persistence/ConfigurationOptions.cs new file mode 100644 index 0000000..67f5dd5 --- /dev/null +++ b/src/Persistence/ConfigurationOptions.cs @@ -0,0 +1,16 @@ +namespace cuqmbr.TravelGuide.Persistence; + +public sealed class ConfigurationOptions +{ + public static string SectionName { get; } = "Application:Datastore"; + + public string Type { get; set; } = "inmemory"; + + public string ConnectionString { get; set; } = "InMemory"; + + public string PartitionName { get; set; } = "application"; + + public bool Migrate { get; set; } = true; + + public bool Seed { get; set; } = false; +} diff --git a/src/Persistence/DbSeeder.cs b/src/Persistence/DbSeeder.cs new file mode 100644 index 0000000..baefefe --- /dev/null +++ b/src/Persistence/DbSeeder.cs @@ -0,0 +1,13 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; + +namespace cuqmbr.TravelGuide.Persistence; + +public static class DbSeeder +{ + public static void Seed(UnitOfWork unitOfWork) + { + // Do Seeding Here + + unitOfWork.Dispose(); + } +} diff --git a/src/Persistence/Exceptions/UnSupportedDatastoreException.cs b/src/Persistence/Exceptions/UnSupportedDatastoreException.cs new file mode 100644 index 0000000..e665ea0 --- /dev/null +++ b/src/Persistence/Exceptions/UnSupportedDatastoreException.cs @@ -0,0 +1,11 @@ +namespace cuqmbr.TravelGuide.Persistence.Exceptions; + +public class UnSupportedDatastoreException : Exception +{ + public UnSupportedDatastoreException() + : base() { } + + public UnSupportedDatastoreException(string message) + : base(message) { } +} + diff --git a/src/Persistence/InMemory/Configurations/BaseConfiguration.cs b/src/Persistence/InMemory/Configurations/BaseConfiguration.cs new file mode 100644 index 0000000..4ed923a --- /dev/null +++ b/src/Persistence/InMemory/Configurations/BaseConfiguration.cs @@ -0,0 +1,55 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; + +public class BaseConfiguration : IEntityTypeConfiguration + where TEntity : EntityBase +{ + public virtual void Configure(EntityTypeBuilder builder) + { + builder + .HasKey(b => b.Id); + // .HasName($"pk_{builder.Metadata.GetTableName()}"); + + builder + .Property(b => b.Id) + .HasColumnName("id"); + // .HasColumnType("bigint") + // .UseSequence( + // $"{builder.Metadata.GetTableName()}_" + + // $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + + // "sequence"); + // + // builder + // .HasIndex(b => b.Id) + // .HasDatabaseName( + // "ix_" + + // $"{builder.Metadata.GetTableName()}_" + + // $"{builder.Property(b => b.Id).Metadata.GetColumnName()}") + // .IsUnique(); + // + // + // builder + // .HasAlternateKey(b => b.Guid) + // .HasName( + // "altk_" + + // $"{builder.Metadata.GetTableName()}_" + + // $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); + // + builder + .Property(b => b.Guid) + .HasColumnName("uuid") + // .HasColumnType("uuid") + .IsRequired(true); + // + // builder + // .HasIndex(b => b.Guid) + // .HasDatabaseName( + // "ix_" + + // $"{builder.Metadata.GetTableName()}_" + + // $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}") + // .IsUnique(); + } +} diff --git a/src/Persistence/InMemory/Configurations/CountryConfiguration.cs b/src/Persistence/InMemory/Configurations/CountryConfiguration.cs new file mode 100644 index 0000000..a335dfa --- /dev/null +++ b/src/Persistence/InMemory/Configurations/CountryConfiguration.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; + +public class CountryConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("countries"); + + base.Configure(builder); + + + builder + .Property(c => c.Name) + .HasColumnName("name") + // .HasColumnType("varchar(64)") + .IsRequired(true); + + // TODO: Remove comment + // builder + // .HasAlternateKey(c => c.Name) + // .HasName( + // "altk_" + + // $"{builder.Metadata.GetTableName()}_" + + // $"{builder.Property(c => c.Name).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/InMemory/Configurations/RegionConfiguration.cs b/src/Persistence/InMemory/Configurations/RegionConfiguration.cs new file mode 100644 index 0000000..87c4f8f --- /dev/null +++ b/src/Persistence/InMemory/Configurations/RegionConfiguration.cs @@ -0,0 +1,40 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Configurations; + +public class RegionConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("regions"); + + base.Configure(builder); + + + builder + .Property(r => r.Name) + .HasColumnName("name") + // .HasColumnType("varchar(64)") + .IsRequired(true); + + + builder + .Property(r => r.CountryId) + .HasColumnName("country_id") + // .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(r => r.Country) + .WithMany(c => c.Regions) + .HasForeignKey(r => r.CountryId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/Persistence/InMemory/InMemoryDbContext.cs b/src/Persistence/InMemory/InMemoryDbContext.cs new file mode 100644 index 0000000..6c74ec5 --- /dev/null +++ b/src/Persistence/InMemory/InMemoryDbContext.cs @@ -0,0 +1,45 @@ +using System.Reflection; +// using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +// using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace cuqmbr.TravelGuide.Persistence.InMemory; + +public class InMemoryDbContext : DbContext +{ + public InMemoryDbContext(DbContextOptions options) + : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // builder.HasPostgresEnum( + // "vehicle_type", + // VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); + // + builder + .ApplyConfigurationsFromAssembly( + Assembly.GetExecutingAssembly(), + t => t.Namespace == + "cuqmbr.TravelGuide.Persistence.InMemory.Configurations"); + } + + protected override void ConfigureConventions( + ModelConfigurationBuilder builder) + { + // builder + // .Properties() + // .HaveColumnType("vehicle_type") + // .HaveConversion(); + } +} + +// public class VehicleTypeConverter : ValueConverter +// { +// public VehicleTypeConverter() +// : base( +// v => v.Name, +// v => VehicleType.FromName(v)) +// { } +// } diff --git a/src/Persistence/InMemory/InMemoryUnitOfWork.cs b/src/Persistence/InMemory/InMemoryUnitOfWork.cs new file mode 100644 index 0000000..23af267 --- /dev/null +++ b/src/Persistence/InMemory/InMemoryUnitOfWork.cs @@ -0,0 +1,46 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +namespace cuqmbr.TravelGuide.Persistence.InMemory; + +public sealed class InMemoryUnitOfWork : UnitOfWork +{ + private readonly InMemoryDbContext _dbContext; + + public InMemoryUnitOfWork( + InMemoryDbContext dbContext) + { + _dbContext = dbContext; + + CountryRepository = new InMemoryCountryRepository(_dbContext); + RegionRepository = new InMemoryRegionRepository(_dbContext); + } + + public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + + public int Save() + { + return _dbContext.SaveChanges(); + } + + public async Task SaveAsync(CancellationToken cancellationToken) + { + return await _dbContext.SaveChangesAsync(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + if (disposing) + { + _dbContext.Dispose(); + } + } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs new file mode 100644 index 0000000..cb32f90 --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryBaseRepository.cs @@ -0,0 +1,132 @@ +using System.Linq.Expressions; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public abstract class InMemoryBaseRepository : BaseRepository + where TEntity : EntityBase +{ + protected readonly DbSet _dbSet; + + public InMemoryBaseRepository(InMemoryDbContext dbContext) + { + _dbSet = dbContext.Set(); + } + + public async Task AddOneAsync( + TEntity entity, + CancellationToken cancellationToken) + { + await _dbSet.AddAsync(entity, cancellationToken); + return entity; + } + + public async Task GetOneAsync( + Expression> predicate, + CancellationToken cancellationToken) + { + return await _dbSet.SingleOrDefaultAsync(predicate); + } + + public async Task GetOneAsync( + Expression> predicate, + Expression> includeSelector, + CancellationToken cancellationToken) + { + return + await _dbSet + .Include(includeSelector) + .SingleOrDefaultAsync(predicate); + } + + public async Task> GetPageAsync( + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.CountAsync(); + + var entities = + await _dbSet + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public async Task> GetPageAsync( + Expression> includeSelector, + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.CountAsync(); + + var entities = + await _dbSet + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .Include(includeSelector) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public async Task> GetPageAsync( + Expression> predicate, + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.Where(predicate).CountAsync(); + + var entities = + await _dbSet + .Where(predicate) + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public async Task> GetPageAsync( + Expression> predicate, + Expression> includeSelector, + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.Where(predicate).CountAsync(); + + var entities = + await _dbSet + .Where(predicate) + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .Include(includeSelector) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public Task UpdateOneAsync( + TEntity entity, + CancellationToken cancellationToken) + { + _dbSet.Update(entity); + return Task.FromResult(entity); + } + + public Task DeleteOneAsync( + TEntity entity, + CancellationToken cancellationToken) + { + _dbSet.Remove(entity); + return Task.CompletedTask; + } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs new file mode 100644 index 0000000..a0e0e1f --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryCountryRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryCountryRepository : + InMemoryBaseRepository, CountryRepository +{ + public InMemoryCountryRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs b/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs new file mode 100644 index 0000000..63a668e --- /dev/null +++ b/src/Persistence/InMemory/Repositories/InMemoryRegionRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.InMemory.Repositories; + +public sealed class InMemoryRegionRepository : + InMemoryBaseRepository, RegionRepository +{ + public InMemoryRegionRepository(InMemoryDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/Json/JsonDbContext.cs b/src/Persistence/Json/JsonDbContext.cs new file mode 100644 index 0000000..e69de29 diff --git a/src/Persistence/Persistence.csproj b/src/Persistence/Persistence.csproj new file mode 100644 index 0000000..c8621f3 --- /dev/null +++ b/src/Persistence/Persistence.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + true + + + diff --git a/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs new file mode 100644 index 0000000..2efda70 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/AddressConfiguration.cs @@ -0,0 +1,55 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class AddressConfiguration : BaseConfiguration
+{ + public override void Configure(EntityTypeBuilder
builder) + { + builder + .ToTable("addresses"); + + base.Configure(builder); + + + builder + .Property(a => a.Name) + .HasColumnName("name") + .HasColumnType("varchar(128)") + .IsRequired(true); + + // builder + // .Property(a => a.VehicleType) + // .HasColumnName("vehicle_type") + // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") + // .HasConversion() + // .IsRequired(true); + + + builder + .Property(a => a.CityId) + .HasColumnName("city_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(a => a.City) + .WithMany(c => c.Addresses) + .HasForeignKey(a => a.CityId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(a => a.CityId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(a => a.CityId).Metadata.GetColumnName()}") + .IsUnique(); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs new file mode 100644 index 0000000..5905cc3 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/BaseConfiguration.cs @@ -0,0 +1,55 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class BaseConfiguration : IEntityTypeConfiguration + where TEntity : EntityBase +{ + public virtual void Configure(EntityTypeBuilder builder) + { + builder + .HasKey(b => b.Id) + .HasName($"pk_{builder.Metadata.GetTableName()}"); + + builder + .Property(b => b.Id) + .HasColumnName("id") + .HasColumnType("bigint") + .UseSequence( + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(b => b.Id).Metadata.GetColumnName()}_" + + "sequence"); + + builder + .HasIndex(b => b.Id) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(b => b.Id).Metadata.GetColumnName()}") + .IsUnique(); + + + builder + .HasAlternateKey(b => b.Guid) + .HasName( + "altk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}"); + + builder + .Property(b => b.Guid) + .HasColumnName("uuid") + .HasColumnType("uuid") + .IsRequired(true); + + builder + .HasIndex(b => b.Guid) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(b => b.Guid).Metadata.GetColumnName()}") + .IsUnique(); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs new file mode 100644 index 0000000..dc6114c --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/CityConfiguration.cs @@ -0,0 +1,48 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class CityConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("cities"); + + base.Configure(builder); + + + builder + .Property(c => c.Name) + .HasColumnName("name") + .HasColumnType("varchar(64)") + .IsRequired(true); + + + builder + .Property(c => c.RegionId) + .HasColumnName("region_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(c => c.Region) + .WithMany(r => r.Cities) + .HasForeignKey(c => c.RegionId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(c => c.RegionId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(c => c.RegionId).Metadata.GetColumnName()}") + .IsUnique(); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/CountryConfiguration.cs b/src/Persistence/PostgreSql/Configurations/CountryConfiguration.cs new file mode 100644 index 0000000..b7c721d --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/CountryConfiguration.cs @@ -0,0 +1,31 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class CountryConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("countries"); + + base.Configure(builder); + + + builder + .Property(c => c.Name) + .HasColumnName("name") + .HasColumnType("varchar(64)") + .IsRequired(true); + + // TODO: Remove comment + // builder + // .HasAlternateKey(c => c.Name) + // .HasName( + // "altk_" + + // $"{builder.Metadata.GetTableName()}_" + + // $"{builder.Property(c => c.Name).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs new file mode 100644 index 0000000..1cd8573 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RegionConfiguration.cs @@ -0,0 +1,40 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RegionConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("regions"); + + base.Configure(builder); + + + builder + .Property(r => r.Name) + .HasColumnName("name") + .HasColumnType("varchar(64)") + .IsRequired(true); + + + builder + .Property(r => r.CountryId) + .HasColumnName("country_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(r => r.Country) + .WithMany(c => c.Regions) + .HasForeignKey(r => r.CountryId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(r => r.CountryId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs new file mode 100644 index 0000000..547d410 --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RouteAddressConfiguration.cs @@ -0,0 +1,83 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RouteAddressConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("route_addresses"); + + base.Configure(builder); + + + builder + .Property(ra => ra.Order) + .HasColumnName("order") + .HasColumnType("smallint") + .IsRequired(true); + + + builder + .Property(ra => ra.AddressId) + .HasColumnName("address_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ra => ra.Address) + .WithMany(a => a.AddressRoutes) + .HasForeignKey(ra => ra.AddressId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ra => ra.AddressId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}") + .IsUnique(); + + + builder + .Property(ra => ra.RouteId) + .HasColumnName("route_id") + .HasColumnType("bigint") + .IsRequired(true); + + builder + .HasOne(ra => ra.Route) + .WithMany(a => a.RouteAddresses) + .HasForeignKey(ra => ra.RouteId) + .HasConstraintName( + "fk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasIndex(ra => ra.RouteId) + .HasDatabaseName( + "ix_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}") + .IsUnique(); + + + builder + .HasAlternateKey(ra => new { ra.AddressId, ra.RouteId, ra.Order }) + .HasName( + "altk_" + + $"{builder.Metadata.GetTableName()}_" + + $"{builder.Property(ra => ra.AddressId).Metadata.GetColumnName()}_" + + $"{builder.Property(ra => ra.RouteId).Metadata.GetColumnName()}_" + + $"{builder.Property(ra => ra.Order).Metadata.GetColumnName()}"); + } +} diff --git a/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs new file mode 100644 index 0000000..6c81f2e --- /dev/null +++ b/src/Persistence/PostgreSql/Configurations/RouteConfiguration.cs @@ -0,0 +1,30 @@ +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations; + +public class RouteConfiguration : BaseConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder + .ToTable("routes"); + + base.Configure(builder); + + + builder + .Property(r => r.Name) + .HasColumnName("name") + .HasColumnType("varchar(64)") + .IsRequired(true); + + // builder + // .Property(r => r.VehicleType) + // .HasColumnName("vehicle_type") + // .HasColumnType($"{PostgreSqlDbContext.DefaultSchema}.vehicle_type") + // .HasConversion() + // .IsRequired(true); + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs b/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs new file mode 100644 index 0000000..d68963c --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.Designer.cs @@ -0,0 +1,392 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20250427160059_Initial_migration")] + partial class Initial_migration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("application.vehicle_type") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_Guid"); + + b.HasIndex("CityId") + .IsUnique() + .HasDatabaseName("ix_addresses_city_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_addresses_id"); + + b.ToTable("addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_cities_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_cities_id"); + + b.HasIndex("RegionId") + .IsUnique() + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_countries_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_countries_id"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_Guid"); + + b.HasIndex("CountryId"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_regions_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_regions_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("application.vehicle_type") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_routes_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_routes_id"); + + b.ToTable("routes", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_Guid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .IsUnique() + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_route_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_route_addresses_id"); + + b.HasIndex("RouteId") + .IsUnique() + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs b/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs new file mode 100644 index 0000000..2e209c1 --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/20250427160059_Initial_migration.cs @@ -0,0 +1,348 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + /// + public partial class Initial_migration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "application"); + + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:Enum:vehicle_type", "bus,train,aircraft"); + + migrationBuilder.CreateSequence( + name: "addresses_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "cities_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "countries_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "regions_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "route_addresses_id_sequence", + schema: "application"); + + migrationBuilder.CreateSequence( + name: "routes_id_sequence", + schema: "application"); + + migrationBuilder.CreateTable( + name: "countries", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.countries_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_countries", x => x.id); + table.UniqueConstraint("altk_countries_Guid", x => x.uuid); + }); + + migrationBuilder.CreateTable( + name: "routes", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.routes_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_routes", x => x.id); + table.UniqueConstraint("altk_routes_Guid", x => x.uuid); + }); + + migrationBuilder.CreateTable( + name: "regions", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.regions_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + country_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_regions", x => x.id); + table.UniqueConstraint("altk_regions_Guid", x => x.uuid); + table.ForeignKey( + name: "fk_regions_country_id", + column: x => x.country_id, + principalSchema: "application", + principalTable: "countries", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "cities", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.cities_id_sequence')"), + name = table.Column(type: "varchar(64)", nullable: false), + region_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_cities", x => x.id); + table.UniqueConstraint("altk_cities_Guid", x => x.uuid); + table.ForeignKey( + name: "fk_cities_region_id", + column: x => x.region_id, + principalSchema: "application", + principalTable: "regions", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "addresses", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.addresses_id_sequence')"), + name = table.Column(type: "varchar(128)", nullable: false), + vehicle_type = table.Column(type: "application.vehicle_type", nullable: false), + city_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_addresses", x => x.id); + table.UniqueConstraint("altk_addresses_Guid", x => x.uuid); + table.ForeignKey( + name: "fk_addresses_city_id", + column: x => x.city_id, + principalSchema: "application", + principalTable: "cities", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "route_addresses", + schema: "application", + columns: table => new + { + id = table.Column(type: "bigint", nullable: false, defaultValueSql: "nextval('application.route_addresses_id_sequence')"), + order = table.Column(type: "smallint", nullable: false), + address_id = table.Column(type: "bigint", nullable: false), + route_id = table.Column(type: "bigint", nullable: false), + uuid = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_route_addresses", x => x.id); + table.UniqueConstraint("altk_route_addresses_address_id_route_id_order", x => new { x.address_id, x.route_id, x.order }); + table.UniqueConstraint("altk_route_addresses_Guid", x => x.uuid); + table.ForeignKey( + name: "fk_route_addresses_address_id", + column: x => x.address_id, + principalSchema: "application", + principalTable: "addresses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_route_addresses_route_id", + column: x => x.route_id, + principalSchema: "application", + principalTable: "routes", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_addresses_city_id", + schema: "application", + table: "addresses", + column: "city_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_addresses_id", + schema: "application", + table: "addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_addresses_uuid", + schema: "application", + table: "addresses", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_cities_id", + schema: "application", + table: "cities", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_cities_region_id", + schema: "application", + table: "cities", + column: "region_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_cities_uuid", + schema: "application", + table: "cities", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_countries_id", + schema: "application", + table: "countries", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_countries_uuid", + schema: "application", + table: "countries", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_regions_country_id", + schema: "application", + table: "regions", + column: "country_id"); + + migrationBuilder.CreateIndex( + name: "ix_regions_id", + schema: "application", + table: "regions", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_regions_uuid", + schema: "application", + table: "regions", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_address_id", + schema: "application", + table: "route_addresses", + column: "address_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_id", + schema: "application", + table: "route_addresses", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_route_id", + schema: "application", + table: "route_addresses", + column: "route_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_route_addresses_uuid", + schema: "application", + table: "route_addresses", + column: "uuid", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_id", + schema: "application", + table: "routes", + column: "id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_routes_uuid", + schema: "application", + table: "routes", + column: "uuid", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "route_addresses", + schema: "application"); + + migrationBuilder.DropTable( + name: "addresses", + schema: "application"); + + migrationBuilder.DropTable( + name: "routes", + schema: "application"); + + migrationBuilder.DropTable( + name: "cities", + schema: "application"); + + migrationBuilder.DropTable( + name: "regions", + schema: "application"); + + migrationBuilder.DropTable( + name: "countries", + schema: "application"); + + migrationBuilder.DropSequence( + name: "addresses_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "cities_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "countries_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "regions_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "route_addresses_id_sequence", + schema: "application"); + + migrationBuilder.DropSequence( + name: "routes_id_sequence", + schema: "application"); + } + } +} diff --git a/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs new file mode 100644 index 0000000..9e89c0f --- /dev/null +++ b/src/Persistence/PostgreSql/Migrations/PostgreSqlDbContextModelSnapshot.cs @@ -0,0 +1,389 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using cuqmbr.TravelGuide.Persistence.PostgreSql; + +#nullable disable + +namespace Persistence.PostgreSql.Migrations +{ + [DbContext(typeof(PostgreSqlDbContext))] + partial class PostgreSqlDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("application") + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "vehicle_type", new[] { "bus", "train", "aircraft" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.HasSequence("addresses_id_sequence"); + + modelBuilder.HasSequence("cities_id_sequence"); + + modelBuilder.HasSequence("countries_id_sequence"); + + modelBuilder.HasSequence("regions_id_sequence"); + + modelBuilder.HasSequence("route_addresses_id_sequence"); + + modelBuilder.HasSequence("routes_id_sequence"); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "addresses_id_sequence"); + + b.Property("CityId") + .HasColumnType("bigint") + .HasColumnName("city_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(128)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("application.vehicle_type") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_addresses_Guid"); + + b.HasIndex("CityId") + .IsUnique() + .HasDatabaseName("ix_addresses_city_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_addresses_id"); + + b.ToTable("addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.cities_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "cities_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("bigint") + .HasColumnName("region_id"); + + b.HasKey("Id") + .HasName("pk_cities"); + + b.HasAlternateKey("Guid") + .HasName("altk_cities_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_cities_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_cities_id"); + + b.HasIndex("RegionId") + .IsUnique() + .HasDatabaseName("ix_cities_region_id"); + + b.ToTable("cities", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.countries_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "countries_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_countries"); + + b.HasAlternateKey("Guid") + .HasName("altk_countries_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_countries_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_countries_id"); + + b.ToTable("countries", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.regions_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "regions_id_sequence"); + + b.Property("CountryId") + .HasColumnType("bigint") + .HasColumnName("country_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_regions"); + + b.HasAlternateKey("Guid") + .HasName("altk_regions_Guid"); + + b.HasIndex("CountryId"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_regions_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_regions_id"); + + b.ToTable("regions", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.routes_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "routes_id_sequence"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("varchar(64)") + .HasColumnName("name"); + + b.Property("VehicleType") + .IsRequired() + .HasColumnType("application.vehicle_type") + .HasColumnName("vehicle_type"); + + b.HasKey("Id") + .HasName("pk_routes"); + + b.HasAlternateKey("Guid") + .HasName("altk_routes_Guid"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_routes_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_routes_id"); + + b.ToTable("routes", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id") + .HasDefaultValueSql("nextval('application.route_addresses_id_sequence')"); + + NpgsqlPropertyBuilderExtensions.UseSequence(b.Property("Id"), "route_addresses_id_sequence"); + + b.Property("AddressId") + .HasColumnType("bigint") + .HasColumnName("address_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("uuid"); + + b.Property("Order") + .HasColumnType("smallint") + .HasColumnName("order"); + + b.Property("RouteId") + .HasColumnType("bigint") + .HasColumnName("route_id"); + + b.HasKey("Id") + .HasName("pk_route_addresses"); + + b.HasAlternateKey("Guid") + .HasName("altk_route_addresses_Guid"); + + b.HasAlternateKey("AddressId", "RouteId", "Order") + .HasName("altk_route_addresses_address_id_route_id_order"); + + b.HasIndex("AddressId") + .IsUnique() + .HasDatabaseName("ix_route_addresses_address_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_route_addresses_uuid"); + + b.HasIndex("Id") + .IsUnique() + .HasDatabaseName("ix_route_addresses_id"); + + b.HasIndex("RouteId") + .IsUnique() + .HasDatabaseName("ix_route_addresses_route_id"); + + b.ToTable("route_addresses", "application"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.City", "City") + .WithMany("Addresses") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_addresses_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_cities_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_regions_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.RouteAddress", b => + { + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Address", "Address") + .WithMany("AddressRoutes") + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_address_id"); + + b.HasOne("cuqmbr.TravelGuide.Domain.Entities.Route", "Route") + .WithMany("RouteAddresses") + .HasForeignKey("RouteId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_route_addresses_route_id"); + + b.Navigation("Address"); + + b.Navigation("Route"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Address", b => + { + b.Navigation("AddressRoutes"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.City", b => + { + b.Navigation("Addresses"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("cuqmbr.TravelGuide.Domain.Entities.Route", b => + { + b.Navigation("RouteAddresses"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Persistence/PostgreSql/PostgreSqlDbContext.cs b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs new file mode 100644 index 0000000..fff9242 --- /dev/null +++ b/src/Persistence/PostgreSql/PostgreSqlDbContext.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using cuqmbr.TravelGuide.Domain.Enums; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.Extensions.Options; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql; + +public class PostgreSqlDbContext : DbContext +{ + public PostgreSqlDbContext( + DbContextOptions options, + IOptions configurationOptions) + : base(options) + { + DefaultSchema = configurationOptions.Value.PartitionName; + } + + public static string DefaultSchema { get; private set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.HasDefaultSchema(DefaultSchema); + + builder.HasPostgresEnum( + "vehicle_type", + VehicleType.Enumerations.Select(e => e.Value.Name).ToArray()); + + builder + .ApplyConfigurationsFromAssembly( + Assembly.GetExecutingAssembly(), + t => t.Namespace == + "cuqmbr.TravelGuide.Persistence.PostgreSql.Configurations"); + } + + protected override void ConfigureConventions( + ModelConfigurationBuilder builder) + { + builder + .Properties() + .HaveColumnType("vehicle_type") + .HaveConversion(); + } +} + +public class VehicleTypeConverter : ValueConverter +{ + public VehicleTypeConverter() + : base( + v => v.Name, + v => VehicleType.FromName(v)) + { } +} diff --git a/src/Persistence/PostgreSql/PostgreSqlDbInitializer.cs b/src/Persistence/PostgreSql/PostgreSqlDbInitializer.cs new file mode 100644 index 0000000..278d2f3 --- /dev/null +++ b/src/Persistence/PostgreSql/PostgreSqlDbInitializer.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql; + +public static class PostgreSqlDbInitializer +{ + public static void Initialize(PostgreSqlDbContext dbContext) + { + var totalMigrationsCount = + dbContext.Database.GetMigrations().Count(); + var appliedMigrationCount = + dbContext.Database.GetAppliedMigrations().Count(); + + if (totalMigrationsCount - appliedMigrationCount > 0) + { + dbContext.Database.Migrate(); + } + + dbContext.Dispose(); + } +} diff --git a/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs new file mode 100644 index 0000000..320b7f2 --- /dev/null +++ b/src/Persistence/PostgreSql/PostgreSqlUnitOfWork.cs @@ -0,0 +1,46 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql; + +public sealed class PostgreSqlUnitOfWork : UnitOfWork +{ + private readonly PostgreSqlDbContext _dbContext; + + public PostgreSqlUnitOfWork( + PostgreSqlDbContext dbContext) + { + _dbContext = dbContext; + + CountryRepository = new PostgreSqlCountryRepository(_dbContext); + RegionRepository = new PostgreSqlRegionRepository(_dbContext); + } + + public CountryRepository CountryRepository { get; init; } + public RegionRepository RegionRepository { get; init; } + + public int Save() + { + return _dbContext.SaveChanges(); + } + + public async Task SaveAsync(CancellationToken cancellationToken) + { + return await _dbContext.SaveChangesAsync(); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + if (disposing) + { + _dbContext.Dispose(); + } + } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs new file mode 100644 index 0000000..a5c1dae --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlBaseRepository.cs @@ -0,0 +1,133 @@ +using System.Linq.Expressions; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public abstract class PostgreSqlBaseRepository : BaseRepository + where TEntity : EntityBase +{ + protected readonly DbSet _dbSet; + + public PostgreSqlBaseRepository(PostgreSqlDbContext dbContext) + { + _dbSet = dbContext.Set(); + } + + public async Task AddOneAsync( + TEntity entity, + CancellationToken cancellationToken) + { + await _dbSet.AddAsync(entity, cancellationToken); + return entity; + } + + public async Task GetOneAsync( + Expression> predicate, + CancellationToken cancellationToken) + { + return await _dbSet.SingleOrDefaultAsync(predicate); + } + + public async Task GetOneAsync( + Expression> predicate, + Expression> includeSelector, + CancellationToken cancellationToken) + { + return + await _dbSet + .Include(includeSelector) + .SingleOrDefaultAsync(predicate); + } + + public async Task> GetPageAsync( + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.CountAsync(); + + var entities = + await _dbSet + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public async Task> GetPageAsync( + Expression> includeSelector, + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.CountAsync(); + + var entities = + await _dbSet + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .Include(includeSelector) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public async Task> GetPageAsync( + Expression> predicate, + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.Where(predicate).CountAsync(); + + var entities = + await _dbSet + .Where(predicate) + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public async Task> GetPageAsync( + Expression> predicate, + Expression> includeSelector, + int pageNumber, int pageSize, + CancellationToken cancellationToken) + { + var count = await _dbSet.Where(predicate).CountAsync(); + + var entities = + await _dbSet + .Where(predicate) + .Skip((pageNumber - 1) * pageSize).Take(pageSize) + .Include(includeSelector) + .ToListAsync(); + + return new PaginatedList( + entities, count, + pageNumber, pageSize); + } + + public Task UpdateOneAsync( + TEntity entity, + CancellationToken cancellationToken) + { + _dbSet.Update(entity); + return Task.FromResult(entity); + } + + public Task DeleteOneAsync( + TEntity entity, + CancellationToken cancellationToken) + { + _dbSet.Remove(entity); + return Task.CompletedTask; + } + +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs new file mode 100644 index 0000000..78d315b --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlCountryRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlCountryRepository : + PostgreSqlBaseRepository, CountryRepository +{ + public PostgreSqlCountryRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs b/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs new file mode 100644 index 0000000..036018e --- /dev/null +++ b/src/Persistence/PostgreSql/Repositories/PostgreSqlRegionRepository.cs @@ -0,0 +1,11 @@ +using cuqmbr.TravelGuide.Application.Common.Interfaces.Persistence.Repositories; +using cuqmbr.TravelGuide.Domain.Entities; + +namespace cuqmbr.TravelGuide.Persistence.PostgreSql.Repositories; + +public sealed class PostgreSqlRegionRepository : + PostgreSqlBaseRepository, RegionRepository +{ + public PostgreSqlRegionRepository(PostgreSqlDbContext dbContext) + : base(dbContext) { } +} diff --git a/src/Persistence/packages.lock.json b/src/Persistence/packages.lock.json new file mode 100644 index 0000000..af85452 --- /dev/null +++ b/src/Persistence/packages.lock.json @@ -0,0 +1,340 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "+5IAX0aicQYCRfN4pAjad+JPwdEYoVEM3Z1Cl8/EiEv3FVHQHdd8TJQpQIslQDDQS/UsUMb0MsOXwqOh+TJtRw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.4", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "VHjUwjvN8UKjb8xtKJ/o+dc9tTeHOW3QzlfkzX3JrUspLkPIjwMdZCcw6eS4gsFjby0NFkcXBjHtrgTjVOfO5Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "YruNASPuiCjLOVxO09lpQT4e2OYvpsoD0e5NGEQKOcPCu143RDzWTNlpzcxhArBgAS0FPwQ+OEGZOWhwgWHvOA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.10", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.Extensions.Options": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, + "AspNetCore.Localizer.Json": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", + "dependencies": { + "Microsoft.AspNetCore.Components": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Localization": "9.0.0" + } + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" + }, + "MediatR": { + "type": "Transitive", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "MediatR.Behaviors.Authorization": { + "type": "Transitive", + "resolved": "12.2.0", + "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", + "dependencies": { + "MediatR": "12.4.1", + "MediatR.Contracts": "2.0.1" + } + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "9.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "rnVGier1R0w9YEAzxOlUl8koFwq4QLwuYKiJN6NFqbCNCPrRLGW3f7x0OtL/Sp1KBMVhgffUIP6jWPppzhpa2Q==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E0pkWzI0liqu2ogqJ1kohk2eGkYRhf5tI75HGF6IQDARsshY/0w+prGyLvNuUeV7B8I7vYQZ4CzAKYKxw7b9gQ==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cMsm1O7g9X5qbB2wjHf3BVVvGwkG+zeXQ+M91I1Bm6RfylFMImqBPzs0+vmuef7fPxr2yOzPhIfJ2wQJfmtaSw==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "OjJ+xh/wQff5b0wiC3SPvoQqTA2boZeJQf+15+3+OJPtjBKzvxuwr25QRIu1p1t+K8ryQ8pzaoZ7eOpXfNzVGA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "96NFbmjcZsO9HkSdWAwh5tn/7LKIu7cLW+zubyGV1BR1w8xpcqPXZcTW4S/0eA0d9BxyFnH8tSDRjUerWGoU/Q==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "9.0.4", + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" + }, + "Microsoft.Extensions.Localization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.10", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.10" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "System.Linq.Dynamic.Core": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g==" + }, + "application": { + "type": "Project", + "dependencies": { + "AspNetCore.Localizer.Json": "[1.0.1, )", + "AutoMapper": "[14.0.0, )", + "Domain": "[1.0.0, )", + "FluentValidation": "[11.11.0, )", + "MediatR": "[12.4.1, )", + "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "System.Linq.Dynamic.Core": "[1.6.2, )" + } + }, + "domain": { + "type": "Project" + } + } + } +} \ No newline at end of file diff --git a/tst/Application.IntegrationTests/Application.IntegrationTests.csproj b/tst/Application.IntegrationTests/Application.IntegrationTests.csproj new file mode 100644 index 0000000..5351f98 --- /dev/null +++ b/tst/Application.IntegrationTests/Application.IntegrationTests.csproj @@ -0,0 +1,39 @@ + + + + enable + enable + Exe + Application.IntegrationTests + net9.0 + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/tst/Application.IntegrationTests/BaseTest.cs b/tst/Application.IntegrationTests/BaseTest.cs new file mode 100644 index 0000000..c919add --- /dev/null +++ b/tst/Application.IntegrationTests/BaseTest.cs @@ -0,0 +1,120 @@ +using Microsoft.Extensions.DependencyInjection; +using cuqmbr.TravelGuide.Configuration.Configuration; +using cuqmbr.TravelGuide.Configuration.Logging; +using cuqmbr.TravelGuide.Configuration.Application; +using cuqmbr.TravelGuide.Configuration.Persistence; +using cuqmbr.TravelGuide.Configuration.Identity; + +using Moq; +using System.Globalization; +using cuqmbr.TravelGuide.Application.Common.Interfaces.Services; +using cuqmbr.TravelGuide.Application.Common.Models; + +namespace cuqmbr.TravelGuide.Application.IntegrationTests; + +public abstract class TestBase : IDisposable +{ + protected readonly IServiceCollection _serviceCollection; + private IServiceScope _scope; + + public TestBase() + { + _serviceCollection = new ServiceCollection(); + + _serviceCollection + .ConfigureConfiguration( + new string[] + { + "--Application:Datastore:Type", "inmemory", + "--Application:Logging:LogLevel", "None" + }) + .ConfigureLogging() + .ConfigureApplication() + .ConfigurePersistence(); + // TODO: Create InMemory configuration for Identity + // .ConfigureIdentity(); + + SetCulture("en-US"); + SetTimeZone("Europe/Kyiv"); + } + + public T GetService() + { + var serviceProvider = _serviceCollection.BuildServiceProvider(); + _scope = serviceProvider.CreateScope(); + return _scope.ServiceProvider.GetRequiredService(); + } + + public void Dispose() + { + _scope.Dispose(); + GC.SuppressFinalize(this); + } + + protected void SetAuthenticatedUserRoles(IdentityRole[] roles = default!) + { + _serviceCollection + .AddScoped(_ => + { + var mock = new Mock(); + + var guid = Guid.NewGuid(); + + mock.Setup(s => s.Email).Returns(guid.ToString()); + mock.Setup(s => s.Id).Returns(guid.GetHashCode()); + mock.Setup(s => s.Uuid).Returns(Guid.NewGuid()); + mock.Setup(s => s.IsAuthenticated).Returns(true); + mock.Setup(s => s.Roles).Returns(roles); + + return mock.Object; + }); + } + + protected void SetUnAuthenticatedUser() + { + _serviceCollection + .AddScoped(_ => + { + var mock = new Mock(); + + mock.Setup(s => s.IsAuthenticated).Returns(false); + + return mock.Object; + }); + } + + public void SetCulture(string culture) + { + var cultureInfo = CultureInfo.GetCultureInfo(culture); + + _serviceCollection + .AddScoped(_ => + { + var mock = new Mock(); + + mock + .Setup(s => s.Culture) + .Returns(cultureInfo); + + return mock.Object; + }); + + CultureInfo.DefaultThreadCurrentCulture = cultureInfo; + CultureInfo.DefaultThreadCurrentUICulture = cultureInfo; + } + + public void SetTimeZone(string timeZone) + { + _serviceCollection + .AddScoped(_ => + { + var mock = new Mock(); + + mock + .Setup(s => s.TimeZone) + .Returns(TimeZoneInfo.FindSystemTimeZoneById(timeZone)); + + return mock.Object; + }); + } +} diff --git a/tst/Application.IntegrationTests/CountriesTests.cs b/tst/Application.IntegrationTests/CountriesTests.cs new file mode 100644 index 0000000..20e6233 --- /dev/null +++ b/tst/Application.IntegrationTests/CountriesTests.cs @@ -0,0 +1,765 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; +using cuqmbr.TravelGuide.Application.Countries.Commands.UpdateCountry; +using cuqmbr.TravelGuide.Application.Countries.Commands.DeleteCountry; +using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountry; +using cuqmbr.TravelGuide.Application.Countries.Queries.GetCountriesPage; + + +namespace cuqmbr.TravelGuide.Application.IntegrationTests; + +public class CountriesTests : TestBase +{ + [Fact] + public async Task AddCountry_WithAdminRole_CountryCreated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string name = "Name"; + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = name + }, TestContext.Current.CancellationToken); + + var getCountryResult = await mediator.Send( + new GetCountryQuery() + { + Guid = createCountryResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountryResult); + Assert.NotNull(getCountryResult.Name); + Assert.Equal(name, getCountryResult.Name); + } + + [Fact] + public async Task + AddDuplicateCountry_WithAdminRole_ThrowsDuplicateEntityException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string name = "Name"; + + await mediator.Send( + new AddCountryCommand() + { + Name = name + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCountryCommand() + { + Name = name + }, TestContext.Current.CancellationToken)); + } + + [Theory] + // Empty + [InlineData("")] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + AddCountry_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCountryCommand() + { + Name = name + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task AddCountry_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + AddCountry_WithUnAuthenticatedUser_ThrowsUnAuthorizedException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task UpdateCountry_WithAdminRole_CountryUpdated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + string newName = "Different Name"; + + var editCountryResult = await mediator.Send( + new UpdateCountryCommand() + { + Guid = createCountryResult.Uuid, + Name = newName + }, TestContext.Current.CancellationToken); + + var getCountryResult = await mediator.Send( + new GetCountryQuery() + { + Guid = createCountryResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountryResult); + Assert.Equal(newName, getCountryResult.Name); + Assert.Equal(createCountryResult.Uuid, getCountryResult.Uuid); + } + + [Fact] + public async Task + UpdateDuplicateCountry_WithAdminRole_ThrowsDuplicateEntityException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = "Name 1" + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = "Name 2" + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCountryCommand() + { + Guid = createCountryResult2.Uuid, + Name = createCountryResult1.Name + }, TestContext.Current.CancellationToken)); + } + + [Theory] + // Empty + [InlineData("")] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + UpdateCountry_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name 1" + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCountryCommand() + { + Guid = createCountryResult.Uuid, + Name = name + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateCountry_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCountryCommand() + { + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Name = "Name" + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateCountry_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCountryCommand() + { + Guid = Guid.NewGuid(), + Name = "Name" + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task UpdateCountry_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCountryCommand() + { + Guid = Guid.NewGuid(), + Name = "Name" + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task UpdateCountry_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateCountryCommand() + { + Guid = Guid.NewGuid(), + Name = "Name" + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task DeleteCountry_WithAdminRole_CountryDeleted() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + await mediator.Send( + new DeleteCountryCommand() + { + Guid = createCountryResult.Uuid, + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountryQuery() + { + Guid = createCountryResult.Uuid, + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + DeleteCountry_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCountryCommand() + { + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + DeleteCountry_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCountryCommand() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task DeleteCountry_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCountryCommand() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task DeleteCountry_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteCountryCommand() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task GetCountry_WithAdminRole_CountryReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string name = "Name"; + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = name + }, TestContext.Current.CancellationToken); + + var getCountryResult = await mediator.Send( + new GetCountryQuery() + { + Guid = createCountryResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountryResult); + Assert.NotNull(getCountryResult.Name); + Assert.Equal(name, getCountryResult.Name); + Assert.Equal(createCountryResult.Uuid, getCountryResult.Uuid); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + GetCountry_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountryQuery() + { + Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + GetCountry_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountryQuery() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetCountry_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountryQuery() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task GetCountry_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountryQuery() + { + Guid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task GetCountriesPage_WithAdminRole_CountriesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string name1 = "Name 1"; + string name2 = "Name 2"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = name1 + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = name2 + }, TestContext.Current.CancellationToken); + + var getCountriesResult = await mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = 1, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountriesResult); + Assert.Equal(1, getCountriesResult.PageNumber); + Assert.Equal(2, getCountriesResult.TotalCount); + Assert.Equal(2, getCountriesResult.TotalPages); + Assert.True(getCountriesResult.HasNextPage); + Assert.False(getCountriesResult.HasPreviousPage); + Assert.NotNull(getCountriesResult.Items); + Assert.Single(getCountriesResult.Items); + Assert.NotNull(getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Name, getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Uuid, getCountriesResult.Items.First().Uuid); + + getCountriesResult = await mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = 2, + PageSize = 1 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountriesResult); + Assert.Equal(2, getCountriesResult.PageNumber); + Assert.Equal(2, getCountriesResult.TotalCount); + Assert.Equal(2, getCountriesResult.TotalPages); + Assert.False(getCountriesResult.HasNextPage); + Assert.True(getCountriesResult.HasPreviousPage); + Assert.NotNull(getCountriesResult.Items); + Assert.Single(getCountriesResult.Items); + Assert.NotNull(getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult2.Name, getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult2.Uuid, getCountriesResult.Items.First().Uuid); + + getCountriesResult = await mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = 1, + PageSize = 10 + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountriesResult); + Assert.Equal(1, getCountriesResult.PageNumber); + Assert.Equal(2, getCountriesResult.TotalCount); + Assert.Equal(1, getCountriesResult.TotalPages); + Assert.False(getCountriesResult.HasNextPage); + Assert.False(getCountriesResult.HasPreviousPage); + Assert.NotNull(getCountriesResult.Items); + Assert.Equal(2, getCountriesResult.Items.Count()); + Assert.NotNull(getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Name, getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Uuid, getCountriesResult.Items.First().Uuid); + Assert.NotNull(getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult2.Name, getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult2.Uuid, getCountriesResult.Items.Last().Uuid); + } + + [Fact] + public async Task + GetCountriesPage_WithSearch_WithAdminRole_SearchedCountriesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string name1 = "Name 1"; + string name2 = "Some 3 String"; + string name3 = "3 Name Some"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = name1 + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = name2 + }, TestContext.Current.CancellationToken); + + var createCountryResult3 = await mediator.Send( + new AddCountryCommand() + { + Name = name3 + }, TestContext.Current.CancellationToken); + + var getCountriesResult = await mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "name" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountriesResult); + Assert.Equal(1, getCountriesResult.PageNumber); + Assert.Equal(2, getCountriesResult.TotalCount); + Assert.Equal(1, getCountriesResult.TotalPages); + Assert.False(getCountriesResult.HasNextPage); + Assert.False(getCountriesResult.HasPreviousPage); + Assert.NotNull(getCountriesResult.Items); + Assert.Equal(2, getCountriesResult.Items.Count()); + Assert.NotNull(getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Name, getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Uuid, getCountriesResult.Items.First().Uuid); + Assert.NotNull(getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult3.Name, getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult3.Uuid, getCountriesResult.Items.Last().Uuid); + + getCountriesResult = await mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Search = "3" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountriesResult); + Assert.Equal(1, getCountriesResult.PageNumber); + Assert.Equal(2, getCountriesResult.TotalCount); + Assert.Equal(1, getCountriesResult.TotalPages); + Assert.False(getCountriesResult.HasNextPage); + Assert.False(getCountriesResult.HasPreviousPage); + Assert.NotNull(getCountriesResult.Items); + Assert.Equal(2, getCountriesResult.Items.Count()); + Assert.NotNull(getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult2.Name, getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult2.Uuid, getCountriesResult.Items.First().Uuid); + Assert.NotNull(getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult3.Name, getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult3.Uuid, getCountriesResult.Items.Last().Uuid); + } + + [Fact] + public async Task + GetCountriesPage_WithSort_WithAdminRole_SortedCountriesPageReturned() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string name1 = "Name 1"; + string name2 = "Some 2"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = name1 + }, TestContext.Current.CancellationToken); + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = name2 + }, TestContext.Current.CancellationToken); + + var getCountriesResult = await mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "-name" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountriesResult); + Assert.Equal(1, getCountriesResult.PageNumber); + Assert.Equal(2, getCountriesResult.TotalCount); + Assert.Equal(1, getCountriesResult.TotalPages); + Assert.False(getCountriesResult.HasNextPage); + Assert.False(getCountriesResult.HasPreviousPage); + Assert.NotNull(getCountriesResult.Items); + Assert.Equal(2, getCountriesResult.Items.Count()); + Assert.NotNull(getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult2.Name, getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult2.Uuid, getCountriesResult.Items.First().Uuid); + Assert.NotNull(getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult1.Name, getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult1.Uuid, getCountriesResult.Items.Last().Uuid); + + getCountriesResult = await mediator.Send( + new GetCountriesPageQuery() + { + PageNumber = 1, + PageSize = 10, + Sort = "+name" + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getCountriesResult); + Assert.Equal(1, getCountriesResult.PageNumber); + Assert.Equal(2, getCountriesResult.TotalCount); + Assert.Equal(1, getCountriesResult.TotalPages); + Assert.False(getCountriesResult.HasNextPage); + Assert.False(getCountriesResult.HasPreviousPage); + Assert.NotNull(getCountriesResult.Items); + Assert.Equal(2, getCountriesResult.Items.Count()); + Assert.NotNull(getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Name, getCountriesResult.Items.First().Name); + Assert.Equal( + createCountryResult1.Uuid, getCountriesResult.Items.First().Uuid); + Assert.NotNull(getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult2.Name, getCountriesResult.Items.Last().Name); + Assert.Equal( + createCountryResult2.Uuid, getCountriesResult.Items.Last().Uuid); + } + + [Theory] + // Length > 64 (65) + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + GetCountriesPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException + (string search) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountriesPageQuery() + { + Search = search + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + public async Task + GetCountriesPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException + (int pageNumber) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountriesPageQuery() + { + PageNumber = pageNumber + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(0)] + [InlineData(51)] + [InlineData(int.MaxValue)] + public async Task + GetCountriesPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException + (int pageSize) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetCountriesPageQuery() + { + PageSize = pageSize + }, TestContext.Current.CancellationToken)); + } +} diff --git a/tst/Application.IntegrationTests/RegionsTests.cs b/tst/Application.IntegrationTests/RegionsTests.cs new file mode 100644 index 0000000..247d706 --- /dev/null +++ b/tst/Application.IntegrationTests/RegionsTests.cs @@ -0,0 +1,1290 @@ +using MediatR; +using cuqmbr.TravelGuide.Application.Common.Models; +using cuqmbr.TravelGuide.Application.Common.Exceptions; +using cuqmbr.TravelGuide.Application.Countries.Commands.AddCountry; +using cuqmbr.TravelGuide.Application.Regions.Commands.AddRegion; +using cuqmbr.TravelGuide.Application.Regions.Commands.UpdateRegion; +using cuqmbr.TravelGuide.Application.Regions.Commands.DeleteRegion; +using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegion; +using cuqmbr.TravelGuide.Application.Regions.Queries.GetRegionsPage; + + +namespace cuqmbr.TravelGuide.Application.IntegrationTests; + +public class RegionsTests : TestBase +{ + [Fact] + public async Task AddRegion_WithAdminRole_RegionCreated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Regin Name"; + + var createRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionResult = await mediator.Send( + new GetRegionQuery() + { + Uuid = createRegionResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionResult); + Assert.NotNull(getRegionResult.Name); + Assert.Equal(regionName, getRegionResult.Name); + Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + } + + [Fact] + public async Task + AddDuplicateRegion_WithAdminRole_ThrowsDuplicateEntityException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Regin Name"; + + var createRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddRegionCommand() + { + Name = regionName, + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + AddSameRegionsToDifferentCountries_WithAdminRole_RegionsCreated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName1 = "Country Name 1"; + + var createCountryResult1 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName1 + }, TestContext.Current.CancellationToken); + + string countryName2 = "Country Name2 "; + + var createCountryResult2 = await mediator.Send( + new AddCountryCommand() + { + Name = countryName2 + }, TestContext.Current.CancellationToken); + + string regionName = "Regin Name"; + + var createRegionResult1 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = createCountryResult1.Uuid + }, TestContext.Current.CancellationToken); + + var createRegionResult2 = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = createCountryResult2.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionResult1 = await mediator.Send( + new GetRegionQuery() + { + Uuid = createRegionResult1.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionResult1); + Assert.NotNull(getRegionResult1.Name); + Assert.Equal(regionName, getRegionResult1.Name); + Assert.Equal(createCountryResult1.Uuid, getRegionResult1.CountryUuid); + + var getRegionResult2 = await mediator.Send( + new GetRegionQuery() + { + Uuid = createRegionResult2.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionResult2); + Assert.NotNull(getRegionResult2.Name); + Assert.Equal(regionName, getRegionResult2.Name); + Assert.Equal(createCountryResult2.Uuid, getRegionResult2.CountryUuid); + } + + [Fact] + public async Task + AddRegion_WithNonExistentCountryUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddRegionCommand() + { + Name = "Name", + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + AddRegion_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddRegionCommand() + { + Name = name + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + AddRegion_WithInvalidCountryUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddRegionCommand() + { + Name = "Name", + CountryUuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task AddRegion_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddRegionCommand() + { + Name = "Name", + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task AddRegion_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new AddRegionCommand() + { + Name = "Name", + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task UpdateRegion_WithAdminRole_RegionUpdated() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + string countryName = "Country Name"; + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = countryName + }, TestContext.Current.CancellationToken); + + string regionName = "Region Name"; + + var createRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = regionName, + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + string newName = "Different Name"; + + var editRegionResult = await mediator.Send( + new UpdateRegionCommand() + { + Uuid = createRegionResult.Uuid, + Name = newName, + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + var getRegionResult = await mediator.Send( + new GetRegionQuery() + { + Uuid = createRegionResult.Uuid, + }, TestContext.Current.CancellationToken); + + Assert.NotNull(getRegionResult); + Assert.NotNull(getRegionResult.Name); + Assert.Equal(newName, getRegionResult.Name); + Assert.Equal(createCountryResult.Uuid, getRegionResult.CountryUuid); + } + + [Theory] + [InlineData("")] + [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + public async Task + UpdateRegion_WithInvalidName_WithAdminRole_ThrowsValidationException + (string name) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateRegionCommand() + { + Name = name, + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateRegion_WithInvalidCountryUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateRegionCommand() + { + Uuid = Guid.NewGuid(), + Name = "Name", + CountryUuid = + Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + UpdateRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateRegionCommand() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + Name = "Name", + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateRegionCommand() + { + Uuid = Guid.NewGuid(), + Name = "Different Name", + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + UpdateRegion_WithNonExistentCountryUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var createRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateRegionCommand() + { + Uuid = createCountryResult.Uuid, + Name = "Different Name", + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task UpdateRegion_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateRegionCommand() + { + Uuid = Guid.NewGuid(), + Name = "Name", + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task UpdateRegion_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new UpdateRegionCommand() + { + Uuid = Guid.NewGuid(), + Name = "Name", + CountryUuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + [Fact] + public async Task DeleteRegion_WithAdminRole_RegionDeleted() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + var createCountryResult = await mediator.Send( + new AddCountryCommand() + { + Name = "Name" + }, TestContext.Current.CancellationToken); + + var createRegionResult = await mediator.Send( + new AddRegionCommand() + { + Name = "Name", + CountryUuid = createCountryResult.Uuid + }, TestContext.Current.CancellationToken); + + await mediator.Send( + new DeleteRegionCommand() + { + Uuid = createRegionResult.Uuid, + }, TestContext.Current.CancellationToken); + + await Assert.ThrowsAsync(() => + mediator.Send(new GetRegionQuery() + { + Uuid = createRegionResult.Uuid, + }, TestContext.Current.CancellationToken)); + } + + [Theory] + [InlineData("")] + [InlineData("not an uuid")] + public async Task + DeleteRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException + (string uuid) + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteRegionCommand() + { + Uuid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task + DeleteRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteRegionCommand() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task DeleteRegion_WithUserRole_ThrowsForbiddenException() + { + SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteRegionCommand() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with user role (copy tests with admin role) + + [Fact] + public async Task DeleteRegion_UnAuthnticatedUser_ThrowsForbiddenException() + { + SetUnAuthenticatedUser(); + + var mediator = GetService(); + + await Assert.ThrowsAsync(() => + mediator.Send(new DeleteRegionCommand() + { + Uuid = Guid.NewGuid() + }, TestContext.Current.CancellationToken)); + } + + // TODO: Add more tests with unauthenticated user + // (copy tests with admin role) + + // TODO: Add test for GetRegion and GetRegionPage + + // [Theory] + // // Empty + // [InlineData("")] + // // Length > 64 (65) + // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + // public async Task + // AddRegion_WithInvalidName_WithAdminRole_ThrowsValidationException + // (string name) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new AddRegionCommand() + // { + // Name = name + // })); + // } + // + // [Fact] + // public async Task AddRegion_WithUserRole_ThrowsForbiddenException() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new AddRegionCommand() + // { + // Name = "Name" + // })); + // } + // + // [Fact] + // public async Task + // AddRegion_WithUnAuthenticatedUser_ThrowsUnAuthorizedException() + // { + // SetUnAuthenticatedUser(); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new AddRegionCommand() + // { + // Name = "Name" + // })); + // } + // + // [Fact] + // public async Task UpdateRegion_WithAdminRole_RegionUpdated() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // var createRegionResult = await mediator.Send( + // new AddRegionCommand() + // { + // Name = "Name" + // }); + // + // string newName = "Different Name"; + // + // var editRegionResult = await mediator.Send( + // new UpdateRegionCommand() + // { + // Guid = createRegionResult.Uuid, + // Name = newName + // }); + // + // var getRegionResult = await mediator.Send( + // new GetRegionQuery() + // { + // Guid = createRegionResult.Uuid, + // }); + // + // Assert.NotNull(getRegionResult); + // Assert.Equal(newName, getRegionResult.Name); + // Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); + // } + // + // [Fact] + // public async Task + // UpdateDuplicateRegion_WithAdminRole_DuplicateEntityExceptionThrown() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // var createRegionResult1 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = "Name 1" + // }); + // + // var createRegionResult2 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = "Name 2" + // }); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new UpdateRegionCommand() + // { + // Guid = createRegionResult2.Uuid, + // Name = createRegionResult1.Name + // })); + // } + // + // [Theory] + // // Empty + // [InlineData("")] + // // Length > 64 (65) + // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + // public async Task + // UpdateRegion_WithInvalidName_WithAdminRole_ThrowsValidationException + // (string name) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // var createRegionResult = await mediator.Send( + // new AddRegionCommand() + // { + // Name = "Name 1" + // }); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new UpdateRegionCommand() + // { + // Guid = createRegionResult.Uuid, + // Name = name + // })); + // } + // + // [Theory] + // [InlineData("")] + // [InlineData("not an uuid")] + // public async Task + // UpdateRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException + // (string uuid) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new UpdateRegionCommand() + // { + // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty, + // Name = "Name" + // })); + // } + // + // [Fact] + // public async Task + // UpdateRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new UpdateRegionCommand() + // { + // Guid = Guid.NewGuid(), + // Name = "Name" + // })); + // } + // + // [Fact] + // public async Task UpdateRegion_WithUserRole_ThrowsForbiddenException() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new UpdateRegionCommand() + // { + // Guid = Guid.NewGuid(), + // Name = "Name" + // })); + // } + // + // // TODO: Add more tests with user role (copy tests with admin role) + // + // [Fact] + // public async Task UpdateRegion_UnAuthnticatedUser_ThrowsForbiddenException() + // { + // SetUnAuthenticatedUser(); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new UpdateRegionCommand() + // { + // Guid = Guid.NewGuid(), + // Name = "Name" + // })); + // } + // + // // TODO: Add more tests with unauthenticated user + // // (copy tests with admin role) + // + // [Fact] + // public async Task DeleteRegion_WithAdminRole_RegionDeleted() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // var createRegionResult = await mediator.Send( + // new AddRegionCommand() + // { + // Name = "Name" + // }); + // + // await mediator.Send( + // new DeleteRegionCommand() + // { + // Guid = createRegionResult.Uuid, + // }); + // + // await Assert.ThrowsAsync(() => + // mediator.Send( + // new GetRegionQuery() + // { + // Guid = createRegionResult.Uuid, + // })); + // } + // + // [Theory] + // [InlineData("")] + // [InlineData("not an uuid")] + // public async Task + // DeleteRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException + // (string uuid) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new DeleteRegionCommand() + // { + // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + // })); + // } + // + // [Fact] + // public async Task + // DeleteRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new DeleteRegionCommand() + // { + // Guid = Guid.NewGuid() + // })); + // } + // + // [Fact] + // public async Task DeleteRegion_WithUserRole_ThrowsForbiddenException() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new DeleteRegionCommand() + // { + // Guid = Guid.NewGuid() + // })); + // } + // + // // TODO: Add more tests with user role (copy tests with admin role) + // + // [Fact] + // public async Task DeleteRegion_UnAuthnticatedUser_ThrowsForbiddenException() + // { + // SetUnAuthenticatedUser(); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new DeleteRegionCommand() + // { + // Guid = Guid.NewGuid() + // })); + // } + // + // // TODO: Add more tests with unauthenticated user + // // (copy tests with admin role) + // + // [Fact] + // public async Task GetRegion_WithAdminRole_RegionReturned() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // string name = "Name"; + // + // var createRegionResult = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name + // }); + // + // var getRegionResult = await mediator.Send( + // new GetRegionQuery() + // { + // Guid = createRegionResult.Uuid, + // }); + // + // Assert.NotNull(getRegionResult); + // Assert.NotNull(getRegionResult.Name); + // Assert.Equal(name, getRegionResult.Name); + // Assert.NotNull(getRegionResult.Uuid); + // Assert.Equal(createRegionResult.Uuid, getRegionResult.Uuid); + // } + // + // [Theory] + // [InlineData("")] + // [InlineData("not an uuid")] + // public async Task + // GetRegion_WithInvalidUuid_WithAdminRole_ThrowsValidationException + // (string uuid) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send( + // new GetRegionQuery() + // { + // Guid = Guid.TryParse(uuid, out var guid) ? guid : Guid.Empty + // })); + // } + // + // [Fact] + // public async Task + // GetRegion_WithNonExistentUuid_WithAdminRole_ThrowsNotFoundException() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send( + // new GetRegionQuery() + // { + // Guid = Guid.NewGuid() + // })); + // } + // + // [Fact] + // public async Task GetRegion_WithUserRole_ThrowsForbiddenException() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.User }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send( + // new GetRegionQuery() + // { + // Guid = Guid.NewGuid() + // })); + // } + // + // // TODO: Add more tests with user role (copy tests with admin role) + // + // [Fact] + // public async Task GetRegion_UnAuthnticatedUser_ThrowsForbiddenException() + // { + // SetUnAuthenticatedUser(); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send(new GetRegionQuery() + // { + // Guid = Guid.NewGuid() + // })); + // } + // + // // TODO: Add more tests with unauthenticated user + // // (copy tests with admin role) + // + // [Fact] + // public async Task GetRegionsPage_WithAdminRole_RegionsPageReturned() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // string name1 = "Name 1"; + // string name2 = "Name 2"; + // + // var createRegionResult1 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name1 + // }); + // + // var createRegionResult2 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name2 + // }); + // + // var getRegionsResult = await mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = 1, + // PageSize = 1 + // }); + // + // Assert.NotNull(getRegionsResult); + // Assert.NotNull(getRegionsResult.PageNumber); + // Assert.Equal(1, getRegionsResult.PageNumber); + // Assert.NotNull(getRegionsResult.TotalCount); + // Assert.Equal(2, getRegionsResult.TotalCount); + // Assert.NotNull(getRegionsResult.TotalPages); + // Assert.Equal(2, getRegionsResult.TotalPages); + // Assert.NotNull(getRegionsResult.HasNextPage); + // Assert.Equal(true, getRegionsResult.HasNextPage); + // Assert.NotNull(getRegionsResult.HasPreviousPage); + // Assert.Equal(false, getRegionsResult.HasPreviousPage); + // Assert.NotNull(getRegionsResult.Items); + // Assert.Equal(1, getRegionsResult.Items.Count()); + // Assert.NotNull(getRegionsResult.Items.First().Name); + // Assert.Equal( + // createRegionResult1.Name, getRegionsResult.Items.First().Name); + // Assert.NotNull(getRegionsResult.Items.First().Uuid); + // Assert.Equal( + // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + // + // getRegionsResult = await mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = 2, + // PageSize = 1 + // }); + // + // Assert.NotNull(getRegionsResult); + // Assert.NotNull(getRegionsResult.PageNumber); + // Assert.Equal(2, getRegionsResult.PageNumber); + // Assert.NotNull(getRegionsResult.TotalCount); + // Assert.Equal(2, getRegionsResult.TotalCount); + // Assert.NotNull(getRegionsResult.TotalPages); + // Assert.Equal(2, getRegionsResult.TotalPages); + // Assert.NotNull(getRegionsResult.HasNextPage); + // Assert.Equal(false, getRegionsResult.HasNextPage); + // Assert.NotNull(getRegionsResult.HasPreviousPage); + // Assert.Equal(true, getRegionsResult.HasPreviousPage); + // Assert.NotNull(getRegionsResult.Items); + // Assert.Equal(1, getRegionsResult.Items.Count()); + // Assert.NotNull(getRegionsResult.Items.First().Name); + // Assert.Equal( + // createRegionResult2.Name, getRegionsResult.Items.First().Name); + // Assert.NotNull(getRegionsResult.Items.First().Uuid); + // Assert.Equal( + // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + // + // getRegionsResult = await mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = 1, + // PageSize = 10 + // }); + // + // Assert.NotNull(getRegionsResult); + // Assert.NotNull(getRegionsResult.PageNumber); + // Assert.Equal(1, getRegionsResult.PageNumber); + // Assert.NotNull(getRegionsResult.TotalCount); + // Assert.Equal(2, getRegionsResult.TotalCount); + // Assert.NotNull(getRegionsResult.TotalPages); + // Assert.Equal(1, getRegionsResult.TotalPages); + // Assert.NotNull(getRegionsResult.HasNextPage); + // Assert.Equal(false, getRegionsResult.HasNextPage); + // Assert.NotNull(getRegionsResult.HasPreviousPage); + // Assert.Equal(false, getRegionsResult.HasPreviousPage); + // Assert.NotNull(getRegionsResult.Items); + // Assert.Equal(2, getRegionsResult.Items.Count()); + // Assert.NotNull(getRegionsResult.Items.First().Name); + // Assert.Equal( + // createRegionResult1.Name, getRegionsResult.Items.First().Name); + // Assert.NotNull(getRegionsResult.Items.First().Uuid); + // Assert.Equal( + // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + // Assert.NotNull(getRegionsResult.Items.Last().Name); + // Assert.Equal( + // createRegionResult2.Name, getRegionsResult.Items.Last().Name); + // Assert.NotNull(getRegionsResult.Items.Last().Uuid); + // Assert.Equal( + // createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); + // } + // + // [Fact] + // public async Task + // GetRegionsPage_WithSearch_WithAdminRole_SearchedRegionsPageReturned() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // string name1 = "Name 1"; + // string name2 = "Some 3 String"; + // string name3 = "3 Name Some"; + // + // var createRegionResult1 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name1 + // }); + // + // var createRegionResult2 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name2 + // }); + // + // var createRegionResult3 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name3 + // }); + // + // var getRegionsResult = await mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = 1, + // PageSize = 10, + // Search = "name" + // }); + // + // Assert.NotNull(getRegionsResult); + // Assert.NotNull(getRegionsResult.PageNumber); + // Assert.Equal(1, getRegionsResult.PageNumber); + // Assert.NotNull(getRegionsResult.TotalCount); + // Assert.Equal(2, getRegionsResult.TotalCount); + // Assert.NotNull(getRegionsResult.TotalPages); + // Assert.Equal(1, getRegionsResult.TotalPages); + // Assert.NotNull(getRegionsResult.HasNextPage); + // Assert.Equal(false, getRegionsResult.HasNextPage); + // Assert.NotNull(getRegionsResult.HasPreviousPage); + // Assert.Equal(false, getRegionsResult.HasPreviousPage); + // Assert.NotNull(getRegionsResult.Items); + // Assert.Equal(2, getRegionsResult.Items.Count()); + // Assert.NotNull(getRegionsResult.Items.First().Name); + // Assert.Equal( + // createRegionResult1.Name, getRegionsResult.Items.First().Name); + // Assert.NotNull(getRegionsResult.Items.First().Uuid); + // Assert.Equal( + // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + // Assert.NotNull(getRegionsResult.Items.Last().Name); + // Assert.Equal( + // createRegionResult3.Name, getRegionsResult.Items.Last().Name); + // Assert.NotNull(getRegionsResult.Items.Last().Uuid); + // Assert.Equal( + // createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); + // + // getRegionsResult = await mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = 1, + // PageSize = 10, + // Search = "3" + // }); + // + // Assert.NotNull(getRegionsResult); + // Assert.NotNull(getRegionsResult.PageNumber); + // Assert.Equal(1, getRegionsResult.PageNumber); + // Assert.NotNull(getRegionsResult.TotalCount); + // Assert.Equal(2, getRegionsResult.TotalCount); + // Assert.NotNull(getRegionsResult.TotalPages); + // Assert.Equal(1, getRegionsResult.TotalPages); + // Assert.NotNull(getRegionsResult.HasNextPage); + // Assert.Equal(false, getRegionsResult.HasNextPage); + // Assert.NotNull(getRegionsResult.HasPreviousPage); + // Assert.Equal(false, getRegionsResult.HasPreviousPage); + // Assert.NotNull(getRegionsResult.Items); + // Assert.Equal(2, getRegionsResult.Items.Count()); + // Assert.NotNull(getRegionsResult.Items.First().Name); + // Assert.Equal( + // createRegionResult2.Name, getRegionsResult.Items.First().Name); + // Assert.NotNull(getRegionsResult.Items.First().Uuid); + // Assert.Equal( + // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + // Assert.NotNull(getRegionsResult.Items.Last().Name); + // Assert.Equal( + // createRegionResult3.Name, getRegionsResult.Items.Last().Name); + // Assert.NotNull(getRegionsResult.Items.Last().Uuid); + // Assert.Equal( + // createRegionResult3.Uuid, getRegionsResult.Items.Last().Uuid); + // } + // + // [Fact] + // public async Task + // GetRegionsPage_WithSort_WithAdminRole_SortedRegionsPageReturned() + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // string name1 = "Name 1"; + // string name2 = "Some 2"; + // + // var createRegionResult1 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name1 + // }); + // + // var createRegionResult2 = await mediator.Send( + // new AddRegionCommand() + // { + // Name = name2 + // }); + // + // var getRegionsResult = await mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = 1, + // PageSize = 10, + // Sort = "-name" + // }); + // + // Assert.NotNull(getRegionsResult); + // Assert.NotNull(getRegionsResult.PageNumber); + // Assert.Equal(1, getRegionsResult.PageNumber); + // Assert.NotNull(getRegionsResult.TotalCount); + // Assert.Equal(2, getRegionsResult.TotalCount); + // Assert.NotNull(getRegionsResult.TotalPages); + // Assert.Equal(1, getRegionsResult.TotalPages); + // Assert.NotNull(getRegionsResult.HasNextPage); + // Assert.Equal(false, getRegionsResult.HasNextPage); + // Assert.NotNull(getRegionsResult.HasPreviousPage); + // Assert.Equal(false, getRegionsResult.HasPreviousPage); + // Assert.NotNull(getRegionsResult.Items); + // Assert.Equal(2, getRegionsResult.Items.Count()); + // Assert.NotNull(getRegionsResult.Items.First().Name); + // Assert.Equal( + // createRegionResult2.Name, getRegionsResult.Items.First().Name); + // Assert.NotNull(getRegionsResult.Items.First().Uuid); + // Assert.Equal( + // createRegionResult2.Uuid, getRegionsResult.Items.First().Uuid); + // Assert.NotNull(getRegionsResult.Items.Last().Name); + // Assert.Equal( + // createRegionResult1.Name, getRegionsResult.Items.Last().Name); + // Assert.NotNull(getRegionsResult.Items.Last().Uuid); + // Assert.Equal( + // createRegionResult1.Uuid, getRegionsResult.Items.Last().Uuid); + // + // getRegionsResult = await mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = 1, + // PageSize = 10, + // Sort = "+name" + // }); + // + // Assert.NotNull(getRegionsResult); + // Assert.NotNull(getRegionsResult.PageNumber); + // Assert.Equal(1, getRegionsResult.PageNumber); + // Assert.NotNull(getRegionsResult.TotalCount); + // Assert.Equal(2, getRegionsResult.TotalCount); + // Assert.NotNull(getRegionsResult.TotalPages); + // Assert.Equal(1, getRegionsResult.TotalPages); + // Assert.NotNull(getRegionsResult.HasNextPage); + // Assert.Equal(false, getRegionsResult.HasNextPage); + // Assert.NotNull(getRegionsResult.HasPreviousPage); + // Assert.Equal(false, getRegionsResult.HasPreviousPage); + // Assert.NotNull(getRegionsResult.Items); + // Assert.Equal(2, getRegionsResult.Items.Count()); + // Assert.NotNull(getRegionsResult.Items.First().Name); + // Assert.Equal( + // createRegionResult1.Name, getRegionsResult.Items.First().Name); + // Assert.NotNull(getRegionsResult.Items.First().Uuid); + // Assert.Equal( + // createRegionResult1.Uuid, getRegionsResult.Items.First().Uuid); + // Assert.NotNull(getRegionsResult.Items.Last().Name); + // Assert.Equal( + // createRegionResult2.Name, getRegionsResult.Items.Last().Name); + // Assert.NotNull(getRegionsResult.Items.Last().Uuid); + // Assert.Equal( + // createRegionResult2.Uuid, getRegionsResult.Items.Last().Uuid); + // } + // + // [Theory] + // // Length > 64 (65) + // [InlineData("01234567890123456789012345678901234567890123456789012345678901234")] + // public async Task + // GetRegionsPage_WithInvalidSearch_WithAdminRole_ThrowsValidationException + // (string search) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send( + // new GetRegionsPageQuery() + // { + // Search = search + // })); + // } + // + // [Theory] + // [InlineData(int.MinValue)] + // [InlineData(0)] + // public async Task + // GetRegionsPage_WithInvalidPageNumber_WithAdminRole_ThrowsValidationException + // (int pageNumber) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send( + // new GetRegionsPageQuery() + // { + // PageNumber = pageNumber + // })); + // } + // + // [Theory] + // [InlineData(int.MinValue)] + // [InlineData(0)] + // [InlineData(51)] + // [InlineData(int.MaxValue)] + // public async Task + // GetRegionsPage_WithInvalidPageSize_WithAdminRole_ThrowsValidationException + // (int pageSize) + // { + // SetAuthenticatedUserRoles(new[] { IdentityRole.Administrator }); + // + // var mediator = GetService(); + // + // await Assert.ThrowsAsync(() => + // mediator.Send( + // new GetRegionsPageQuery() + // { + // PageSize = pageSize + // })); + // } +} diff --git a/tst/Application.IntegrationTests/packages.lock.json b/tst/Application.IntegrationTests/packages.lock.json new file mode 100644 index 0000000..017ffbc --- /dev/null +++ b/tst/Application.IntegrationTests/packages.lock.json @@ -0,0 +1,1024 @@ +{ + "version": 1, + "dependencies": { + "net9.0": { + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[9.0.4, )", + "resolved": "9.0.4", + "contentHash": "f2MTUaS2EQ3lX4325ytPAISZqgBfXmY0WvgD80ji6Z20AoDNiCESxsqo6mFRwHJD/jfVKRw9FsW6+86gNre3ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[17.13.0, )", + "resolved": "17.13.0", + "contentHash": "W19wCPizaIC9Zh47w8wWI/yxuqR7/dtABwOrc8r2jX/8mUNxM2vw4fXDh+DJTeogxV+KzKwg5jNNGQVwf3LXyA==", + "dependencies": { + "Microsoft.CodeCoverage": "17.13.0", + "Microsoft.TestPlatform.TestHost": "17.13.0" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.72, )", + "resolved": "4.20.72", + "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.0.2, )", + "resolved": "3.0.2", + "contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[2.0.1, )", + "resolved": "2.0.1", + "contentHash": "aZd9scfbb2bq8i2d9LDh8A/R1DZX/M4eASfxuL3RZUHw/5VaHi8+sb9jPODVPTA/hYRsCu+2DsEOLI2AQJCrhw==", + "dependencies": { + "xunit.analyzers": "1.21.0", + "xunit.v3.assert": "[2.0.1]", + "xunit.v3.core": "[2.0.1]" + } + }, + "AspNetCore.Localizer.Json": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "Qfv5l+8X9hLRWt6aB4+fjTzz2Pt4wwrMwwfcyrY4t85sJ+7CuB8Jl9f+yccWfFsAZSODKBAz7yFXidaYslsjlA==", + "dependencies": { + "Microsoft.AspNetCore.Components": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Localization": "9.0.0" + } + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "FluentValidation": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "cyIVdQBwSipxWG8MA3Rqox7iNbUNUTK5bfJi9tIdm4CAfH71Oo5ABLP4/QyrUwuakqpUEPGtE43BDddvEehuYw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "Transitive", + "resolved": "11.11.0", + "contentHash": "viTKWaMbL3yJYgGI0DiCeavNbE9UPMWFVK2XS9nYXGbm3NDMd0/L5ER4wBzmTtW3BYh3SrlSXm9RACiKZ6stlA==", + "dependencies": { + "FluentValidation": "11.11.0", + "Microsoft.Extensions.Dependencyinjection.Abstractions": "2.1.0" + } + }, + "MediatR": { + "type": "Transitive", + "resolved": "12.4.1", + "contentHash": "0tLxCgEC5+r1OCuumR3sWyiVa+BMv3AgiU4+pz8xqTc+2q1WbUEXFOr7Orm96oZ9r9FsldgUtWvB2o7b9jDOaw==", + "dependencies": { + "MediatR.Contracts": "[2.0.1, 3.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "MediatR.Behaviors.Authorization": { + "type": "Transitive", + "resolved": "12.2.0", + "contentHash": "/rXuisxwJviu9PIffZlcZ6UY0MafX8dNtRi0bS04KciEVxkln8txJZt+rvKgerW3zKdKHfqt2EwRuiOCN9Aszg==", + "dependencies": { + "MediatR": "12.4.1", + "MediatR.Contracts": "2.0.1" + } + }, + "MediatR.Contracts": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "FYv95bNT4UwcNA+G/J1oX5OpRiSUxteXaUt2BJbRSdRNiIUNbggJF69wy6mnk2wYToaanpdXZdCwVylt96MpwQ==" + }, + "Microsoft.AspNetCore.Authentication": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "Tq6bxTOe65Ikh9dWVTEOqpvNqBGIQueO0J+zl2rQba0yP0YV66iYDkSz9MqTdRZftvJ2I5kMeRUm9Z2mjEAbUQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Core": "2.3.0", + "Microsoft.AspNetCore.DataProtection": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Extensions.WebEncoders": "8.0.11" + } + }, + "Microsoft.AspNetCore.Authentication.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "ve6uvLwKNRkfnO/QeN9M8eUJ49lCnWv/6/9p6iTEuiI6Rtsz+myaBAjdMzLuTViQY032xbTF5AdZF5BJzJJyXQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.AspNetCore.Authentication.Cookies": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "w3JPWHreXJ/Uv9CLkQtGCLwTbxZKY+94QPVi1RxcMuBTyRp+C9SdynznHEjnHWnw6QFNEHnBuHmWW3OYrvbpEQ==", + "dependencies": { + "Microsoft.AspNetCore.Authentication": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authentication.Core": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "gnLnKGawBjqBnU9fEuel3VcYAARkjyONAliaGDfMc8o8HBtfh+HrOPEoR8Xx4b2RnMb7uxdBDOvEAC7sul79ig==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http": "2.3.0", + "Microsoft.AspNetCore.Http.Extensions": "2.3.0" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0HgfWPfnjlzWFbW4pw6FYNuIMV8obVU+MUkiZ33g4UOpvZcmdWzdayfheKPZ5+EUly8SvfgW0dJwwIrW4IVLZQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "qDJlBC5pUQ/3o6/C6Vuo9CGKtV5TAe5AdKeHvDR2bgmw8vwPxsAy3KG5eU0i1C+iAUNbmq+iDTbiKt16f9pRiA==", + "dependencies": { + "Microsoft.AspNetCore.Metadata": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "xKzY0LRqWrwuPVzKIF9k1kC21NrLmIE2qPhhKlInEAdYqNe8qcMoPWZy7fo1uScHkz5g73nTqDDra3+aAV7mTQ==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "9.0.0", + "Microsoft.AspNetCore.Components.Analyzers": "9.0.0" + } + }, + "Microsoft.AspNetCore.Components.Analyzers": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "maOE1qlJ9hf1Fb7PhFLw9bgP9mWckuDOcn1uKNt9/msdJG2YHl3cPRHojYa6CxliGHIXL8Da4qPgeUc4CaOoeg==" + }, + "Microsoft.AspNetCore.Cryptography.Internal": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E4pHyEb2Ul5a6bIwraGtw9TN39a/C2asyVPEJoyItc0reV4Y26FsPcEdcXyKjBbP4kSz9iU1Cz4Yhx/aOFPpqA==" + }, + "Microsoft.AspNetCore.Cryptography.KeyDerivation": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "5v9Kj2arRrCftLKW80Hfj31HkNnjcKyw57lQhF84drvGxJlCR63J0zMM1sMM+Hc+KCQjuoDmHtjwN0uOT+X3ag==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "9.0.4" + } + }, + "Microsoft.AspNetCore.DataProtection": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "C+FhGaA8ekrfes0Ujhtkhk74Bpkt6Zt+NrMaGrCWBqW1LFzqw/pXDbMbpcAyI9hbYgZfC6+t01As4LGXbdxG4A==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.Internal": "2.3.0", + "Microsoft.AspNetCore.DataProtection.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Win32.Registry": "4.5.0", + "System.Security.Cryptography.Xml": "8.0.2", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "Microsoft.AspNetCore.DataProtection.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "71GdtUkVDagLsBt+YatfzUItnbT2vIjHxWySNE2MkgIDhqT3g4sNNxOj/0PlPTpc1+mG3ZwfUoZ61jIt1wPw7g==" + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "4ivq53W2k6Nj4eez9wc81ytfGj6HR1NaZJCpOrvghJo9zHuQF57PLhPoQH5ItyCpHXnrN/y7yJDUm+TGYzrx0w==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "2.3.0", + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "F5iHx7odAbFKBV1DNPDkFFcVmD5Tk7rk+tYm3LMQxHEFFdjlg5QcYb5XhHAefl5YaaPeG6ad+/ck8kSG3/D6kw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "I9azEG2tZ4DDHAFgv+N38e6Yhttvf+QjE2j2UYyCACE7Swm5/0uoihCMWZ87oOZYeqiEFSxbsfpT71OYHe2tpw==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.AspNetCore.WebUtilities": "2.3.0", + "Microsoft.Extensions.ObjectPool": "8.0.11", + "Microsoft.Extensions.Options": "8.0.2", + "Microsoft.Net.Http.Headers": "2.3.0" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "EY2u/wFF5jsYwGXXswfQWrSsFPmiXsniAlUWo3rv/MGYf99ZFsENDnZcQP6W3c/+xQmQXq0NauzQ7jyy+o1LDQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "2.3.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.AspNetCore.Identity": { + "type": "Transitive", + "resolved": "2.3.1", + "contentHash": "JcQ4pNXg+IISfcR95jeO2ZRt38N67MrUEj28HBmwfqD96BUyw4S54tQhrBmCOyPlf2vgNvSz/tsGAG7EgC0yRg==", + "dependencies": { + "Microsoft.AspNetCore.Authentication.Cookies": "2.3.0", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "2.3.0", + "Microsoft.AspNetCore.Hosting.Abstractions": "2.3.0", + "Microsoft.Extensions.Identity.Core": "2.3.0" + } + }, + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "IC3X6Db6H0cXdE2zGtyk/jmSwXhHbJZaiNpg7TNFV/Biu/NgO6l/GuwgE0D1U6U9pca00WsqxESkNov+WA77CA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Identity.Stores": "9.0.4" + } + }, + "Microsoft.AspNetCore.Metadata": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "X81C891nMuWgzNHyZ0C3s+blSDxRHzQHDFYQoOKtFvFuxGq3BbkLbc5CfiCqIzA/sWIfz6u8sGBgwntQwBJWBw==" + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "trbXdWzoAEUVd0PE2yTopkz4kjZaAIA7xUWekd5uBw+7xE8Do/YOVTeb9d9koPTlbtZT539aESJjSLSqD8eYrQ==", + "dependencies": { + "Microsoft.Net.Http.Headers": "2.3.0", + "System.Text.Encodings.Web": "8.0.0" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "17.13.0", + "contentHash": "9LIUy0y+DvUmEPtbRDw6Bay3rzwqFV8P4efTrK4CZhQle3M/QwLPjISghfcolmEGAPWxuJi6m98ZEfk4VR4Lfg==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "rnVGier1R0w9YEAzxOlUl8koFwq4QLwuYKiJN6NFqbCNCPrRLGW3f7x0OtL/Sp1KBMVhgffUIP6jWPppzhpa2Q==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "+5IAX0aicQYCRfN4pAjad+JPwdEYoVEM3Z1Cl8/EiEv3FVHQHdd8TJQpQIslQDDQS/UsUMb0MsOXwqOh+TJtRw==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.4", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "E0pkWzI0liqu2ogqJ1kohk2eGkYRhf5tI75HGF6IQDARsshY/0w+prGyLvNuUeV7B8I7vYQZ4CzAKYKxw7b9gQ==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cMsm1O7g9X5qbB2wjHf3BVVvGwkG+zeXQ+M91I1Bm6RfylFMImqBPzs0+vmuef7fPxr2yOzPhIfJ2wQJfmtaSw==" + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "VHjUwjvN8UKjb8xtKJ/o+dc9tTeHOW3QzlfkzX3JrUspLkPIjwMdZCcw6eS4gsFjby0NFkcXBjHtrgTjVOfO5Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "OjJ+xh/wQff5b0wiC3SPvoQqTA2boZeJQf+15+3+OJPtjBKzvxuwr25QRIu1p1t+K8ryQ8pzaoZ7eOpXfNzVGA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "YruNASPuiCjLOVxO09lpQT4e2OYvpsoD0e5NGEQKOcPCu143RDzWTNlpzcxhArBgAS0FPwQ+OEGZOWhwgWHvOA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.10", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "96NFbmjcZsO9HkSdWAwh5tn/7LKIu7cLW+zubyGV1BR1w8xpcqPXZcTW4S/0eA0d9BxyFnH8tSDRjUerWGoU/Q==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "9.0.4", + "Microsoft.EntityFrameworkCore.Relational": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyModel": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "SQLitePCLRaw.core": "2.1.10", + "System.Text.Json": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "imcZ5BGhBw5mNsWLepBbqqumWaFe0GtvyCvne2/2wsDIBRa2+Lhx4cU/pKt/4BwOizzUEOls2k1eOJQXHGMalg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "G5rEq1Qez5VJDTEyRsRUnewAspKjaY57VGsdZ8g8Ja6sXXzoiI3PpTd1t43HjHqNWD5A06MQveb2lscn+2CU+w==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KIVBrMbItnCJDd1RF4KEaE8jZwDJcDUJW5zXpbwQ05HNYTK1GveHxHK0B3SjgDJuR48GRACXAO+BLhL8h34S7g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0LN/DiIKvBrkqp7gkF3qhGIeZk6/B63PthAHjQsxymJfIBcz0kbf4/p/t4lMgggVxZ+flRi5xvTwlpPOoZk8fg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cdrjcl9RIcwt3ECbnpP0Gt1+pkjdW90mq5yFYy8D9qRj2NqFFcv3yDp141iEamsd9E218sGxK8WHaIOcrqgDJg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "TbM2HElARG7z1gxwakdppmOkm1SykPqDcu3EF97daEwSb/+TXnRrFfJtF+5FWWxcsNhbRrmLfS2WszYcab7u1A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "2IGiG3FtVnD83IA6HYGuNei8dOw455C09yEhGl8bjcY6aGZgoC6yhYvDnozw8wlTowfoG9bxVrdTsr2ACZOYHg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UY864WQ3AS2Fkc8fYLombWnjrXwYt+BEHHps0hY4sxlgqaVW06AxbpgRZjfYf8PyRbplJqruzZDB/nSLT+7RLQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4", + "Microsoft.Extensions.FileProviders.Physical": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "vVXI70CgT/dmXV3MM+n/BR2rLXEoAyoK0hQT+8MrbCMuJBiLRxnTtSrksNiASWCwOtxo/Tyy7CO8AGthbsYxnw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.FileExtensions": "9.0.4", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "UI0TQPVkS78bFdjkTodmkH0Fe8lXv9LnhGFKgKrsgUJ5a5FVdFRcgjIkBVLbGgdRhxWirxH/8IXUtEyYJx6GQg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "ACtnvl3H3M/f8Z42980JxsNu7V9PPbzys4vBs83ZewnsgKd7JeYK18OMPo0g+MxAHrpgMrjmlinXDiaSRPcVnA==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "elH2vmwNmsXuKmUeMQ4YW9ldXiF+gSGDgg1vORksob5POnpaI6caj1Hu8zaYbEuibhqCoWg0YRWDazBY3zjBfg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "gQN2o/KnBfVk6Bd71E2YsvO5lsqrqHmaepDGk+FB/C4aiQY9B0XKKNKfl5/TqcNOs9OEithm4opiMHAErMFyEw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "qkQ9V7KFZdTWNThT7ke7E/Jad38s46atSs3QUYZB8f3thBTrcrousdY4Y/tyCtcH5YjsPSiByjuN+L8W/ThMQg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.4", + "Microsoft.Extensions.FileSystemGlobbing": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "05Lh2ItSk4mzTdDWATW9nEcSybwprN8Tz42Fs5B+jwdXUpauktdAQUI1Am4sUQi2C63E5hvQp8gXvfwfg9mQGQ==" + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "nHwq9aPBdBPYXPti6wYEEfgXddfBrYC+CQLn+qISiwQq5tpfaqDZSKOJNxoe9rfQxGf1c+2wC/qWFe1QYJPYqw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.1", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Microsoft.Extensions.Identity.Core": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "KKfCsoIHFGZmmCEjZBPuvDW0pCjboMru/Z3vbEyC/OIwUVeKrdPugFyjc81i7rNSjcPcDxVvGl/Ks8HLelKocg==", + "dependencies": { + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Identity.Stores": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0F6lSngwyXzrv+qtX46nhHYBOlPxEzj0qyCCef1kvlyEYhbj8kBL13FuDk4nEPkzk1yVjZgsnXBG19+TrNdakQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.4", + "Microsoft.Extensions.Identity.Core": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4" + } + }, + "Microsoft.Extensions.Localization": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "Up8Juy8Bh+vL+fXmMWsoSg/G6rszmLFiF44aI2tpOMJE7Ln4D9s37YxOOm81am4Z+V7g8Am3AgVwHYJzi+cL/g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Localization.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" + } + }, + "Microsoft.Extensions.Localization.Abstractions": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "wc7PaRhPOnio5Csj80b3UgBWA5l6bp28EhGem7gtfpVopcwbkfPb2Sk8Cu6eBnIW3ZNf1YUgYJzwtjzZEM8+iw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "xW6QPYsqhbuWBO9/1oA43g/XPKbohJx+7G8FLQgQXIriYvY7s+gxr2wjQJfRoPO900dvvv2vVH7wZovG+M1m6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "0MXlimU4Dud6t+iNi5NEz3dO2w1HXdhoOLaYFuLPCjAsvlPQGwOT6V2KZRMLEhCAm/stSZt1AUv0XmDdkjvtbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "/kF+rSnoo3/nIwGzWsR4RgBnoTOdZ3lzz2qFRyp/GgaNid4j6hOAQrs/O+QHXhlcAdZxjg37MvtIE+pAvIgi9g==", + "dependencies": { + "Microsoft.Extensions.Configuration": "9.0.4", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.4" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "cI0lQe0js65INCTCtAgnlVJWKgzgoRHVAW1B1zwCbmcliO4IZoTf92f1SYbLeLk7FzMJ/GlCvjLvJegJ6kltmQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging": "9.0.4", + "Microsoft.Extensions.Logging.Abstractions": "9.0.4", + "Microsoft.Extensions.Logging.Configuration": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "6ApKcHNJigXBfZa6XlDQ8feJpq7SG1ogZXg6M4FiNzgd6irs3LUAzo0Pfn4F2ZI9liGnH1XIBR/OtSbZmJAV5w==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "fiFI2+58kicqVZyt/6obqoFwHiab7LC4FkQ3mmiBJ28Yy4fAvy2+v9MRnSvvlOO8chTOjKsdafFl/K9veCPo5g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "aridVhAT3Ep+vsirR1pzjaOw0Jwiob6dc73VFQn2XmDfBA2X98M8YKO1GarvsXRX7gX1Aj+hj2ijMzrMHDOm0A==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "9.0.4", + "Microsoft.Extensions.Configuration.Binder": "9.0.4", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.4", + "Microsoft.Extensions.Options": "9.0.4", + "Microsoft.Extensions.Primitives": "9.0.4" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "SPFyMjyku1nqTFFJ928JAMd0QnRe4xjE7KeKnZMWXf3xk+6e0WiOZAluYtLdbJUXtsl2cCRSi8cBquJ408k8RA==" + }, + "Microsoft.Extensions.WebEncoders": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "EwF+KaQzTa/MoIm8gciABL6xeeiGKowqyam+lPYWukTppwch1P3QeL8CpgtLs8kIWuEowpAAUrVfP1kyZsZgqg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "0lKw+f3vkmV9t3PLe6sY3xPrYrHYiMRFxuOse5CMkKPxhQYiabpfJsuk6wX2RrVQ86Dn+t/8poHpH0nbp6sFvA==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "lepOkZZTMfJCPSnWITXxV+4Wxb54g+9oIybs9YovlOzZWuR1i2DOpzaDgSe+piDJaGtnSrcUlcB9fZ5Swur7Uw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.8.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "sUyoxzg/WBZobbFLJK8loT9IILKtS9ePmWu5B11ogQqhSHppE6SRZKw0fhI6Fd16X6ey52cbbWc2rvMBC98EQA==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.8.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.0.1", + "System.IdentityModel.Tokens.Jwt": "8.0.1" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.8.0", + "contentHash": "09hnbUJh/18gUmu5nCVFMvyzAFC4l1qyc4bwSJaKzUBqHN7aNDwmSx8dE3/MMJImbvnKq9rEtkkgnrS/OUBtjA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.IdentityModel.Logging": "8.8.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "/M0wVg6tJUOHutWD3BMOUVZAioJVXe0tCpFiovzv0T9T12TBf4MnaHP0efO8TCr1a6O9RZgQeZ9Gdark8L9XdA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0", + "System.Buffers": "4.6.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==" + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.6.3", + "contentHash": "0MdowM+3IDVWE5VBzVe9NvxsE4caSbM3fO+jlWVzEBr/Vnc3BWx+uV/Ex0dLLpkxkeUKH2gGWTNLb39rw3DDqw==", + "dependencies": { + "Microsoft.Testing.Platform": "1.6.3" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.6.3", + "contentHash": "DqMZukaPo+vKzColfqd1I5qZebfISZT6ND70AOem/dYQmHsaMN0xg/JG7E0e80rwfxL7wAA4ylSg8j6KJf1Tuw==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.6.3", + "contentHash": "PXSYI5Iae29GM5636zOL8PlQD1YyOa9cfzfYLR43hrLjjK7RDJgMTvgAet3oZLgDTvz6pbzABZvhx+S/W5m8YA==", + "dependencies": { + "Microsoft.Testing.Platform": "1.6.3" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "17.13.0", + "contentHash": "bt0E0Dx+iqW97o4A59RCmUmz/5NarJ7LRL+jXbSHod72ibL5XdNm1Ke+UO5tFhBG4VwHLcSjqq9BUSblGNWamw==", + "dependencies": { + "System.Reflection.Metadata": "1.6.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "17.13.0", + "contentHash": "9GGw08Dc3AXspjekdyTdZ/wYWFlxbgcF0s7BKxzVX+hzAwpifDOdxM+ceVaaJSQOwqt3jtuNlHn3XTpKUS9x9Q==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "17.13.0", + "Newtonsoft.Json": "13.0.1" + } + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", + "dependencies": { + "System.Security.AccessControl": "4.5.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Npgsql": { + "type": "Transitive", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", + "Npgsql": "9.0.3" + } + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "UxWuisvZ3uVcVOLJQv7urM/JiQH+v3TmaJc1BLKl5Dxfm/nTzTUrqswCqg/INiYLi61AXnHo1M1JPmPqqLnAdg==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.10", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.10" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "Ii8JCbC7oiVclaE/mbDEK000EFIJ+ShRPwAvvV89GOZhQ+ZLtlnSWl6ksCNMKu/VGXA4Nfi2B7LhN/QFN9oBcw==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "mAr69tDbnf3QJpRy2nJz8Qdpebdil00fvycyByR58Cn9eARvR+UiG2Vzsp+4q1tV3ikwiYIjlXCQFc12GfebbA==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.10", + "contentHash": "uZVTi02C1SxqzgT0HqTWatIbWGb40iIkfc3FpFCpE/r7g6K0PqzDUeefL6P6HPhDtc6BacN3yQysfzP7ks+wSQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.10" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.6.0", + "contentHash": "lN6tZi7Q46zFzAbRYXTIvfXcyvQQgxnY7Xm6C6xQ9784dEL1amjM6S6Iw4ZpsvesAKnRVsM4scrDQaDqSClkjA==" + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", + "Microsoft.IdentityModel.Tokens": "8.0.1" + } + }, + "System.Linq.Dynamic.Core": { + "type": "Transitive", + "resolved": "1.6.2", + "contentHash": "piIcdelf4dGotuIjFlyu7JLIZkYTmYM0ZTLGpCcxs9iCJFflhJht0nchkIV+GS5wfA3OtC3QNjIcUqyOdBdOsA==" + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "1.6.0", + "contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ==" + }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "2.0.0", + "System.Security.Principal.Windows": "4.5.0" + } + }, + "System.Security.Cryptography.Pkcs": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" + }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "8.0.2", + "contentHash": "aDM/wm0ZGEZ6ZYJLzgqjp2FZdHbDHh6/OmpGfb7AdZ105zYmPn/83JRU2xLIbwgoNz9U1SLUTJN0v5th3qmvjA==", + "dependencies": { + "System.Security.Cryptography.Pkcs": "8.0.1" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "9.0.4", + "contentHash": "pYtmpcO6R3Ef1XilZEHgXP2xBPVORbYEzRP7dl0IAAbN8Dm+kfwio8aCKle97rAWXOExr292MuxWYurIuwN62g==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.21.0", + "contentHash": "2KvcXvsqZQnwQmdEJC4BGXCsllgMtfbhlnY7MlDu5ZsKLWDEnYT5PNqGLiQQnxQYqwkOyTPAbrCwUyI9ZfJ2fg==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "kRHFGQx8InD5VhLxUVGoNLQ+zieMHGRcqIoybggCqWJJR/YQVo/BbfVq2tlxuC/D6Fh7hciDQQtuiz0qxbkHLg==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "yAiB2YncUZ9NTzaByPElXFXZLnmksy5H9ufNqV1vAWRBWVohBnM7pwVcJG86nI7ZYnCJAulEqSTlw6O+JaXi5A==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "CtLhyvFOMvd+OG83GoYXOAtlImg9kVG0mC0Oz520BG8UkRLXXrHxQH8o4VG97FTNouCQV5luejUc2S6F3Plgsg==", + "dependencies": { + "Microsoft.Testing.Platform.MSBuild": "1.6.3", + "xunit.v3.extensibility.core": "[2.0.1]", + "xunit.v3.runner.inproc.console": "[2.0.1]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "bSJU4PidfMf9qnLzEozplpy2BrZcj+lZshuUtzuT2Itq4h7RuanldGkz/ybYTI7jThXiUUkSytnCT3+D5h1yrA==", + "dependencies": { + "xunit.v3.common": "[2.0.1]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "XhHprIUiDH1gHdlbHz2Dd2FIiooo82wkzOe+llaw+rIHij4G/vSDxLUTvhSUX6ZpbeHc1i3oEc2DahCFKidwzQ==", + "dependencies": { + "xunit.v3.common": "[2.0.1]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "2.0.1", + "contentHash": "MBk3QekEbnlueTQVfNzXbrDXRSPFUsqBppSHPmkZpcJai06bbv6CAHUZciOjxUj5Y0F2MKhdc5XSiu0bwZUrfA==", + "dependencies": { + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.6.3", + "Microsoft.Testing.Platform": "1.6.3", + "xunit.v3.extensibility.core": "[2.0.1]", + "xunit.v3.runner.common": "[2.0.1]" + } + }, + "application": { + "type": "Project", + "dependencies": { + "AspNetCore.Localizer.Json": "[1.0.1, )", + "AutoMapper": "[14.0.0, )", + "Domain": "[1.0.0, )", + "FluentValidation": "[11.11.0, )", + "MediatR": "[12.4.1, )", + "MediatR.Behaviors.Authorization": "[12.2.0, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "System.Linq.Dynamic.Core": "[1.6.2, )" + } + }, + "configuration": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "AspNetCore.Localizer.Json": "[1.0.1, )", + "Domain": "[1.0.0, )", + "FluentValidation.DependencyInjectionExtensions": "[11.11.0, )", + "Identity": "[1.0.0, )", + "Infrastructure": "[1.0.0, )", + "Microsoft.AspNetCore.Identity": "[2.3.1, )", + "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[9.0.4, )", + "Microsoft.Extensions.Configuration": "[9.0.4, )", + "Microsoft.Extensions.Configuration.CommandLine": "[9.0.4, )", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "[9.0.4, )", + "Microsoft.Extensions.Configuration.Json": "[9.0.4, )", + "Microsoft.Extensions.DependencyInjection": "[9.0.4, )", + "Microsoft.Extensions.Logging": "[9.0.4, )", + "Microsoft.Extensions.Logging.Console": "[9.0.4, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[9.0.4, )", + "Persistence": "[1.0.0, )", + "System.Text.Json": "[9.0.4, )" + } + }, + "domain": { + "type": "Project" + }, + "identity": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.4, )", + "Microsoft.AspNetCore.Identity": "[2.3.1, )", + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "[9.0.4, )", + "Microsoft.Extensions.Options": "[9.0.4, )", + "Microsoft.IdentityModel.JsonWebTokens": "[8.8.0, )", + "Microsoft.IdentityModel.Tokens": "[8.8.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } + }, + "infrastructure": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )" + } + }, + "persistence": { + "type": "Project", + "dependencies": { + "Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.InMemory": "[9.0.4, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[9.0.4, )", + "Microsoft.Extensions.Options": "[9.0.4, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[9.0.4, )" + } + } + } + } +} \ No newline at end of file diff --git a/tst/Application.IntegrationTests/xunit.runner.json b/tst/Application.IntegrationTests/xunit.runner.json new file mode 100644 index 0000000..249d815 --- /dev/null +++ b/tst/Application.IntegrationTests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +}