From 603b526a6f2b9b27a709ec924b4602b7b561e472 Mon Sep 17 00:00:00 2001 From: Adrian Franczak <44712992+Nairda015@users.noreply.github.com> Date: Wed, 30 Nov 2022 22:45:13 +0100 Subject: [PATCH 1/4] Move users to Auth0 --- IGroceryStore.sln.DotSettings.user | 1 + src/API/API.csproj | 5 +- .../AuthenticationConfiguration.cs | 72 ++++++++++ src/API/Configuration/AwsConfiguration.cs | 14 ++ src/API/Configuration/LoggingConfiguration.cs | 24 ++++ .../MassTransitConfiguration.cs} | 37 +++-- .../OpenTelemetryConfiguration.cs | 58 ++++++++ src/API/Configuration/SwaggerConfiguration.cs | 36 +++++ src/API/Initializers/EnvironmentService.cs | 10 ++ src/API/Middlewares/ExceptionMiddleware.cs | 28 +++- src/API/ModulesInstrumentation.cs | 17 --- src/API/Program.cs | 104 +++----------- src/API/appsettings.Development.json | 12 +- src/API/appsettings.json | 14 +- src/Baskets/Baskets.Core/BasketsModule.cs | 2 +- .../Features/Baskets/AddBasket.cs | 2 +- .../Features/Baskets/AddProductsToBasket.cs | 4 +- .../Features/Products/GetPricesForShop.cs | 2 +- .../Notifications/NotificationsModule.cs | 2 +- .../Allergens/Commands/AddAllergen.cs | 2 +- .../Allergens/Queries/GetAllergens.cs | 2 +- .../Features/Brands/Commands/AddBrand.cs | 2 +- .../Features/Brands/Commands/DeleteBrand.cs | 2 +- .../Features/Brands/Commands/UpdateBrand.cs | 2 +- .../Features/Brands/Queries/GetBrand.cs | 2 +- .../Features/Brands/Queries/GetBrands.cs | 2 +- .../Categories/Commands/CreateCategory.cs | 2 +- .../Categories/Commands/DeleteCategory.cs | 2 +- .../Categories/Commands/UpdateCategory.cs | 2 +- .../Categories/Queries/GetCategories.cs | 2 +- .../Countries/Queries/GetCountries.cs | 2 +- .../Products/Commands/AddAllergenToProduct.cs | 2 +- .../Products/Commands/CreateProduct.cs | 2 +- .../Products/Commands/MarkAsObsolete.cs | 6 +- .../Features/Products/Queries/FindSimilar.cs | 2 +- .../Features/Products/Queries/GetProducts.cs | 2 +- .../Persistence/Contexts/ProductsDbContext.cs | 10 +- src/Products/Products.Core/ProductsModule.cs | 4 +- src/Shared/Shared/Constants.cs | 1 - ...DateTimeService.cs => DateTimeProvider.cs} | 11 +- .../Shared/Services/EnvironmentService.cs | 14 -- src/Shared/Shared/Shared.csproj | 6 +- .../Features/Products/ReportCurrentPrice.cs | 2 +- .../Repositories/ShopsRepository.cs | 4 +- src/Shops/Shops.Core/Shops.Core.csproj | 2 +- src/Shops/Shops.Core/ShopsModule.cs | 24 +--- .../Subscribers/Products/AddProduct.cs | 8 +- .../Users.Core/Entities/LoginMetadata.cs | 26 ++++ src/Users/Users.Core/Entities/Preferences.cs | 6 + .../Users.Core/Entities/SecurityMetadata.cs | 7 + src/Users/Users.Core/Entities/TokenStore.cs | 26 ++++ src/Users/Users.Core/Entities/User.cs | 133 ++++-------------- .../Users.Core/Factories/IUserFactory.cs | 10 -- src/Users/Users.Core/Factories/UserFactory.cs | 12 -- .../Users.Core/Features/Users/GetUser.cs | 2 +- .../Users.Core/Features/Users/GetUsers.cs | 2 +- .../Users.Core/Features/Users/Register.cs | 34 ++--- src/Users/Users.Core/JWT/JwtSettings.cs | 65 --------- .../Persistence/Contexts/UsersDbContext.cs | 16 +-- .../Persistence/Mongo/DbModels/DbModels.cs | 27 ++++ .../Persistence/Mongo/UsersRepository.cs | 72 ++++++++++ .../Persistence/Seeders/UsersDbSeed.cs | 18 --- .../Users.Core/ReadModels/TokensReadModel.cs | 4 +- .../Users.Core/Services/JwtTokenManager.cs | 58 -------- src/Users/Users.Core/Users.Core.csproj | 1 + src/Users/Users.Core/UsersModule.cs | 22 +-- .../Users.Core/ValueObjects/PasswordHash.cs | 11 +- .../Users.Core/ValueObjects/RefreshToken.cs | 2 +- .../modulesettings.Development.json | 8 +- src/Users/Users.Core/modulesettings.Test.json | 8 +- src/Users/Users.Core/modulesettings.json | 8 +- src/Worker/Program.cs | 2 +- src/Worker/Worker.csproj | 4 +- tests/Shared/Shared.csproj | 6 +- .../Shops.IntegrationTests.csproj | 9 +- .../Shops.UnitTests/Shops.UnitTests.csproj | 9 +- .../Users.IntegrationTests/UserApiFactory.cs | 4 +- .../Users/GetUserTests.cs | 3 +- tools/terraform/dev/main.tf | 35 ++++- 79 files changed, 633 insertions(+), 585 deletions(-) create mode 100644 src/API/Configuration/AuthenticationConfiguration.cs create mode 100644 src/API/Configuration/AwsConfiguration.cs create mode 100644 src/API/Configuration/LoggingConfiguration.cs rename src/API/{Extensions.cs => Configuration/MassTransitConfiguration.cs} (52%) create mode 100644 src/API/Configuration/OpenTelemetryConfiguration.cs create mode 100644 src/API/Configuration/SwaggerConfiguration.cs create mode 100644 src/API/Initializers/EnvironmentService.cs delete mode 100644 src/API/ModulesInstrumentation.cs rename src/Shared/Shared/Services/{DateTimeService.cs => DateTimeProvider.cs} (55%) delete mode 100644 src/Shared/Shared/Services/EnvironmentService.cs create mode 100644 src/Users/Users.Core/Entities/LoginMetadata.cs create mode 100644 src/Users/Users.Core/Entities/Preferences.cs create mode 100644 src/Users/Users.Core/Entities/SecurityMetadata.cs create mode 100644 src/Users/Users.Core/Entities/TokenStore.cs delete mode 100644 src/Users/Users.Core/Factories/IUserFactory.cs delete mode 100644 src/Users/Users.Core/Factories/UserFactory.cs delete mode 100644 src/Users/Users.Core/JWT/JwtSettings.cs create mode 100644 src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs create mode 100644 src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs delete mode 100644 src/Users/Users.Core/Persistence/Seeders/UsersDbSeed.cs delete mode 100644 src/Users/Users.Core/Services/JwtTokenManager.cs diff --git a/IGroceryStore.sln.DotSettings.user b/IGroceryStore.sln.DotSettings.user index 88d78ab..19debab 100644 --- a/IGroceryStore.sln.DotSettings.user +++ b/IGroceryStore.sln.DotSettings.user @@ -3,6 +3,7 @@ <AssemblyExplorer> <PhysicalFolder Path="/Users/adrianfranczak/.nuget/packages/opentelemetry.instrumentation.http/1.0.0-rc9.5" Loaded="True" /> <PhysicalFolder Path="/Users/adrianfranczak/.nuget/packages/opentelemetry.instrumentation.http/1.0.0-rc9.6" Loaded="True" /> + <Assembly Path="/Users/adrianfranczak/.nuget/packages/fluentvalidation/11.3.0/lib/net7.0/FluentValidation.dll" /> </AssemblyExplorer> /Users/adrianfranczak/Library/Caches/JetBrains/Rider2022.2/resharper-host/temp/Rider/vAny/CoverageData/_IGroceryStore.1213299661/Snapshot/snapshot.utdcvr diff --git a/src/API/API.csproj b/src/API/API.csproj index 251ba30..23c6e4e 100644 --- a/src/API/API.csproj +++ b/src/API/API.csproj @@ -8,6 +8,7 @@ IGroceryStore.API preview IGroceryStore.API + f57debf8-2b9a-43cc-b82a-3b5be9176885 @@ -25,10 +26,10 @@ - + - + diff --git a/src/API/Configuration/AuthenticationConfiguration.cs b/src/API/Configuration/AuthenticationConfiguration.cs new file mode 100644 index 0000000..40c9c73 --- /dev/null +++ b/src/API/Configuration/AuthenticationConfiguration.cs @@ -0,0 +1,72 @@ +using System.Security.Claims; +using FluentValidation; +using IGroceryStore.Shared.Settings; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; + +namespace IGroceryStore.API.Configuration; + +public class Auth0Settings : SettingsBase, ISettings +{ + public static string SectionName => "Authentication:Schemes:Bearer"; + + public string ValidIssuer { get; set; } + public string ValidAudience { get; set; } + + public Auth0Settings() + { + RuleFor(x => ValidIssuer).NotEmpty(); + RuleFor(x => ValidAudience).NotEmpty(); + } +} + +public static class AuthenticationConfiguration +{ + public static void ConfigureAuthentication(this WebApplicationBuilder builder) + { + var options = builder.Configuration.GetOptions(); + + builder.Services.AddAuthentication(o => + { + o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(o => + { + //Is this validated automatically by asp.net core? + o.Authority = options.ValidIssuer; + o.Audience = options.ValidAudience; + }); + + builder.Services.AddAuthorization(o => + { + o.AddPolicy("read:messages", policy + => policy.Requirements.Add(new HasScopeRequirement("read:messages")) + ); + }); + } +} + +public class HasScopeRequirement : IAuthorizationRequirement +{ + public string Scope { get; } + + public HasScopeRequirement(string scope) + { + Scope = scope ?? throw new ArgumentNullException(nameof(scope)); + } +} + +public class HasScopeHandler : AuthorizationHandler +{ + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement) + { + if (!context.User.HasClaim(c => c.Type == "scope")) return Task.CompletedTask; + + var scopes = context.User.FindFirstValue("scope")? + .Split(' '); + if (scopes == null) return Task.CompletedTask; + + if (scopes.Any(s => s == requirement.Scope)) context.Succeed(requirement); + return Task.CompletedTask; + } +} diff --git a/src/API/Configuration/AwsConfiguration.cs b/src/API/Configuration/AwsConfiguration.cs new file mode 100644 index 0000000..8b5e4f2 --- /dev/null +++ b/src/API/Configuration/AwsConfiguration.cs @@ -0,0 +1,14 @@ +using IGroceryStore.API.Initializers; + +namespace IGroceryStore.API.Configuration; + +public static class AwsConfiguration +{ + public static void ConfigureSystemManager(this WebApplicationBuilder builder) + { + if (!builder.Environment.IsDevelopment() && !builder.Environment.IsTestEnvironment()) + { + builder.Configuration.AddSystemsManager("/Production/IGroceryStore", TimeSpan.FromSeconds(30)); + } + } +} diff --git a/src/API/Configuration/LoggingConfiguration.cs b/src/API/Configuration/LoggingConfiguration.cs new file mode 100644 index 0000000..20399eb --- /dev/null +++ b/src/API/Configuration/LoggingConfiguration.cs @@ -0,0 +1,24 @@ +using Serilog; +using Serilog.Sinks.Elasticsearch; + +namespace IGroceryStore.API.Configuration; + +public static class LoggingConfiguration +{ + public static void ConfigureLogging(this WebApplicationBuilder builder) + { + builder.Host.UseSerilog((context, loggerConfiguration) => + { + loggerConfiguration + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .WriteTo.Console() + .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(context.Configuration["ElasticConfiguration:Uri"])) + { + IndexFormat = $"{context.Configuration["ApplicationName"]}-logs".Replace(".", "-"), + AutoRegisterTemplate = true + }) + .ReadFrom.Configuration(context.Configuration); + }); + } +} diff --git a/src/API/Extensions.cs b/src/API/Configuration/MassTransitConfiguration.cs similarity index 52% rename from src/API/Extensions.cs rename to src/API/Configuration/MassTransitConfiguration.cs index 761b41d..fc7806d 100644 --- a/src/API/Extensions.cs +++ b/src/API/Configuration/MassTransitConfiguration.cs @@ -1,11 +1,9 @@ -using System.Diagnostics; +using IGroceryStore.Shared.Settings; using MassTransit; -using OpenTelemetry; -using OpenTelemetry.Trace; -namespace IGroceryStore.API; +namespace IGroceryStore.API.Configuration; -public static class Extensions +public static class MassTransitConfiguration { static bool? _isRunningInContainer; @@ -29,22 +27,23 @@ public static void ConfigureMassTransit(this IBusRegistrationConfigurator config cfg.ConfigureEndpoints(context); }); } - - public static TracerProviderBuilder AddJaeger(this TracerProviderBuilder builder) + + public static void ConfigureMassTransit(this WebApplicationBuilder builder) { - return builder.AddJaegerExporter(o => + var rabbitSettings = builder.Configuration.GetOptions(); + builder.Services.AddMassTransit(bus => { - o.AgentHost = /*Extensions.IsRunningInContainer ? "jaeger" : */"localhost"; - o.AgentPort = 6831; - o.MaxPayloadSizeInBytes = 4096; - o.ExportProcessorType = ExportProcessorType.Batch; - o.BatchExportProcessorOptions = new BatchExportProcessorOptions + bus.SetKebabCaseEndpointNameFormatter(); + bus.UsingRabbitMq((ctx, cfg) => { - MaxQueueSize = 2048, - ScheduledDelayMilliseconds = 5000, - ExporterTimeoutMilliseconds = 30000, - MaxExportBatchSize = 512, - }; + cfg.Host(rabbitSettings.Host, rabbitSettings.VirtualHost, h => + { + h.Username(rabbitSettings.Username); + h.Password(rabbitSettings.Password); + }); + + cfg.ConfigureEndpoints(ctx); + }); }); } -} \ No newline at end of file +} diff --git a/src/API/Configuration/OpenTelemetryConfiguration.cs b/src/API/Configuration/OpenTelemetryConfiguration.cs new file mode 100644 index 0000000..7a31038 --- /dev/null +++ b/src/API/Configuration/OpenTelemetryConfiguration.cs @@ -0,0 +1,58 @@ +using System.Diagnostics; +using IGroceryStore.Shared.Common; +using Npgsql; +using OpenTelemetry; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; + +namespace IGroceryStore.API.Configuration; + +public static class OpenTelemetryConfiguration +{ + public static void ConfigureOpenTelemetry(this WebApplicationBuilder builder, IEnumerable modules) + { + builder.Services.AddOpenTelemetryTracing(x => + { + x.SetResourceBuilder(ResourceBuilder.CreateDefault() + .AddService("IGroceryStore") + .AddTelemetrySdk() + .AddEnvironmentVariableDetector()) + .AddHttpClientInstrumentation() + .AddAspNetCoreInstrumentation() + .AddModulesInstrumentation(modules) + .AddSource("MassTransit") + .AddEntityFrameworkCoreInstrumentation() + .AddNpgsql() + .AddAWSInstrumentation() + .AddJaeger(); + }); + } + + private static TracerProviderBuilder AddModulesInstrumentation(this TracerProviderBuilder builder, IEnumerable modules) + { + foreach (var module in modules) + { + builder.AddSource(module.Name); + } + + return builder; + } + + private static TracerProviderBuilder AddJaeger(this TracerProviderBuilder builder) + { + return builder.AddJaegerExporter(o => + { + o.AgentHost = /*Extensions.IsRunningInContainer ? "jaeger" : */"localhost"; + o.AgentPort = 6831; + o.MaxPayloadSizeInBytes = 4096; + o.ExportProcessorType = ExportProcessorType.Batch; + o.BatchExportProcessorOptions = new BatchExportProcessorOptions + { + MaxQueueSize = 2048, + ScheduledDelayMilliseconds = 5000, + ExporterTimeoutMilliseconds = 30000, + MaxExportBatchSize = 512, + }; + }); + } +} diff --git a/src/API/Configuration/SwaggerConfiguration.cs b/src/API/Configuration/SwaggerConfiguration.cs new file mode 100644 index 0000000..3b11486 --- /dev/null +++ b/src/API/Configuration/SwaggerConfiguration.cs @@ -0,0 +1,36 @@ +using Microsoft.OpenApi.Models; + +namespace IGroceryStore.API.Configuration; + +public static class SwaggerConfiguration +{ + public static void ConfigureSwagger(this WebApplicationBuilder builder) + { + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(c => + { + c.OrderActionsBy(x => x.HttpMethod); + + var securitySchema = new OpenApiSecurityScheme + { + Description = "Using the Authorization header with the Bearer scheme.", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }; + + c.AddSecurityDefinition("Bearer", securitySchema); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { securitySchema, new[] { "Bearer" } } + }); + }); + } +} diff --git a/src/API/Initializers/EnvironmentService.cs b/src/API/Initializers/EnvironmentService.cs new file mode 100644 index 0000000..e6764bc --- /dev/null +++ b/src/API/Initializers/EnvironmentService.cs @@ -0,0 +1,10 @@ +namespace IGroceryStore.API.Initializers; + +public static class EnvironmentService +{ + public const string TestEnvironment = "Test"; + public static bool IsTestEnvironment(this IHostEnvironment env) => + env.EnvironmentName == TestEnvironment; + public static bool IsEnvironment(this IHostEnvironment env, string environment) => + env.EnvironmentName == environment; +} diff --git a/src/API/Middlewares/ExceptionMiddleware.cs b/src/API/Middlewares/ExceptionMiddleware.cs index fffd1b0..cc3fef2 100644 --- a/src/API/Middlewares/ExceptionMiddleware.cs +++ b/src/API/Middlewares/ExceptionMiddleware.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics; using System.Text.Json; using IGroceryStore.Shared.Exceptions; @@ -18,6 +19,29 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) { await next(context); } + catch (UnreachableException ex) + { + context.Response.StatusCode = 500; + context.Response.Headers.Add("content-type", "application/json"); + + if (ex.InnerException is null) + { + var json = JsonSerializer.Serialize(new + { + ErrorCode = nameof(UnreachableException), + Message = "Something went wrong. Please contact support." + }); + await context.Response.WriteAsync(json); + } + else + { + var errorCode = ToUnderscoreCase(ex.InnerException.GetType().Name.Replace("Exception", string.Empty)); + var json = JsonSerializer.Serialize(new { ErrorCode = errorCode, ex.InnerException.Message }); + await context.Response.WriteAsync(json); + } + + _logger.LogCritical(ex, "UnreachableException Message: {Message}", ex.Message); + } catch (GroceryStoreException ex) { context.Response.StatusCode = (int)ex.StatusCode; @@ -61,9 +85,9 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) var json = JsonSerializer.Serialize(new {ErrorCode = errorCode, ex.Message}); await context.Response.WriteAsync(json); - _logger.LogCritical(ex, "{error} Message: {message}", ex.GetType().Name, ex.Message); + _logger.LogCritical(ex, "{Exception} Message: {Message}", ex.GetType().Name, ex.Message); } } private static string ToUnderscoreCase(string value) => string.Concat(value.Select((x, i) => i > 0 && char.IsUpper(x) && !char.IsUpper(value[i-1]) ? $"_{x}" : x.ToString())).ToLower(); -} \ No newline at end of file +} diff --git a/src/API/ModulesInstrumentation.cs b/src/API/ModulesInstrumentation.cs deleted file mode 100644 index f4ea0d5..0000000 --- a/src/API/ModulesInstrumentation.cs +++ /dev/null @@ -1,17 +0,0 @@ -using IGroceryStore.Shared.Common; -using OpenTelemetry.Trace; - -namespace IGroceryStore.API; - -public static class ModulesInstrumentation -{ - public static TracerProviderBuilder AddModulesInstrumentation(this TracerProviderBuilder builder, IEnumerable modules) - { - foreach (var module in modules) - { - builder.AddSource(module.Name); - } - - return builder; - } -} \ No newline at end of file diff --git a/src/API/Program.cs b/src/API/Program.cs index 615fe68..d90de76 100644 --- a/src/API/Program.cs +++ b/src/API/Program.cs @@ -1,22 +1,15 @@ using FluentValidation; -using IGroceryStore.API; +using IGroceryStore.API.Configuration; using IGroceryStore.API.Initializers; using IGroceryStore.API.Middlewares; using IGroceryStore.Shared; using IGroceryStore.Shared.Services; using IGroceryStore.Shared.Configuration; -using IGroceryStore.Shared.Settings; -using MassTransit; -using Npgsql; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using Serilog; -using Serilog.Sinks.Elasticsearch; var builder = WebApplication.CreateBuilder(args); + builder.Host.ConfigureModules(); builder.Host.ConfigureEnvironmentVariables(); - var (assemblies, moduleAssemblies, modules) = AppInitializer.Initialize(builder); foreach (var module in modules) @@ -24,93 +17,38 @@ module.Register(builder.Services, builder.Configuration); } -//AWS -if (!builder.Environment.IsDevelopment() && !builder.Environment.IsTestEnvironment()) -{ - builder.Configuration.AddSystemsManager("/Production/IGroceryStore", TimeSpan.FromSeconds(30)); -} - -//DateTime -builder.Services.AddSingleton(); - -//Services -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(c => { c.OrderActionsBy(x => x.HttpMethod); }); +builder.ConfigureSystemManager(); +builder.ConfigureLogging(); +builder.ConfigureAuthentication(); +builder.ConfigureMassTransit(); +builder.ConfigureSwagger(); +builder.ConfigureOpenTelemetry(modules); builder.Services.AddHttpContextAccessor(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); -//Db builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - -//Middlewares builder.Services.AddScoped(); builder.Services.AddValidatorsFromAssemblies(moduleAssemblies, includeInternalTypes: true); -//Messaging -var rabbitSettings = builder.Configuration.GetOptions(); -builder.Services.AddMassTransit(bus => -{ - bus.SetKebabCaseEndpointNameFormatter(); - bus.UsingRabbitMq((ctx, cfg) => - { - cfg.Host(rabbitSettings.Host, rabbitSettings.VirtualHost, h => - { - h.Username(rabbitSettings.Username); - h.Password(rabbitSettings.Password); - }); - - cfg.ConfigureEndpoints(ctx); - }); -}); - -//Logging -builder.Host.UseSerilog((context, loggerConfiguration) => -{ - loggerConfiguration - .Enrich.FromLogContext() - .Enrich.WithMachineName() - .WriteTo.Console() - .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(context.Configuration["ElasticConfiguration:Uri"])) - { - IndexFormat = $"{context.Configuration["ApplicationName"]}-logs".Replace(".", "-"), - AutoRegisterTemplate = true - }) - .ReadFrom.Configuration(context.Configuration); -}); - -builder.Services.AddOpenTelemetryTracing(x => -{ - x.SetResourceBuilder(ResourceBuilder.CreateDefault() - .AddService("IGroceryStore") - .AddTelemetrySdk() - .AddEnvironmentVariableDetector()) - .AddHttpClientInstrumentation() - .AddAspNetCoreInstrumentation() - .AddModulesInstrumentation(modules) - .AddSource("MassTransit") - .AddEntityFrameworkCoreInstrumentation() - .AddNpgsql() - .AddAWSInstrumentation() - .AddJaeger(); -}); - +//**********************************// var app = builder.Build(); +//**********************************// System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); app.UseSwagger(); -// TODO: is this needed? -// Configure the HTTP request pipeline. -if (!app.Environment.IsDevelopment()) +if (app.Environment.IsDevelopment()) { - app.UseMigrationsEndPoint(); + app.UseDeveloperExceptionPage(); } else { + app.UseExceptionHandler("/error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } @@ -118,7 +56,6 @@ app.UseHttpsRedirection(); app.UseRouting(); app.UseMiddleware(); - app.UseAuthentication(); app.UseAuthorization(); @@ -139,10 +76,11 @@ app.MapFallbackToFile("index.html"); -var databaseInitializer = app.Services.GetRequiredService(); -if (builder.Environment.IsDevelopment() || builder.Environment.IsTestEnvironment()) -{ - await databaseInitializer.MigrateWithEnsuredDeletedAsync(moduleAssemblies); -} +//TODO: uncomment +// var databaseInitializer = app.Services.GetRequiredService(); +// if (builder.Environment.IsDevelopment() || builder.Environment.IsTestEnvironment()) +// { +// await databaseInitializer.MigrateWithEnsuredDeletedAsync(moduleAssemblies); +// } app.Run(); diff --git a/src/API/appsettings.Development.json b/src/API/appsettings.Development.json index 5ee5691..fb263b6 100644 --- a/src/API/appsettings.Development.json +++ b/src/API/appsettings.Development.json @@ -28,5 +28,13 @@ "Username": "guest", "Password": "guest" }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "Authentication": { + "Schemes": { + "Bearer": { + "ValidAudience": "http://localhost:5000", + "ValidIssuer": "dotnet-user-jwts" + } + } + } +} \ No newline at end of file diff --git a/src/API/appsettings.json b/src/API/appsettings.json index 01c64cd..f0c1a51 100644 --- a/src/API/appsettings.json +++ b/src/API/appsettings.json @@ -29,5 +29,17 @@ "Username": "foo", "Password": "foo" }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Authentication": { + "Schemes": { + "Bearer": { + "ValidAudience": "foo", + "ValidIssuer": "foo" + } + } + } } + + +//"ValidAudience": "https://igrocerystore-dev.com", +// "ValidIssuer": "https://dev-2aavmucrgobcu1on.us.auth0.com/" \ No newline at end of file diff --git a/src/Baskets/Baskets.Core/BasketsModule.cs b/src/Baskets/Baskets.Core/BasketsModule.cs index 09ef099..7de1870 100644 --- a/src/Baskets/Baskets.Core/BasketsModule.cs +++ b/src/Baskets/Baskets.Core/BasketsModule.cs @@ -39,7 +39,7 @@ public void Use(IApplicationBuilder app) public void Expose(IEndpointRouteBuilder endpoints) { - endpoints.MapGet($"/api/{Name.ToLower()}/health", () => $"{Name} module is healthy") + endpoints.MapGet($"/api/health/{Name.ToLower()}", () => $"{Name} module is healthy") .WithTags(Constants.SwaggerTags.HealthChecks); endpoints.RegisterEndpoints(); diff --git a/src/Baskets/Baskets.Core/Features/Baskets/AddBasket.cs b/src/Baskets/Baskets.Core/Features/Baskets/AddBasket.cs index 9f245df..79f03d3 100644 --- a/src/Baskets/Baskets.Core/Features/Baskets/AddBasket.cs +++ b/src/Baskets/Baskets.Core/Features/Baskets/AddBasket.cs @@ -45,7 +45,7 @@ public AddBasketHandler( _usersCollection = usersCollection; } - public async Task HandleAsync(AddBasket command, CancellationToken cancellationToken = default) + public async Task HandleAsync(AddBasket command, CancellationToken cancellationToken) { var userId = _currentUserService.UserId; var user = await _usersCollection diff --git a/src/Baskets/Baskets.Core/Features/Baskets/AddProductsToBasket.cs b/src/Baskets/Baskets.Core/Features/Baskets/AddProductsToBasket.cs index de4314b..ceb4491 100644 --- a/src/Baskets/Baskets.Core/Features/Baskets/AddProductsToBasket.cs +++ b/src/Baskets/Baskets.Core/Features/Baskets/AddProductsToBasket.cs @@ -15,11 +15,9 @@ public record AddProductsToBasketBody(ulong BasketId, ulong ProductId); public class AddProductsToBasketEndpoint : IEndpoint { public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Baskets.MapPut("{basketId}/{productId}") - .Produces(); + builder.Baskets.MapPut("add-products-to-basket"); } - internal class AddProductsToBasketHandler : IHttpCommandHandler { private readonly EventStoreClient _client; diff --git a/src/Baskets/Baskets.Core/Features/Products/GetPricesForShop.cs b/src/Baskets/Baskets.Core/Features/Products/GetPricesForShop.cs index 4180b8e..ffe4ed4 100644 --- a/src/Baskets/Baskets.Core/Features/Products/GetPricesForShop.cs +++ b/src/Baskets/Baskets.Core/Features/Products/GetPricesForShop.cs @@ -11,7 +11,7 @@ internal record GetPricesForShop(ulong ProductId, ulong ShopId) : IHttpQuery; public class GetPricesForShopEndpoint : IEndpoint { public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Baskets.MapGet("{shopId}/{productId}") + builder.Baskets.MapGet("/{shopId}/{productId}") .Produces(); } diff --git a/src/Notifications/Notifications/NotificationsModule.cs b/src/Notifications/Notifications/NotificationsModule.cs index 0a02096..5ea9463 100644 --- a/src/Notifications/Notifications/NotificationsModule.cs +++ b/src/Notifications/Notifications/NotificationsModule.cs @@ -26,7 +26,7 @@ public void Use(IApplicationBuilder app) public void Expose(IEndpointRouteBuilder endpoints) { - endpoints.MapGet($"/api/{Name.ToLower()}/health", () => $"{Name} module is healthy") + endpoints.MapGet($"/api/health/{Name.ToLower()}", () => $"{Name} module is healthy") .WithTags(Constants.SwaggerTags.HealthChecks); endpoints.RegisterEndpoints(); diff --git a/src/Products/Products.Core/Features/Allergens/Commands/AddAllergen.cs b/src/Products/Products.Core/Features/Allergens/Commands/AddAllergen.cs index 93ed94a..bcf5b2e 100644 --- a/src/Products/Products.Core/Features/Allergens/Commands/AddAllergen.cs +++ b/src/Products/Products.Core/Features/Allergens/Commands/AddAllergen.cs @@ -28,7 +28,7 @@ public AddAllergenHandler(ProductsDbContext productsDbContext, ISnowflakeService _snowflakeService = snowflakeService; } - public async Task HandleAsync(AddAllergen command, CancellationToken cancellationToken = default) + public async Task HandleAsync(AddAllergen command, CancellationToken cancellationToken) { var allergen = new Allergen { diff --git a/src/Products/Products.Core/Features/Allergens/Queries/GetAllergens.cs b/src/Products/Products.Core/Features/Allergens/Queries/GetAllergens.cs index 4e09684..76e5666 100644 --- a/src/Products/Products.Core/Features/Allergens/Queries/GetAllergens.cs +++ b/src/Products/Products.Core/Features/Allergens/Queries/GetAllergens.cs @@ -24,7 +24,7 @@ public GetAllergensHttpHandler(ProductsDbContext context) } public async Task HandleAsync(GetAllergens query, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken) { var allergens = await _context.Allergens .Select(x => new AllergenReadModel(x.Id, x.Name)) diff --git a/src/Products/Products.Core/Features/Brands/Commands/AddBrand.cs b/src/Products/Products.Core/Features/Brands/Commands/AddBrand.cs index 009419d..dd74d8a 100644 --- a/src/Products/Products.Core/Features/Brands/Commands/AddBrand.cs +++ b/src/Products/Products.Core/Features/Brands/Commands/AddBrand.cs @@ -33,7 +33,7 @@ public AddBrandHandler(ProductsDbContext productsDbContext, ISnowflakeService sn _snowflakeService = snowflakeService; } - public async Task HandleAsync(AddBrand command, CancellationToken cancellationToken = default) + public async Task HandleAsync(AddBrand command, CancellationToken cancellationToken) { var alreadyExists = await _productsDbContext.Brands.AnyAsync(b => b.Name.Equals(command.Body.Name), cancellationToken); diff --git a/src/Products/Products.Core/Features/Brands/Commands/DeleteBrand.cs b/src/Products/Products.Core/Features/Brands/Commands/DeleteBrand.cs index 31ab829..95ac88c 100644 --- a/src/Products/Products.Core/Features/Brands/Commands/DeleteBrand.cs +++ b/src/Products/Products.Core/Features/Brands/Commands/DeleteBrand.cs @@ -25,7 +25,7 @@ public DeleteBrandHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(DeleteBrand command, CancellationToken cancellationToken = default) + public async Task HandleAsync(DeleteBrand command, CancellationToken cancellationToken) { var brand = await _productsDbContext.Brands.FirstOrDefaultAsync(x => x.Id.Equals(command.Id), cancellationToken); diff --git a/src/Products/Products.Core/Features/Brands/Commands/UpdateBrand.cs b/src/Products/Products.Core/Features/Brands/Commands/UpdateBrand.cs index 2cf8aca..5cdf9c3 100644 --- a/src/Products/Products.Core/Features/Brands/Commands/UpdateBrand.cs +++ b/src/Products/Products.Core/Features/Brands/Commands/UpdateBrand.cs @@ -27,7 +27,7 @@ public UpdateBrandHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(UpdateBrand command, CancellationToken cancellationToken = default) + public async Task HandleAsync(UpdateBrand command, CancellationToken cancellationToken) { var brand = await _productsDbContext.Brands diff --git a/src/Products/Products.Core/Features/Brands/Queries/GetBrand.cs b/src/Products/Products.Core/Features/Brands/Queries/GetBrand.cs index 7ef2d9d..4e25616 100644 --- a/src/Products/Products.Core/Features/Brands/Queries/GetBrand.cs +++ b/src/Products/Products.Core/Features/Brands/Queries/GetBrand.cs @@ -26,7 +26,7 @@ public GetBrandHttpHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(GetBrand query, CancellationToken cancellationToken = default) + public async Task HandleAsync(GetBrand query, CancellationToken cancellationToken) { var brand = await _productsDbContext.Brands .FirstOrDefaultAsync(x => x.Id == query.id, cancellationToken); diff --git a/src/Products/Products.Core/Features/Brands/Queries/GetBrands.cs b/src/Products/Products.Core/Features/Brands/Queries/GetBrands.cs index d37ae5c..354d959 100644 --- a/src/Products/Products.Core/Features/Brands/Queries/GetBrands.cs +++ b/src/Products/Products.Core/Features/Brands/Queries/GetBrands.cs @@ -23,7 +23,7 @@ public GetBrandsHttpHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(GetBrands query, CancellationToken cancellationToken = default) + public async Task HandleAsync(GetBrands query, CancellationToken cancellationToken) { var brands = await _productsDbContext.Brands .Select(c => new BrandReadModel(c.Id, c.Name)) diff --git a/src/Products/Products.Core/Features/Categories/Commands/CreateCategory.cs b/src/Products/Products.Core/Features/Categories/Commands/CreateCategory.cs index bc5fc6d..5b40314 100644 --- a/src/Products/Products.Core/Features/Categories/Commands/CreateCategory.cs +++ b/src/Products/Products.Core/Features/Categories/Commands/CreateCategory.cs @@ -35,7 +35,7 @@ public CreateCategoryHandler(ProductsDbContext productsDbContext, ISnowflakeServ _snowFlakeService = snowFlakeService; } - public async Task HandleAsync(CreateCategory command, CancellationToken cancellationToken = default) + public async Task HandleAsync(CreateCategory command, CancellationToken cancellationToken) { var isExists = await _productsDbContext.Categories.AnyAsync(x => x.Name.Equals(command.Body.Name), cancellationToken); diff --git a/src/Products/Products.Core/Features/Categories/Commands/DeleteCategory.cs b/src/Products/Products.Core/Features/Categories/Commands/DeleteCategory.cs index 453f553..7c56bc0 100644 --- a/src/Products/Products.Core/Features/Categories/Commands/DeleteCategory.cs +++ b/src/Products/Products.Core/Features/Categories/Commands/DeleteCategory.cs @@ -25,7 +25,7 @@ public DeleteCategoryHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(DeleteCategory command, CancellationToken cancellationToken = default) + public async Task HandleAsync(DeleteCategory command, CancellationToken cancellationToken) { var category = await _productsDbContext.Categories.FirstOrDefaultAsync(x => x.Id.Equals(command.Id), cancellationToken); diff --git a/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs b/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs index 0bf33bf..5ec771f 100644 --- a/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs +++ b/src/Products/Products.Core/Features/Categories/Commands/UpdateCategory.cs @@ -35,7 +35,7 @@ public UpdateCategoryHandler(ProductsDbContext productsDbContext, IBus bus) _bus = bus; } - public async Task HandleAsync(UpdateCategory command, CancellationToken cancellationToken = default) + public async Task HandleAsync(UpdateCategory command, CancellationToken cancellationToken) { var category = await _productsDbContext.Categories diff --git a/src/Products/Products.Core/Features/Categories/Queries/GetCategories.cs b/src/Products/Products.Core/Features/Categories/Queries/GetCategories.cs index d5cdbcb..9145807 100644 --- a/src/Products/Products.Core/Features/Categories/Queries/GetCategories.cs +++ b/src/Products/Products.Core/Features/Categories/Queries/GetCategories.cs @@ -26,7 +26,7 @@ public GetCategoriesHttpHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(GetCategories query, CancellationToken cancellationToken = default) + public async Task HandleAsync(GetCategories query, CancellationToken cancellationToken) { var categories = await _productsDbContext.Categories .Select(c => new CategoryReadModel(c.Id, c.Name)) diff --git a/src/Products/Products.Core/Features/Countries/Queries/GetCountries.cs b/src/Products/Products.Core/Features/Countries/Queries/GetCountries.cs index c2d654a..2cff6aa 100644 --- a/src/Products/Products.Core/Features/Countries/Queries/GetCountries.cs +++ b/src/Products/Products.Core/Features/Countries/Queries/GetCountries.cs @@ -23,7 +23,7 @@ public GetCountriesHttpHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(GetCountries query, CancellationToken cancellationToken = default) + public async Task HandleAsync(GetCountries query, CancellationToken cancellationToken) { var countries = await _productsDbContext.Countries .Select(c => new CountryReadModel(c.Id, c.Name, c.Code)) diff --git a/src/Products/Products.Core/Features/Products/Commands/AddAllergenToProduct.cs b/src/Products/Products.Core/Features/Products/Commands/AddAllergenToProduct.cs index fecb5fa..8dba9ac 100644 --- a/src/Products/Products.Core/Features/Products/Commands/AddAllergenToProduct.cs +++ b/src/Products/Products.Core/Features/Products/Commands/AddAllergenToProduct.cs @@ -26,7 +26,7 @@ public AddAllergenToProductHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(AddAllergenToProduct command, CancellationToken cancellationToken = default) + public async Task HandleAsync(AddAllergenToProduct command, CancellationToken cancellationToken) { var (productId, allergenId) = command.Body; var product = diff --git a/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs b/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs index 193d2f8..73d8aca 100644 --- a/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs +++ b/src/Products/Products.Core/Features/Products/Commands/CreateProduct.cs @@ -44,7 +44,7 @@ public CreateProductHandler(ProductsDbContext productsDbContext, IBus bus, ISnow _snowflakeService = snowflakeService; } - public async Task HandleAsync(CreateProduct command, CancellationToken cancellationToken = default) + public async Task HandleAsync(CreateProduct command, CancellationToken cancellationToken) { var (name, quantityReadModel, brandId, countryId, categoryId, description) = command.Body; var categoryName = await _productsDbContext.Categories diff --git a/src/Products/Products.Core/Features/Products/Commands/MarkAsObsolete.cs b/src/Products/Products.Core/Features/Products/Commands/MarkAsObsolete.cs index 85ed7d7..a4a55ab 100644 --- a/src/Products/Products.Core/Features/Products/Commands/MarkAsObsolete.cs +++ b/src/Products/Products.Core/Features/Products/Commands/MarkAsObsolete.cs @@ -9,13 +9,13 @@ namespace IGroceryStore.Products.Features.Products.Commands; internal record MarkAsObsolete(MarkAsObsolete.MarkAsObsoleteBody Body) : IHttpCommand { - internal record MarkAsObsoleteBody(ProductId Id); + internal record MarkAsObsoleteBody(ulong Id); } public class MarkAsObsoleteEndpoint : IEndpoint { public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Products.MapPost("mark-as-obsolete/{id}"); + builder.Products.MapPost("mark-as-obsolete"); } internal class MarkAsObsoleteHandler : IHttpCommandHandler @@ -27,7 +27,7 @@ public MarkAsObsoleteHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(MarkAsObsolete command, CancellationToken cancellationToken = default) + public async Task HandleAsync(MarkAsObsolete command, CancellationToken cancellationToken) { var product = await _productsDbContext.Products.FirstOrDefaultAsync(x => x.Id == command.Body.Id, cancellationToken); if (product == null) throw new ProductNotFoundException(command.Body.Id); diff --git a/src/Products/Products.Core/Features/Products/Queries/FindSimilar.cs b/src/Products/Products.Core/Features/Products/Queries/FindSimilar.cs index 264314a..917a632 100644 --- a/src/Products/Products.Core/Features/Products/Queries/FindSimilar.cs +++ b/src/Products/Products.Core/Features/Products/Queries/FindSimilar.cs @@ -22,7 +22,7 @@ public FindSimilarHttpHandler(ProductsDbContext context) _context = context; } - public Task HandleAsync(FindSimilar query, CancellationToken cancellationToken = default) + public Task HandleAsync(FindSimilar query, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/Products/Products.Core/Features/Products/Queries/GetProducts.cs b/src/Products/Products.Core/Features/Products/Queries/GetProducts.cs index 2d8a3a1..9257401 100644 --- a/src/Products/Products.Core/Features/Products/Queries/GetProducts.cs +++ b/src/Products/Products.Core/Features/Products/Queries/GetProducts.cs @@ -25,7 +25,7 @@ public GetProductsHandler(ProductsDbContext productsDbContext) _productsDbContext = productsDbContext; } - public async Task HandleAsync(GetProducts query, CancellationToken cancellationToken = default) + public async Task HandleAsync(GetProducts query, CancellationToken cancellationToken) { var (pageNumber, pageSize, categoryId) = query; var products = _productsDbContext.Products diff --git a/src/Products/Products.Core/Persistence/Contexts/ProductsDbContext.cs b/src/Products/Products.Core/Persistence/Contexts/ProductsDbContext.cs index 2b1dab3..1849a68 100644 --- a/src/Products/Products.Core/Persistence/Contexts/ProductsDbContext.cs +++ b/src/Products/Products.Core/Persistence/Contexts/ProductsDbContext.cs @@ -10,15 +10,15 @@ namespace IGroceryStore.Products.Persistence.Contexts; public class ProductsDbContext : DbContext { private readonly ICurrentUserService _currentUserService; - private readonly DateTimeService _dateTimeService; + private readonly IDateTimeProvider _dateTimeProvider; public ProductsDbContext(DbContextOptions options, ICurrentUserService currentUserService, - DateTimeService dateTimeService) + IDateTimeProvider dateTimeProvider) : base(options) { _currentUserService = currentUserService; - _dateTimeService = dateTimeService; + _dateTimeProvider = dateTimeProvider; } internal DbSet Products => Set(); @@ -35,12 +35,12 @@ public ProductsDbContext(DbContextOptions options, { case EntityState.Added: entry.Entity.CreatedBy = _currentUserService.UserId; - entry.Entity.Created = _dateTimeService.Now; + entry.Entity.Created = _dateTimeProvider.Now; break; case EntityState.Modified: entry.Entity.LastModifiedBy = _currentUserService.UserId; - entry.Entity.LastModified = _dateTimeService.Now; + entry.Entity.LastModified = _dateTimeProvider.Now; break; } } diff --git a/src/Products/Products.Core/ProductsModule.cs b/src/Products/Products.Core/ProductsModule.cs index 085864c..f9de150 100644 --- a/src/Products/Products.Core/ProductsModule.cs +++ b/src/Products/Products.Core/ProductsModule.cs @@ -30,11 +30,13 @@ public void Register(IServiceCollection services, IConfiguration configuration) public void Use(IApplicationBuilder app) { + //Maybe use this only in dev? + app.UseMigrationsEndPoint(); } public void Expose(IEndpointRouteBuilder endpoints) { - endpoints.MapGet($"/api/{Name.ToLower()}/health", () => $"{Name} module is healthy") + endpoints.MapGet($"/api/health/{Name.ToLower()}", () => $"{Name} module is healthy") .WithTags(Constants.SwaggerTags.HealthChecks); endpoints.RegisterEndpoints(); diff --git a/src/Shared/Shared/Constants.cs b/src/Shared/Shared/Constants.cs index e2a76be..5e5c4ac 100644 --- a/src/Shared/Shared/Constants.cs +++ b/src/Shared/Shared/Constants.cs @@ -27,7 +27,6 @@ public static class Name public const string UserId = "user-id"; public const string Role = ClaimTypes.Role; public const string RefreshToken = "refresh-token"; - public const string Expire = "exp"; } } diff --git a/src/Shared/Shared/Services/DateTimeService.cs b/src/Shared/Shared/Services/DateTimeProvider.cs similarity index 55% rename from src/Shared/Shared/Services/DateTimeService.cs rename to src/Shared/Shared/Services/DateTimeProvider.cs index a630fc1..f27c787 100644 --- a/src/Shared/Shared/Services/DateTimeService.cs +++ b/src/Shared/Shared/Services/DateTimeProvider.cs @@ -1,8 +1,15 @@ namespace IGroceryStore.Shared.Services; -public sealed class DateTimeService +public interface IDateTimeProvider +{ + DateTime Now { get; } + DateOnly NowDateOnly { get; } + TimeOnly NowTimeOnly { get; } +} + +public sealed class DateTimeProvider : IDateTimeProvider { public DateTime Now => DateTime.UtcNow; public DateOnly NowDateOnly => DateOnly.FromDateTime(DateTime.UtcNow); public TimeOnly NowTimeOnly => TimeOnly.FromDateTime(DateTime.UtcNow); -} \ No newline at end of file +} diff --git a/src/Shared/Shared/Services/EnvironmentService.cs b/src/Shared/Shared/Services/EnvironmentService.cs deleted file mode 100644 index 96ff89c..0000000 --- a/src/Shared/Shared/Services/EnvironmentService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.Extensions.Hosting; - -namespace IGroceryStore.Shared.Services -{ - public static class EnvironmentService - { - public const string TestEnvironment = "Test"; - public static bool IsTestEnvironment(this IHostEnvironment env) => - env.EnvironmentName == TestEnvironment; - public static bool IsEnvironment(this IHostEnvironment env, string environment) => - env.EnvironmentName == environment; - - } -} diff --git a/src/Shared/Shared/Shared.csproj b/src/Shared/Shared/Shared.csproj index 070641a..9a12f55 100644 --- a/src/Shared/Shared/Shared.csproj +++ b/src/Shared/Shared/Shared.csproj @@ -15,11 +15,11 @@ - + - + - + diff --git a/src/Shops/Shops.Core/Features/Products/ReportCurrentPrice.cs b/src/Shops/Shops.Core/Features/Products/ReportCurrentPrice.cs index 997baa4..c27e4fe 100644 --- a/src/Shops/Shops.Core/Features/Products/ReportCurrentPrice.cs +++ b/src/Shops/Shops.Core/Features/Products/ReportCurrentPrice.cs @@ -40,7 +40,7 @@ public CreateProductHandler(IProductsRepository productsRepository, _logger = logger; } - public async Task HandleAsync(ReportCurrentPrice command, CancellationToken cancellationToken = default) + public async Task HandleAsync(ReportCurrentPrice command, CancellationToken cancellationToken) { //from ui user will click on map and select shop var (shopChainId, shopId, productId, price) = command.Body; diff --git a/src/Shops/Shops.Core/Repositories/ShopsRepository.cs b/src/Shops/Shops.Core/Repositories/ShopsRepository.cs index 0e0bf4c..2930e16 100644 --- a/src/Shops/Shops.Core/Repositories/ShopsRepository.cs +++ b/src/Shops/Shops.Core/Repositories/ShopsRepository.cs @@ -21,10 +21,8 @@ public interface IShopsRepository internal class ShopsRepository : IShopsRepository { private readonly IAmazonDynamoDB _dynamoDb; - private readonly IOptions _settings; - public ShopsRepository(IOptions settings, IAmazonDynamoDB dynamoDb) + public ShopsRepository(IAmazonDynamoDB dynamoDb) { - _settings = settings; _dynamoDb = dynamoDb; } diff --git a/src/Shops/Shops.Core/Shops.Core.csproj b/src/Shops/Shops.Core/Shops.Core.csproj index 2b06929..84332fc 100644 --- a/src/Shops/Shops.Core/Shops.Core.csproj +++ b/src/Shops/Shops.Core/Shops.Core.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Shops/Shops.Core/ShopsModule.cs b/src/Shops/Shops.Core/ShopsModule.cs index 91ffca6..778229c 100644 --- a/src/Shops/Shops.Core/ShopsModule.cs +++ b/src/Shops/Shops.Core/ShopsModule.cs @@ -41,7 +41,7 @@ public void Register(IServiceCollection services, IConfiguration configuration) services.AddSingleton(); services.AddSingleton(); - //services.AddScoped(); + services.AddSingleton(); } public void Use(IApplicationBuilder app) @@ -50,29 +50,9 @@ public void Use(IApplicationBuilder app) public void Expose(IEndpointRouteBuilder endpoints) { - endpoints.MapGet($"/api/{Name.ToLower()}/health", () => $"{Name} module is healthy") + endpoints.MapGet($"/api/health/{Name.ToLower()}", () => $"{Name} module is healthy") .WithTags(Constants.SwaggerTags.HealthChecks); endpoints.RegisterEndpoints(); } } - -// public static class Temp -// { -// public static void RegisterEndpoints2(this IGroceryStoreRouteBuilder endpoints) -// where T : class, IModule -// { -// var assembly = Assembly.GetAssembly(typeof(T)); -// var moduleEndpoints = assembly! -// .GetTypes() -// .Where(x => typeof(IEndpoint2).IsAssignableFrom(x) && x.IsClass) -// .OrderBy(x => x.Name) -// .Select(Activator.CreateInstance) -// .Cast() -// .ToList(); -// -// Console.Write(string.Join(' ', moduleEndpoints.Select(x => x.GetType().FullName))); -// -// moduleEndpoints.ForEach(x => x.RegisterEndpoint(endpoints)); -// } -// } diff --git a/src/Shops/Shops.Core/Subscribers/Products/AddProduct.cs b/src/Shops/Shops.Core/Subscribers/Products/AddProduct.cs index a6edf74..34fce04 100644 --- a/src/Shops/Shops.Core/Subscribers/Products/AddProduct.cs +++ b/src/Shops/Shops.Core/Subscribers/Products/AddProduct.cs @@ -12,15 +12,15 @@ public class AddProduct : IConsumer { private readonly ILogger _logger; private readonly IProductsRepository _productsRepository; - private readonly DateTimeService _dateTimeService; + private readonly IDateTimeProvider _dateTimeProvider; public AddProduct(ILogger logger, IProductsRepository productsRepository, - DateTimeService dateTimeService) + IDateTimeProvider dateTimeProvider) { _logger = logger; _productsRepository = productsRepository; - _dateTimeService = dateTimeService; + _dateTimeProvider = dateTimeProvider; } public async Task Consume(ConsumeContext context) @@ -31,7 +31,7 @@ public async Task Consume(ConsumeContext context) { Id = productId, Name = name, - LastUpdated = _dateTimeService.NowDateOnly + LastUpdated = _dateTimeProvider.NowDateOnly }; var result = await _productsRepository.AddAsync(product, context.CancellationToken); diff --git a/src/Users/Users.Core/Entities/LoginMetadata.cs b/src/Users/Users.Core/Entities/LoginMetadata.cs new file mode 100644 index 0000000..bac19d1 --- /dev/null +++ b/src/Users/Users.Core/Entities/LoginMetadata.cs @@ -0,0 +1,26 @@ +namespace IGroceryStore.Users.Entities; + +internal sealed class LoginMetadata +{ + public LoginMetadata(ushort accessFailedCount = 0, DateTime? lockoutEnd = null) + { + AccessFailedCount = accessFailedCount; + LockoutEnd = lockoutEnd; + } + + private const int MaxLoginTry = 5; + public ushort AccessFailedCount { get; private set; } + public DateTime? LockoutEnd { get; private set; } + public bool IsLocked => LockoutEnd.HasValue && LockoutEnd.Value > DateTime.UtcNow; + + internal void ReportLoginFailure() + { + AccessFailedCount++; + if (AccessFailedCount >= MaxLoginTry) + { + Lock(); + } + } + + private void Lock() => LockoutEnd = DateTime.UtcNow.AddMinutes(5); +} diff --git a/src/Users/Users.Core/Entities/Preferences.cs b/src/Users/Users.Core/Entities/Preferences.cs new file mode 100644 index 0000000..decc397 --- /dev/null +++ b/src/Users/Users.Core/Entities/Preferences.cs @@ -0,0 +1,6 @@ +namespace IGroceryStore.Users.Entities; + +internal class Preferences +{ + public string? ColorScheme { get; set; } +} diff --git a/src/Users/Users.Core/Entities/SecurityMetadata.cs b/src/Users/Users.Core/Entities/SecurityMetadata.cs new file mode 100644 index 0000000..cd42933 --- /dev/null +++ b/src/Users/Users.Core/Entities/SecurityMetadata.cs @@ -0,0 +1,7 @@ +namespace IGroceryStore.Users.Entities; + +internal sealed class SecurityMetadata +{ + public bool TwoFactorEnabled { get; set; } + public bool EmailConfirmed { get; set; } +} diff --git a/src/Users/Users.Core/Entities/TokenStore.cs b/src/Users/Users.Core/Entities/TokenStore.cs new file mode 100644 index 0000000..d124910 --- /dev/null +++ b/src/Users/Users.Core/Entities/TokenStore.cs @@ -0,0 +1,26 @@ +using IGroceryStore.Users.ValueObjects; + +namespace IGroceryStore.Users.Entities; + +internal sealed class TokenStore +{ + public TokenStore() => RemoveOldRefreshTokens(); + + private readonly List _refreshTokens = new(); + public IReadOnlyCollection RefreshTokens => _refreshTokens.AsReadOnly(); + + private void RemoveOldRefreshTokens() + => _refreshTokens.RemoveAll(x => x.ExpiresAt <= DateTime.UtcNow); + + internal void AddRefreshToken(RefreshToken refreshToken) + => _refreshTokens.Add(refreshToken); + + public bool TokenExist(string token) + => _refreshTokens.Exists(x => token == x.Value); + + public bool IsJtiValid(string token, Guid jti) + => _refreshTokens.Exists(x => token == x.Value && jti == x.Jti); + + public void RemoveAllRefreshTokens() + => _refreshTokens.RemoveAll(_ => true); +} diff --git a/src/Users/Users.Core/Entities/User.cs b/src/Users/Users.Core/Entities/User.cs index e317b4e..5293704 100644 --- a/src/Users/Users.Core/Entities/User.cs +++ b/src/Users/Users.Core/Entities/User.cs @@ -1,135 +1,56 @@ -using System.Net; +using System.Diagnostics; +using System.Net; using IGroceryStore.Shared.Common; using IGroceryStore.Shared.Exceptions; using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Exceptions; using IGroceryStore.Users.Services; using IGroceryStore.Users.ValueObjects; -using OneOf; namespace IGroceryStore.Users.Entities; -public sealed class User : AuditableEntity +internal sealed class User : AuditableEntity { - private User() + public User(PasswordHash passwordHash) { - } - - internal User(UserId id, - FirstName firstName, - LastName lastName, - Email email, - PasswordHash passwordHash) - { - Id = id; - FirstName = firstName; - LastName = lastName; - Email = email; - _passwordHash = passwordHash; - } - - private const int MaxLoginTry = 5; - private PasswordHash _passwordHash; - private List _refreshTokens = new(); - private ushort _accessFailedCount; - private DateTime _lockoutEnd; - public UserId Id { get; } - public FirstName FirstName { get; private set; } - public LastName LastName { get; private set; } - public Email Email { get; private set; } - public bool TwoFactorEnabled { get; private set; } - public bool EmailConfirmed { get; private set; } - public bool LockoutEnabled { get; private set; } - private void UpdatePassword(string password, string oldPassword) - { - if (!HashingService.ValidatePassword(oldPassword, _passwordHash.Value)) - { - _accessFailedCount++; - throw new IncorrectPasswordException(); - } - _passwordHash = HashingService.HashPassword(password); - } - - private void UpdateEmail(string email) - { - Email = email; + PasswordHash = passwordHash; } - private void ConfirmEmail() - { - EmailConfirmed = true; - } - - internal bool EnableTwoTwoFactor() - { - TwoFactorEnabled = true; - throw new NotImplementedException(); - return true; - } - - private void Lock() - { - LockoutEnabled = true; - _lockoutEnd = DateTime.UtcNow.AddMinutes(5); - } + public required UserId Id { get; init; } + public required FirstName FirstName { get; set; } + public required LastName LastName { get; set; } + public required Email Email { get; set; } + public PasswordHash PasswordHash { get; private set; } + public LoginMetadata LoginMetadata { get; init; } = new(); + public TokenStore TokenStore { get; init; } = new(); + public SecurityMetadata SecurityMetadata { get; init; } = new(); + public Preferences Preferences { get; init; } = new(); - private void Unlock() - { - LockoutEnabled = false; - _accessFailedCount = 0; - } - - private bool TryUnlock() + public bool UpdatePassword(string password, string oldPassword) { - if (_lockoutEnd > DateTime.UtcNow) return false; - Unlock(); + if (!HashingService.ValidatePassword(oldPassword, PasswordHash.Value)) return false; + + PasswordHash = HashingService.HashPassword(password); return true; } - - internal OneOf Login(string password) + public bool IsPasswordCorrect(string password) { - if (!TryUnlock()) return new LoggingTriesExceededException(MaxLoginTry); + if (IsLocked()) throw new UnreachableException( + "This should be checked before", + new LoggingTriesExceededException()); - if (!HashingService.ValidatePassword(password, _passwordHash.Value)) - { - _accessFailedCount++; - return false; - } - if (_accessFailedCount <= MaxLoginTry) return true; - Lock(); + if (HashingService.ValidatePassword(password, PasswordHash.Value)) return true; + LoginMetadata.ReportLoginFailure(); return false; } - - internal void AddRefreshToken(RefreshToken refreshToken) - { - _refreshTokens.Add(refreshToken); - } - - public bool TokenExist(RefreshToken refreshToken) - => _refreshTokens.Exists(x => x.Equals(refreshToken)); - public bool TokenExist(string token) - => _refreshTokens.Exists(x => x.Value == token); - public void UpdateRefreshToken(string oldTokenValue, string newTokenValue) - { - var token = _refreshTokens.First(x => x.Value == oldTokenValue); - var newToken = token with {Value = newTokenValue}; - _refreshTokens.RemoveAll(x => x.Value == oldTokenValue); - _refreshTokens.Add(newToken); - } - - public void TryRemoveOldRefreshToken(string userAgent) - { - if (!_refreshTokens.Exists(x => x.UserAgent == userAgent)) return; - _refreshTokens.RemoveAll(x => x.UserAgent == userAgent); - } + public bool IsLocked() => LoginMetadata.IsLocked; } internal class LoggingTriesExceededException : GroceryStoreException { - public LoggingTriesExceededException(int maxLoginTry) : base("Try again after 5 min") + public LoggingTriesExceededException() : base("Try again after 5 min") { } - public override HttpStatusCode StatusCode => HttpStatusCode.Forbidden; + public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; } diff --git a/src/Users/Users.Core/Factories/IUserFactory.cs b/src/Users/Users.Core/Factories/IUserFactory.cs deleted file mode 100644 index 370b684..0000000 --- a/src/Users/Users.Core/Factories/IUserFactory.cs +++ /dev/null @@ -1,10 +0,0 @@ -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.ValueObjects; - -namespace IGroceryStore.Users.Factories; - -public interface IUserFactory -{ - User Create(UserId id, FirstName firstName, LastName lastName, Email email, string password); -} diff --git a/src/Users/Users.Core/Factories/UserFactory.cs b/src/Users/Users.Core/Factories/UserFactory.cs deleted file mode 100644 index d5827e7..0000000 --- a/src/Users/Users.Core/Factories/UserFactory.cs +++ /dev/null @@ -1,12 +0,0 @@ -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.Services; -using IGroceryStore.Users.ValueObjects; - -namespace IGroceryStore.Users.Factories; - -public class UserFactory : IUserFactory -{ - public User Create(UserId id, FirstName firstName, LastName lastName, Email email, string password) - => new(id, firstName, lastName, email, HashingService.HashPassword(password)); -} diff --git a/src/Users/Users.Core/Features/Users/GetUser.cs b/src/Users/Users.Core/Features/Users/GetUser.cs index e9d7e8f..824118c 100644 --- a/src/Users/Users.Core/Features/Users/GetUser.cs +++ b/src/Users/Users.Core/Features/Users/GetUser.cs @@ -31,7 +31,7 @@ public GetUserHttpHandler(UsersDbContext dbContext) _dbContext = dbContext; } - public async Task HandleAsync(GetUser query, CancellationToken cancellationToken = default) + public async Task HandleAsync(GetUser query, CancellationToken cancellationToken) { var user = await _dbContext.Users .FirstOrDefaultAsync(x => x.Id == new UserId(query.Id), cancellationToken); diff --git a/src/Users/Users.Core/Features/Users/GetUsers.cs b/src/Users/Users.Core/Features/Users/GetUsers.cs index f96231f..045b008 100644 --- a/src/Users/Users.Core/Features/Users/GetUsers.cs +++ b/src/Users/Users.Core/Features/Users/GetUsers.cs @@ -28,7 +28,7 @@ public GetUsersHandler(UsersDbContext dbContext) _dbContext = dbContext; } - public async Task HandleAsync(GetUsers query, CancellationToken cancellationToken = default) + public async Task HandleAsync(GetUsers query, CancellationToken cancellationToken) { var users = await _dbContext.Users .Select(x => new UserReadModel(x.Id, x.FirstName, x.LastName, x.Email)) diff --git a/src/Users/Users.Core/Features/Users/Register.cs b/src/Users/Users.Core/Features/Users/Register.cs index 7b9c810..3de6b27 100644 --- a/src/Users/Users.Core/Features/Users/Register.cs +++ b/src/Users/Users.Core/Features/Users/Register.cs @@ -3,12 +3,11 @@ using IGroceryStore.Shared.EndpointBuilders; using IGroceryStore.Shared.Filters; using IGroceryStore.Users.Contracts.Events; -using IGroceryStore.Users.Factories; -using IGroceryStore.Users.Persistence.Contexts; +using IGroceryStore.Users.Entities; +using IGroceryStore.Users.Persistence.Mongo; using MassTransit; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; using ValidationResult = FluentValidation.Results.ValidationResult; namespace IGroceryStore.Users.Features.Users; @@ -36,32 +35,36 @@ public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => internal class RegisterHandler : IHttpCommandHandler { - private readonly IUserFactory _factory; - private readonly UsersDbContext _context; + private readonly IUserRepository _repository; private readonly IBus _bus; - public RegisterHandler(IUserFactory factory, UsersDbContext context, IBus bus) + public RegisterHandler(IUserRepository repository, IBus bus) { - _factory = factory; - _context = context; + _repository = repository; _bus = bus; } - public async Task HandleAsync(Register command, CancellationToken cancellationToken = default) + public async Task HandleAsync(Register command, CancellationToken cancellationToken) { var (email, password, _, firstName, lastName) = command.Body; - var user = _factory.Create(Guid.NewGuid(), firstName, lastName, email, password); - await _context.Users.AddAsync(user, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); - await _bus.Publish(new UserCreated(user.Id, firstName, lastName), cancellationToken: cancellationToken); + var user = new User(password) + { + Id = Guid.NewGuid(), + FirstName = firstName, + LastName = lastName, + Email = email + }; + + await _repository.AddAsync(user, cancellationToken); + await _bus.Publish(new UserCreated(user.Id, firstName, lastName), cancellationToken); return Results.AcceptedAtRoute(nameof(GetUser),new {Id = user.Id.Value}); } } internal class CreateProductValidator : AbstractValidator { - public CreateProductValidator(UsersDbContext usersDbContext) + public CreateProductValidator(IUserRepository repository) { RuleFor(x => x.Body.Password) .NotEmpty() @@ -75,8 +78,7 @@ public CreateProductValidator(UsersDbContext usersDbContext) .EmailAddress() .CustomAsync(async (email, context, cancellationToken) => { - var user = await usersDbContext.Users.FirstOrDefaultAsync(x => x.Email == email, cancellationToken); - if (user is null) return; + if (!await repository.ExistByEmailAsync(email, cancellationToken)) return; context.AddFailure(new ValidationFailure(nameof(Register.Body.Email), "Email already exists")); }); diff --git a/src/Users/Users.Core/JWT/JwtSettings.cs b/src/Users/Users.Core/JWT/JwtSettings.cs deleted file mode 100644 index 47716bb..0000000 --- a/src/Users/Users.Core/JWT/JwtSettings.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Text; -using FluentValidation; -using IGroceryStore.Shared; -using IGroceryStore.Shared.Settings; -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.IdentityModel.Tokens; - -namespace IGroceryStore.Users.JWT; - -internal class JwtSettings : SettingsBase, ISettings -{ - public static string SectionName => "Users:JwtSettings"; - public string Key { get; set; } - public int ExpireSeconds { get; set; } - public string Issuer { get; set; } - public int ClockSkew { get; set; } - public long TicksPerSecond = 10_000 * 1_000; - - public static void Configure(JwtBearerOptions jwtBearerOptions, string audience, JwtSettings? jwtSettings) - { - jwtBearerOptions.RequireHttpsMetadata = false; - jwtBearerOptions.SaveToken = true; - jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Key)), - ValidIssuer = jwtSettings.Issuer, - ValidateAudience = true, - ValidAudience = audience, - ValidateLifetime = true, - ClockSkew = TimeSpan.FromSeconds(jwtSettings.ClockSkew) - }; - if (audience == Constants.Tokens.Audience.Access) - { - jwtBearerOptions.Events = new JwtBearerEvents - { - OnAuthenticationFailed = context => - { - if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) - { - context.Response.Headers.Add("Token-Expired", "true"); - } - return Task.CompletedTask; - } - }; - } - } - - public JwtSettings() - { - RuleFor(x => x.Key) - .NotEmpty() - .MinimumLength(16); - - RuleFor(x => x.Issuer) - .NotEmpty() - .Custom((issuer, context) => - { - if (!Uri.TryCreate(issuer, UriKind.Absolute, out var uri)) - { - context.AddFailure("Issuer must be a valid URI"); - } - }); - } -} diff --git a/src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs b/src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs index 89fc0d8..c2b5320 100644 --- a/src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs +++ b/src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs @@ -2,7 +2,6 @@ using IGroceryStore.Shared.Common; using IGroceryStore.Shared.Services; using IGroceryStore.Users.Entities; -using IGroceryStore.Users.Persistence.Seeders; using Microsoft.EntityFrameworkCore; namespace IGroceryStore.Users.Persistence.Contexts; @@ -10,15 +9,15 @@ namespace IGroceryStore.Users.Persistence.Contexts; internal class UsersDbContext : DbContext { private readonly ICurrentUserService _currentUserService; - private readonly DateTimeService _dateTimeService; + private readonly IDateTimeProvider _dateTimeProvider; public UsersDbContext(DbContextOptions options, ICurrentUserService currentUserService, - DateTimeService dateTimeService) + IDateTimeProvider dateTimeProvider) : base(options) { _currentUserService = currentUserService; - _dateTimeService = dateTimeService; + _dateTimeProvider = dateTimeProvider; } public DbSet Users => Set(); @@ -31,12 +30,12 @@ public UsersDbContext(DbContextOptions options, { case EntityState.Added: entry.Entity.CreatedBy = _currentUserService.UserId; - entry.Entity.Created = _dateTimeService.Now; + entry.Entity.Created = _dateTimeProvider.Now; break; case EntityState.Modified: entry.Entity.LastModifiedBy = _currentUserService.UserId; - entry.Entity.LastModified = _dateTimeService.Now; + entry.Entity.LastModified = _dateTimeProvider.Now; break; } } @@ -52,9 +51,4 @@ protected override void OnModelCreating(ModelBuilder builder) base.OnModelCreating(builder); } - - public async Task Seed() - { - await this.SeedSampleDataAsync(); - } } diff --git a/src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs b/src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs new file mode 100644 index 0000000..dfa4438 --- /dev/null +++ b/src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs @@ -0,0 +1,27 @@ +using IGroceryStore.Users.Entities; +using IGroceryStore.Users.ValueObjects; + +namespace IGroceryStore.Users.Persistence.Mongo.DbModels; + +internal record UserDbModel +{ + public required string Id { get; init; } + public required string Email { get; init; } + public required string FirstName { get; init; } + public required string LastName { get; init; } + public required string PasswordHash { get; init; } + + public required LoginMetadataDbModel LoginMetadata { get; init; } = new(); + public Preferences Preferences { get; init; } = new(); + public TokenStore TokenStore { get; init; } = new(); + public SecurityMetadata SecurityMetadata { get; init; } = new(); +} + +internal class LoginMetadataDbModel +{ + public ushort AccessFailedCount { get; init; } + public DateTime? LockoutEnd { get; init; } +} + + + diff --git a/src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs b/src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs new file mode 100644 index 0000000..9abdef1 --- /dev/null +++ b/src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs @@ -0,0 +1,72 @@ +using System.Diagnostics; +using IGroceryStore.Shared.ValueObjects; +using IGroceryStore.Users.Entities; +using IGroceryStore.Users.Persistence.Mongo.DbModels; +using IGroceryStore.Users.ValueObjects; +using MongoDB.Driver; + +namespace IGroceryStore.Users.Persistence.Mongo; + +internal interface IUserRepository +{ + Task GetAsync(UserId id, CancellationToken cancellationToken); + Task GetAsync(Email email, CancellationToken cancellationToken); + Task ExistByEmailAsync(string email, CancellationToken cancellationToken); + Task AddAsync(User user, CancellationToken cancellationToken); + Task UpdateAsync(User user, CancellationToken cancellationToken); +} + +internal class UsersRepository : IUserRepository +{ + private readonly IMongoCollection _usersCollection; + + public UsersRepository(IMongoCollection usersCollection) + { + _usersCollection = usersCollection; + } + + public async Task GetAsync(UserId id, CancellationToken cancellationToken) + { + var model = await _usersCollection + .Find(x => x.Id == id.Value.ToString()) + .FirstOrDefaultAsync(); + + throw new NotImplementedException(); + } + + public Task GetAsync(Email email, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public async Task ExistByEmailAsync(string email, CancellationToken cancellationToken) + { + var documentsCount = await _usersCollection + .Find(x => x.Email == email) + .CountDocumentsAsync(cancellationToken); + return documentsCount > 0; + } + + public async Task AddAsync(User user, CancellationToken cancellationToken) + { + if (await ExistByEmailAsync(user.Email, cancellationToken)) + throw new UnreachableException("This should be validated on handler level"); + + // var userDbModel = new UserDbModel + // { + // Id = user.Id.ToString(), + // FirstName = user.FirstName, + // LastName = user.LastName, + // Email = user.Email, + // + // }; + throw new NotImplementedException(); + + //await _usersCollection.InsertOneAsync(userDbModel); + } + + public Task UpdateAsync(User user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/src/Users/Users.Core/Persistence/Seeders/UsersDbSeed.cs b/src/Users/Users.Core/Persistence/Seeders/UsersDbSeed.cs deleted file mode 100644 index 0c04641..0000000 --- a/src/Users/Users.Core/Persistence/Seeders/UsersDbSeed.cs +++ /dev/null @@ -1,18 +0,0 @@ -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.Persistence.Contexts; - -namespace IGroceryStore.Users.Persistence.Seeders; - -internal static class UsersDbSeed -{ - public static async Task SeedSampleDataAsync(this UsersDbContext context) - { - if (context.Users.Any()) return; - - var id = new Guid(1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 0); - - var user = new User(id, "Adrian", "Franczak", "adrian.franczak@gmail.com", "Password123"); - context.Users.Add(user); - await context.SaveChangesAsync(); - } -} diff --git a/src/Users/Users.Core/ReadModels/TokensReadModel.cs b/src/Users/Users.Core/ReadModels/TokensReadModel.cs index 589ac3e..a035727 100644 --- a/src/Users/Users.Core/ReadModels/TokensReadModel.cs +++ b/src/Users/Users.Core/ReadModels/TokensReadModel.cs @@ -2,7 +2,7 @@ namespace IGroceryStore.Users.ReadModels { public class TokensReadModel { - public string AccessToken { get; set; } - public string RefreshToken { get; set; } + public required string AccessToken { get; init; } + public required string RefreshToken { get; init; } } } diff --git a/src/Users/Users.Core/Services/JwtTokenManager.cs b/src/Users/Users.Core/Services/JwtTokenManager.cs deleted file mode 100644 index 6bcfe44..0000000 --- a/src/Users/Users.Core/Services/JwtTokenManager.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using IGroceryStore.Shared; -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.JWT; -using JWT.Algorithms; -using JWT.Builder; -using Microsoft.Extensions.Options; - -namespace IGroceryStore.Users.Services; - -internal class JwtTokenManager : ITokenManager -{ - private readonly JwtSettings _settings; - public JwtTokenManager(IOptionsSnapshot settings) => _settings = settings.Value; - - public string GenerateAccessToken(User user) - { - return new JwtBuilder() - .WithAlgorithm(new HMACSHA256Algorithm()) - .WithSecret(Encoding.ASCII.GetBytes(_settings.Key)) - .AddClaim(Constants.Claims.Name.Expire, DateTimeOffset.UtcNow.AddSeconds(_settings.ExpireSeconds).ToUnixTimeSeconds()) - .AddClaim(Constants.Claims.Name.UserId, user.Id.Value) - .Issuer(_settings.Issuer) - .Audience(Constants.Tokens.Audience.Access) - .Encode(); - } - - public IDictionary VerifyToken(string token) - { - return new JwtBuilder() - .WithSecret(_settings.Key) - .MustVerifySignature() - .Decode>(token); - } - - public (string refreshToken, string jwt) GenerateRefreshToken(User user) - { - var randomNumber = new byte[32]; - using var rng = RandomNumberGenerator.Create(); - rng.GetBytes(randomNumber); - var randomString = Convert.ToBase64String(randomNumber); - - var refreshToken = Encoding.ASCII.GetString(randomNumber); - - var jwt = new JwtBuilder() - .WithAlgorithm(new HMACSHA256Algorithm()) - .WithSecret(_settings.Key) - .AddClaim(Constants.Claims.Name.Expire, DateTimeOffset.UtcNow.AddHours(4).ToUnixTimeSeconds()) - .AddClaim(Constants.Claims.Name.RefreshToken, refreshToken) - .AddClaim(Constants.Claims.Name.UserId, user.Id.Value) - .Issuer(_settings.Issuer) - .Audience(Constants.Tokens.Audience.Refresh) - .Encode(); - - return (refreshToken, jwt); - } -} diff --git a/src/Users/Users.Core/Users.Core.csproj b/src/Users/Users.Core/Users.Core.csproj index b2f5af2..8b6d782 100644 --- a/src/Users/Users.Core/Users.Core.csproj +++ b/src/Users/Users.Core/Users.Core.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Users/Users.Core/UsersModule.cs b/src/Users/Users.Core/UsersModule.cs index 9de04a7..665f779 100644 --- a/src/Users/Users.Core/UsersModule.cs +++ b/src/Users/Users.Core/UsersModule.cs @@ -3,11 +3,7 @@ using IGroceryStore.Shared.Common; using IGroceryStore.Shared.Configuration; using IGroceryStore.Shared.Settings; -using IGroceryStore.Users.Factories; -using IGroceryStore.Users.JWT; using IGroceryStore.Users.Persistence.Contexts; -using IGroceryStore.Users.Services; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -25,27 +21,11 @@ public class UsersModule : IModule public void Register(IServiceCollection services, IConfiguration configuration) { services.RegisterHandlers(); - - services.AddSingleton(); - services.AddScoped(); - - services.AddAuthorization(); var options = configuration.GetOptions(); services.AddDbContext(ctx => ctx.UseNpgsql(options.ConnectionString) .EnableSensitiveDataLogging(options.EnableSensitiveData)); - - var jwtSettings = configuration.GetOptions(); - var authenticationBuilder = services.AddAuthentication(x => - { - x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(jwtBearerOptions => - JwtSettings.Configure(jwtBearerOptions, Constants.Tokens.Audience.Access, jwtSettings)) - .AddJwtBearer(Constants.Tokens.Audience.Refresh, - jwtBearerOptions => JwtSettings.Configure(jwtBearerOptions, Constants.Tokens.Audience.Refresh, jwtSettings)); } public void Use(IApplicationBuilder app) @@ -54,7 +34,7 @@ public void Use(IApplicationBuilder app) public void Expose(IEndpointRouteBuilder endpoints) { - endpoints.MapGet($"/api/{Name.ToLower()}/health", () => $"{Name} module is healthy") + endpoints.MapGet($"/api/health/{Name.ToLower()}", () => $"{Name} module is healthy") .WithTags(Constants.SwaggerTags.HealthChecks); endpoints.RegisterEndpoints(); diff --git a/src/Users/Users.Core/ValueObjects/PasswordHash.cs b/src/Users/Users.Core/ValueObjects/PasswordHash.cs index 0008867..0929b3f 100644 --- a/src/Users/Users.Core/ValueObjects/PasswordHash.cs +++ b/src/Users/Users.Core/ValueObjects/PasswordHash.cs @@ -2,15 +2,10 @@ namespace IGroceryStore.Users.ValueObjects; -internal sealed record PasswordHash +internal sealed record PasswordHash(string Value) { - public string Value { get; } - - public PasswordHash(string value) - { - Value = value ?? throw new InvalidPasswordException(); - } - + public string Value { get; } = Value ?? throw new InvalidPasswordException(); + public static implicit operator string(PasswordHash passwordHash) => passwordHash.Value; public static implicit operator PasswordHash(string value) => new(value); } diff --git a/src/Users/Users.Core/ValueObjects/RefreshToken.cs b/src/Users/Users.Core/ValueObjects/RefreshToken.cs index 789d12b..e7fc680 100644 --- a/src/Users/Users.Core/ValueObjects/RefreshToken.cs +++ b/src/Users/Users.Core/ValueObjects/RefreshToken.cs @@ -1,3 +1,3 @@ namespace IGroceryStore.Users.ValueObjects; -public record RefreshToken(string UserAgent, string Value); +internal sealed record RefreshToken(string Value, DateTime ExpiresAt, Guid Jti); diff --git a/src/Users/Users.Core/modulesettings.Development.json b/src/Users/Users.Core/modulesettings.Development.json index 78bb8e4..197ec00 100644 --- a/src/Users/Users.Core/modulesettings.Development.json +++ b/src/Users/Users.Core/modulesettings.Development.json @@ -1,11 +1,5 @@ { "Users": { - "ModuleEnabled": true, - "JwtSettings": { - "Key": "my-32-character-ultra-secure-and-ultra-long-secret", - "ExpireSeconds": 300, - "Issuer": "https://localhost:5000", - "ClockSkew": 30 - } + "ModuleEnabled": false } } \ No newline at end of file diff --git a/src/Users/Users.Core/modulesettings.Test.json b/src/Users/Users.Core/modulesettings.Test.json index 78bb8e4..77db69a 100644 --- a/src/Users/Users.Core/modulesettings.Test.json +++ b/src/Users/Users.Core/modulesettings.Test.json @@ -1,11 +1,5 @@ { "Users": { - "ModuleEnabled": true, - "JwtSettings": { - "Key": "my-32-character-ultra-secure-and-ultra-long-secret", - "ExpireSeconds": 300, - "Issuer": "https://localhost:5000", - "ClockSkew": 30 - } + "ModuleEnabled": true } } \ No newline at end of file diff --git a/src/Users/Users.Core/modulesettings.json b/src/Users/Users.Core/modulesettings.json index f38aeef..197ec00 100644 --- a/src/Users/Users.Core/modulesettings.json +++ b/src/Users/Users.Core/modulesettings.json @@ -1,11 +1,5 @@ { "Users": { - "ModuleEnabled": false, - "JwtSettings": { - "Key": "foo", - "ExpireSeconds": 0, - "Issuer": "foo", - "ClockSkew": 0 - } + "ModuleEnabled": false } } \ No newline at end of file diff --git a/src/Worker/Program.cs b/src/Worker/Program.cs index f8b1baf..e9f2baf 100644 --- a/src/Worker/Program.cs +++ b/src/Worker/Program.cs @@ -28,7 +28,7 @@ } //Services -builder.Services.AddSingleton(); +builder.Services.AddSingleton(); //Messaging var rabbitSettings = builder.Configuration.GetOptions(); diff --git a/src/Worker/Worker.csproj b/src/Worker/Worker.csproj index 8f71eef..57bf2ec 100644 --- a/src/Worker/Worker.csproj +++ b/src/Worker/Worker.csproj @@ -34,7 +34,7 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/tests/Shared/Shared.csproj b/tests/Shared/Shared.csproj index 2b47929..b988457 100644 --- a/tests/Shared/Shared.csproj +++ b/tests/Shared/Shared.csproj @@ -17,10 +17,10 @@ - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Shops/Shops.IntegrationTests/Shops.IntegrationTests.csproj b/tests/Shops/Shops.IntegrationTests/Shops.IntegrationTests.csproj index 7da0d70..05f56d8 100644 --- a/tests/Shops/Shops.IntegrationTests/Shops.IntegrationTests.csproj +++ b/tests/Shops/Shops.IntegrationTests/Shops.IntegrationTests.csproj @@ -6,16 +6,13 @@ enable false preview + IGroceryStore.Shops.IntegrationTests + IGroceryStore.Shops.IntegrationTests - - - - - - + diff --git a/tests/Shops/Shops.UnitTests/Shops.UnitTests.csproj b/tests/Shops/Shops.UnitTests/Shops.UnitTests.csproj index 697fab7..ca0ed59 100644 --- a/tests/Shops/Shops.UnitTests/Shops.UnitTests.csproj +++ b/tests/Shops/Shops.UnitTests/Shops.UnitTests.csproj @@ -6,16 +6,13 @@ enable false preview + IGroceryStore.Shops.UnitTests + IGroceryStore.Shops.UnitTests - - - - - - + diff --git a/tests/Users/Users.IntegrationTests/UserApiFactory.cs b/tests/Users/Users.IntegrationTests/UserApiFactory.cs index 355c62f..1bad542 100644 --- a/tests/Users/Users.IntegrationTests/UserApiFactory.cs +++ b/tests/Users/Users.IntegrationTests/UserApiFactory.cs @@ -5,6 +5,7 @@ using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; using IGroceryStore.API; +using IGroceryStore.API.Initializers; using IGroceryStore.Products.Persistence.Contexts; using IGroceryStore.Shared; using IGroceryStore.Shared.Services; @@ -17,6 +18,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; using Npgsql; using Respawn; @@ -42,7 +44,7 @@ public class UserApiFactory : WebApplicationFactory, IAsyncLifetime public UserApiFactory() { _user = new MockUser(new Claim(Constants.Claims.Name.UserId, "1"), - new Claim(Constants.Claims.Name.Expire, DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString())); + new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString())); Randomizer.Seed = new Random(420); VerifierSettings.ScrubInlineGuids(); } diff --git a/tests/Users/Users.IntegrationTests/Users/GetUserTests.cs b/tests/Users/Users.IntegrationTests/Users/GetUserTests.cs index fb19e3c..dbaebf1 100644 --- a/tests/Users/Users.IntegrationTests/Users/GetUserTests.cs +++ b/tests/Users/Users.IntegrationTests/Users/GetUserTests.cs @@ -6,6 +6,7 @@ using IGroceryStore.Shared.Tests.Auth; using IGroceryStore.Users.ReadModels; using Microsoft.AspNetCore.TestHost; +using Microsoft.IdentityModel.JsonWebTokens; namespace IGroceryStore.Users.IntegrationTests.Users; @@ -27,7 +28,7 @@ public GetUserTests(UserApiFactory apiFactory) services.RegisterUser(new[] { new Claim(Constants.Claims.Name.UserId, "1"), - new Claim(Constants.Claims.Name.Expire, + new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString()) }); })); // override authorized user; diff --git a/tools/terraform/dev/main.tf b/tools/terraform/dev/main.tf index 85b6473..8f4eb79 100644 --- a/tools/terraform/dev/main.tf +++ b/tools/terraform/dev/main.tf @@ -4,6 +4,10 @@ terraform { source = "hashicorp/aws" version = "~> 4.39" } + postgresql = { + source = "cyrilgdn/postgresql" + version = "1.17.1" + } } } @@ -11,6 +15,7 @@ locals { environment = "dev" table_name = "shops" region = "eu-central-1" + pg_db_name = "IGroceryStoreDb" } provider "aws" { @@ -43,10 +48,30 @@ resource "aws_dynamodb_table" "shops-table" { name = "sk" type = "S" } - - tags = { - Name = "${local.table_name}-${local.environment}" - Environment = local.environment - } +} + +provider "postgresql" { + host = "localhost" + port = 5432 + database = "postgres" + username = "postgres" + password = "admin" + connect_timeout = 15 + sslmode = "disable" + expected_version = "14.4" +} + +resource "postgresql_database" "postgres_db" { + name = local.pg_db_name + owner = "postgres" + connection_limit = -1 + allow_connections = true +} + +resource "postgresql_schema" "postgres_products_schema" { + database = local.pg_db_name + name = "IGroceryStoreDb.Products" + owner = "postgres" + depends_on = [postgresql_database.postgres_db] } From 566d3b842f83510e7f92c9756db7ee6b32c6c8e0 Mon Sep 17 00:00:00 2001 From: Adrian Franczak <44712992+Nairda015@users.noreply.github.com> Date: Sun, 4 Dec 2022 16:35:25 +0100 Subject: [PATCH 2/4] remove UsersModule --- IGroceryStore.sln | 48 ++------ src/API/API.csproj | 2 +- src/API/Program.cs | 3 - src/API/appsettings.Development.json | 4 - src/API/appsettings.Test.json | 4 - src/API/appsettings.json | 4 - src/Baskets/Baskets.Core/Baskets.Core.csproj | 2 +- .../Baskets.Core/Subscribers/Users/AddUser.cs | 2 +- .../Notifications/Notifications.csproj | 2 +- .../Persistence}/PostgresInitializer.cs | 3 +- .../Products.Core/Products.Core.csproj | 2 +- src/Products/Products.Core/ProductsModule.cs | 5 +- .../modulesettings.Development.json | 4 + .../Products.Core/modulesettings.Test.json | 4 + .../Products.Core/modulesettings.json | 4 + .../{Shared => }/Common/AuditableEntity.cs | 0 src/Shared/{Shared => }/Common/IModule.cs | 0 .../{Shared => }/Configuration/AppContext.cs | 0 .../Configuration/AppInitializer.cs | 0 .../Configuration/EndpointsExtensions.cs | 0 .../{Shared => }/Configuration/Extensions.cs | 0 .../Configuration/GroceryStoreRouteBuilder.cs | 0 src/Shared/{Shared => }/Constants.cs | 0 src/Shared/Contracts/Problems.cs | 6 + .../Contracts}/UserCreated.cs | 4 +- .../EndpointBuilders/Extensions.cs | 0 .../{Shared => }/EndpointBuilders/Handlers.cs | 0 .../EndpointBuilders/IEndpoint.cs | 0 .../IGroceryStoreRouteBuilder.cs | 0 .../Exceptions/GroceryStoreException.cs | 0 .../Exceptions/InvalidBasketIdException.cs | 0 .../Exceptions/InvalidProductIdException.cs | 0 .../Exceptions/InvalidProductNameException.cs | 0 .../Exceptions/InvalidUserIdException.cs | 0 .../{Shared => }/Filters/ValidationFilter.cs | 0 .../Services/CurrentUserService.cs | 0 .../{Shared => }/Services/DateTimeProvider.cs | 3 + .../{Shared => }/Services/SnowflakeService.cs | 0 .../{Shared => }/Settings/Extensions.cs | 0 src/Shared/{Shared => }/Settings/ISettings.cs | 0 .../{Shared => }/Settings/PostgresSettings.cs | 0 .../{Shared => }/Settings/RabbitSettings.cs | 0 src/Shared/{Shared => }/Shared.csproj | 0 .../{Shared => }/ValueObjects/BasketId.cs | 0 .../{Shared => }/ValueObjects/ProductId.cs | 3 +- .../{Shared => }/ValueObjects/ProductName.cs | 0 .../{Shared => }/ValueObjects/UserId.cs | 4 +- src/Shops/Shops.Core/Shops.Core.csproj | 3 +- .../Shops.Core/Subscribers/Users/AddUser.cs | 2 +- .../Users.Contracts/Users.Contracts.csproj | 16 --- .../Users.Core/Entities/LoginMetadata.cs | 26 ----- src/Users/Users.Core/Entities/Preferences.cs | 6 - .../Users.Core/Entities/SecurityMetadata.cs | 7 -- src/Users/Users.Core/Entities/TokenStore.cs | 26 ----- src/Users/Users.Core/Entities/User.cs | 56 --------- .../Exceptions/IncorrectPasswordException.cs | 13 --- .../Exceptions/InvalidClaimsException.cs | 13 --- .../Exceptions/InvalidCredentialsException.cs | 13 --- .../Exceptions/InvalidEmailException.cs | 13 --- .../Exceptions/InvalidFirstNameException.cs | 13 --- .../Exceptions/InvalidLastNameException.cs | 13 --- .../Exceptions/InvalidPasswordException.cs | 12 -- .../PasswordDoesNotMatchException.cs | 13 --- .../Exceptions/UserNotFoundException.cs | 12 -- src/Users/Users.Core/Features/Tokens/Login.cs | 70 ----------- .../Users.Core/Features/Tokens/Refresh.cs | 74 ------------ .../Users.Core/Features/Users/GetUser.cs | 44 ------- .../Users.Core/Features/Users/GetUsers.cs | 41 ------- .../Users.Core/Features/Users/Register.cs | 91 --------------- .../Configurations/UserDbConfiguration.cs | 49 -------- .../Persistence/Contexts/UsersDbContext.cs | 54 --------- .../Persistence/Mongo/DbModels/DbModels.cs | 27 ----- .../Persistence/Mongo/UsersRepository.cs | 72 ------------ .../Users.Core/ReadModels/TokensReadModel.cs | 8 -- .../Users.Core/ReadModels/UserReadModel.cs | 2 - .../Users.Core/Services/HashingService.cs | 19 --- .../Users.Core/Services/ITokenManager.cs | 10 -- src/Users/Users.Core/Users.Core.csproj | 32 ----- src/Users/Users.Core/UsersModule.cs | 42 ------- src/Users/Users.Core/ValueObjects/Email.cs | 20 ---- .../Users.Core/ValueObjects/FirstName.cs | 20 ---- src/Users/Users.Core/ValueObjects/LastName.cs | 21 ---- .../Users.Core/ValueObjects/PasswordHash.cs | 11 -- .../Users.Core/ValueObjects/RefreshToken.cs | 3 - .../modulesettings.Development.json | 5 - src/Users/Users.Core/modulesettings.Test.json | 5 - src/Users/Users.Core/modulesettings.json | 5 - src/Worker/Worker.csproj | 1 + tests/Shops/Shops.IntegrationTests/Test.cs | 71 ++++++++++++ .../DbConfigExtensions.cs | 24 ---- .../Users/Users.IntegrationTests/TestUsers.cs | 34 ------ .../Users.IntegrationTests/UserApiFactory.cs | 109 ------------------ .../Users.IntegrationTests/UserCollection.cs | 6 - .../Users.IntegrationTests/UserTestHelper.cs | 29 ----- .../Users.IntegrationTests.csproj | 28 ----- ...er_ReturnsUser_WhenUserExists.verified.txt | 33 ------ .../Users/GetUserTests.cs | 64 ---------- ...r_CreatesUser_WhenDataIsValid.verified.txt | 48 -------- .../Users/RegisterTests.cs | 40 ------- .../Users.UnitTests/Users.UnitTests.csproj | 18 --- 100 files changed, 118 insertions(+), 1482 deletions(-) rename src/{API/Initializers => Products/Products.Core/Persistence}/PostgresInitializer.cs (92%) rename src/Shared/{Shared => }/Common/AuditableEntity.cs (100%) rename src/Shared/{Shared => }/Common/IModule.cs (100%) rename src/Shared/{Shared => }/Configuration/AppContext.cs (100%) rename src/Shared/{Shared => }/Configuration/AppInitializer.cs (100%) rename src/Shared/{Shared => }/Configuration/EndpointsExtensions.cs (100%) rename src/Shared/{Shared => }/Configuration/Extensions.cs (100%) rename src/Shared/{Shared => }/Configuration/GroceryStoreRouteBuilder.cs (100%) rename src/Shared/{Shared => }/Constants.cs (100%) create mode 100644 src/Shared/Contracts/Problems.cs rename src/{Users/Users.Contracts/Events => Shared/Contracts}/UserCreated.cs (51%) rename src/Shared/{Shared => }/EndpointBuilders/Extensions.cs (100%) rename src/Shared/{Shared => }/EndpointBuilders/Handlers.cs (100%) rename src/Shared/{Shared => }/EndpointBuilders/IEndpoint.cs (100%) rename src/Shared/{Shared => }/EndpointBuilders/IGroceryStoreRouteBuilder.cs (100%) rename src/Shared/{Shared => }/Exceptions/GroceryStoreException.cs (100%) rename src/Shared/{Shared => }/Exceptions/InvalidBasketIdException.cs (100%) rename src/Shared/{Shared => }/Exceptions/InvalidProductIdException.cs (100%) rename src/Shared/{Shared => }/Exceptions/InvalidProductNameException.cs (100%) rename src/Shared/{Shared => }/Exceptions/InvalidUserIdException.cs (100%) rename src/Shared/{Shared => }/Filters/ValidationFilter.cs (100%) rename src/Shared/{Shared => }/Services/CurrentUserService.cs (100%) rename src/Shared/{Shared => }/Services/DateTimeProvider.cs (84%) rename src/Shared/{Shared => }/Services/SnowflakeService.cs (100%) rename src/Shared/{Shared => }/Settings/Extensions.cs (100%) rename src/Shared/{Shared => }/Settings/ISettings.cs (100%) rename src/Shared/{Shared => }/Settings/PostgresSettings.cs (100%) rename src/Shared/{Shared => }/Settings/RabbitSettings.cs (100%) rename src/Shared/{Shared => }/Shared.csproj (100%) rename src/Shared/{Shared => }/ValueObjects/BasketId.cs (100%) rename src/Shared/{Shared => }/ValueObjects/ProductId.cs (98%) rename src/Shared/{Shared => }/ValueObjects/ProductName.cs (100%) rename src/Shared/{Shared => }/ValueObjects/UserId.cs (97%) delete mode 100644 src/Users/Users.Contracts/Users.Contracts.csproj delete mode 100644 src/Users/Users.Core/Entities/LoginMetadata.cs delete mode 100644 src/Users/Users.Core/Entities/Preferences.cs delete mode 100644 src/Users/Users.Core/Entities/SecurityMetadata.cs delete mode 100644 src/Users/Users.Core/Entities/TokenStore.cs delete mode 100644 src/Users/Users.Core/Entities/User.cs delete mode 100644 src/Users/Users.Core/Exceptions/IncorrectPasswordException.cs delete mode 100644 src/Users/Users.Core/Exceptions/InvalidClaimsException.cs delete mode 100644 src/Users/Users.Core/Exceptions/InvalidCredentialsException.cs delete mode 100644 src/Users/Users.Core/Exceptions/InvalidEmailException.cs delete mode 100644 src/Users/Users.Core/Exceptions/InvalidFirstNameException.cs delete mode 100644 src/Users/Users.Core/Exceptions/InvalidLastNameException.cs delete mode 100644 src/Users/Users.Core/Exceptions/InvalidPasswordException.cs delete mode 100644 src/Users/Users.Core/Exceptions/PasswordDoesNotMatchException.cs delete mode 100644 src/Users/Users.Core/Exceptions/UserNotFoundException.cs delete mode 100644 src/Users/Users.Core/Features/Tokens/Login.cs delete mode 100644 src/Users/Users.Core/Features/Tokens/Refresh.cs delete mode 100644 src/Users/Users.Core/Features/Users/GetUser.cs delete mode 100644 src/Users/Users.Core/Features/Users/GetUsers.cs delete mode 100644 src/Users/Users.Core/Features/Users/Register.cs delete mode 100644 src/Users/Users.Core/Persistence/Configurations/UserDbConfiguration.cs delete mode 100644 src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs delete mode 100644 src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs delete mode 100644 src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs delete mode 100644 src/Users/Users.Core/ReadModels/TokensReadModel.cs delete mode 100644 src/Users/Users.Core/ReadModels/UserReadModel.cs delete mode 100644 src/Users/Users.Core/Services/HashingService.cs delete mode 100644 src/Users/Users.Core/Services/ITokenManager.cs delete mode 100644 src/Users/Users.Core/Users.Core.csproj delete mode 100644 src/Users/Users.Core/UsersModule.cs delete mode 100644 src/Users/Users.Core/ValueObjects/Email.cs delete mode 100644 src/Users/Users.Core/ValueObjects/FirstName.cs delete mode 100644 src/Users/Users.Core/ValueObjects/LastName.cs delete mode 100644 src/Users/Users.Core/ValueObjects/PasswordHash.cs delete mode 100644 src/Users/Users.Core/ValueObjects/RefreshToken.cs delete mode 100644 src/Users/Users.Core/modulesettings.Development.json delete mode 100644 src/Users/Users.Core/modulesettings.Test.json delete mode 100644 src/Users/Users.Core/modulesettings.json create mode 100644 tests/Shops/Shops.IntegrationTests/Test.cs delete mode 100644 tests/Users/Users.IntegrationTests/DbConfigExtensions.cs delete mode 100644 tests/Users/Users.IntegrationTests/TestUsers.cs delete mode 100644 tests/Users/Users.IntegrationTests/UserApiFactory.cs delete mode 100644 tests/Users/Users.IntegrationTests/UserCollection.cs delete mode 100644 tests/Users/Users.IntegrationTests/UserTestHelper.cs delete mode 100644 tests/Users/Users.IntegrationTests/Users.IntegrationTests.csproj delete mode 100644 tests/Users/Users.IntegrationTests/Users/GetUserTests.GetUser_ReturnsUser_WhenUserExists.verified.txt delete mode 100644 tests/Users/Users.IntegrationTests/Users/GetUserTests.cs delete mode 100644 tests/Users/Users.IntegrationTests/Users/RegisterTests.Register_CreatesUser_WhenDataIsValid.verified.txt delete mode 100644 tests/Users/Users.IntegrationTests/Users/RegisterTests.cs delete mode 100644 tests/Users/Users.UnitTests/Users.UnitTests.csproj diff --git a/IGroceryStore.sln b/IGroceryStore.sln index 73ea95d..e32c534 100644 --- a/IGroceryStore.sln +++ b/IGroceryStore.sln @@ -23,8 +23,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Products.Core", "src\Produc EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{DC323AE4-1C76-4AEB-BD0E-139B47A5F4ED}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "src\Shared\Shared\Shared.csproj", "{F086A490-C28A-4DF9-A05E-8D765CF0FD62}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "API", "src\API\API.csproj", "{F8ED371D-A8D2-43C0-9526-2B5B1026E120}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker", "src\Worker\Worker.csproj", "{13FCD77B-95D7-4FC2-AD7F-FC557C89E08D}" @@ -33,18 +31,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E795DE89 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Baskets.UnitTests", "tests\Baskets\Baskets.UnitTests\Baskets.UnitTests.csproj", "{4BE58296-F624-4C03-A9D2-1E5D3061000E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Users", "Users", "{DAB96D2A-A8E0-419A-A030-A39596BC9F96}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Users.Core", "src\Users\Users.Core\Users.Core.csproj", "{705137AC-A585-4DA0-80E1-A6AEDA0A8B78}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Users.Contracts", "src\Users\Users.Contracts\Users.Contracts.csproj", "{9EFBA892-DA33-4E8B-BE43-06C2CD184732}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4257692D-0F78-4D5D-9B1D-5F7D28DB58CB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Baskets", "Baskets", "{A55FBF25-CB11-4671-8CF4-76816B19A882}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Users", "Users", "{F3EB15AF-4432-40B1-8B20-D3B257D1411E}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Products", "Products", "{E59C08BA-D7AE-4726-ACED-270B35104447}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shops", "Shops", "{159E4352-BDA2-41BA-81FA-D722BDC200E5}" @@ -55,14 +45,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Products.IntegrationTests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shops.IntegrationTests", "tests\Shops\Shops.IntegrationTests\Shops.IntegrationTests.csproj", "{32B71E08-83CC-485E-9C2C-B49D078EE8B1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Users.IntegrationTests", "tests\Users\Users.IntegrationTests\Users.IntegrationTests.csproj", "{492E6B88-37F2-4752-A146-C37B8B3B2C8F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Products.UnitTests", "tests\Products\Products.UnitTests\Products.UnitTests.csproj", "{9B368812-B9E0-4B34-B532-AC90B95D1CBF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shops.UnitTests", "tests\Shops\Shops.UnitTests\Shops.UnitTests.csproj", "{5F1ACEEE-21C2-4F25-AC1E-B7FE4C5BF2D9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Users.UnitTests", "tests\Users\Users.UnitTests\Users.UnitTests.csproj", "{90BAF73D-97DE-4308-894D-4CFBC8546043}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "requests", "requests", "{02241B0B-8C92-40C1-A3AC-F7AC27EDD788}" ProjectSection(SolutionItems) = preProject requests\brands.http = requests\brands.http @@ -144,6 +130,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{F0E45E tools\terraform\global\terraform.tfvars = tools\terraform\global\terraform.tfvars EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "src\Shared\Shared.csproj", "{16A1B41B-B31E-4DC6-90F3-FFF3AFF3B61D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -174,10 +162,6 @@ Global {D8778F33-AEFD-4F9F-AE08-14BF1B13B503}.Debug|Any CPU.Build.0 = Debug|Any CPU {D8778F33-AEFD-4F9F-AE08-14BF1B13B503}.Release|Any CPU.ActiveCfg = Release|Any CPU {D8778F33-AEFD-4F9F-AE08-14BF1B13B503}.Release|Any CPU.Build.0 = Release|Any CPU - {F086A490-C28A-4DF9-A05E-8D765CF0FD62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F086A490-C28A-4DF9-A05E-8D765CF0FD62}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F086A490-C28A-4DF9-A05E-8D765CF0FD62}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F086A490-C28A-4DF9-A05E-8D765CF0FD62}.Release|Any CPU.Build.0 = Release|Any CPU {F8ED371D-A8D2-43C0-9526-2B5B1026E120}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F8ED371D-A8D2-43C0-9526-2B5B1026E120}.Debug|Any CPU.Build.0 = Debug|Any CPU {F8ED371D-A8D2-43C0-9526-2B5B1026E120}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -190,14 +174,6 @@ Global {4BE58296-F624-4C03-A9D2-1E5D3061000E}.Debug|Any CPU.Build.0 = Debug|Any CPU {4BE58296-F624-4C03-A9D2-1E5D3061000E}.Release|Any CPU.ActiveCfg = Release|Any CPU {4BE58296-F624-4C03-A9D2-1E5D3061000E}.Release|Any CPU.Build.0 = Release|Any CPU - {705137AC-A585-4DA0-80E1-A6AEDA0A8B78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {705137AC-A585-4DA0-80E1-A6AEDA0A8B78}.Debug|Any CPU.Build.0 = Debug|Any CPU - {705137AC-A585-4DA0-80E1-A6AEDA0A8B78}.Release|Any CPU.ActiveCfg = Release|Any CPU - {705137AC-A585-4DA0-80E1-A6AEDA0A8B78}.Release|Any CPU.Build.0 = Release|Any CPU - {9EFBA892-DA33-4E8B-BE43-06C2CD184732}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9EFBA892-DA33-4E8B-BE43-06C2CD184732}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9EFBA892-DA33-4E8B-BE43-06C2CD184732}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9EFBA892-DA33-4E8B-BE43-06C2CD184732}.Release|Any CPU.Build.0 = Release|Any CPU {0A86368E-D354-4998-8590-943EADEF867A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0A86368E-D354-4998-8590-943EADEF867A}.Debug|Any CPU.Build.0 = Debug|Any CPU {0A86368E-D354-4998-8590-943EADEF867A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -210,10 +186,6 @@ Global {32B71E08-83CC-485E-9C2C-B49D078EE8B1}.Debug|Any CPU.Build.0 = Debug|Any CPU {32B71E08-83CC-485E-9C2C-B49D078EE8B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {32B71E08-83CC-485E-9C2C-B49D078EE8B1}.Release|Any CPU.Build.0 = Release|Any CPU - {492E6B88-37F2-4752-A146-C37B8B3B2C8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {492E6B88-37F2-4752-A146-C37B8B3B2C8F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {492E6B88-37F2-4752-A146-C37B8B3B2C8F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {492E6B88-37F2-4752-A146-C37B8B3B2C8F}.Release|Any CPU.Build.0 = Release|Any CPU {9B368812-B9E0-4B34-B532-AC90B95D1CBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9B368812-B9E0-4B34-B532-AC90B95D1CBF}.Debug|Any CPU.Build.0 = Debug|Any CPU {9B368812-B9E0-4B34-B532-AC90B95D1CBF}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -222,10 +194,6 @@ Global {5F1ACEEE-21C2-4F25-AC1E-B7FE4C5BF2D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {5F1ACEEE-21C2-4F25-AC1E-B7FE4C5BF2D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {5F1ACEEE-21C2-4F25-AC1E-B7FE4C5BF2D9}.Release|Any CPU.Build.0 = Release|Any CPU - {90BAF73D-97DE-4308-894D-4CFBC8546043}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {90BAF73D-97DE-4308-894D-4CFBC8546043}.Debug|Any CPU.Build.0 = Debug|Any CPU - {90BAF73D-97DE-4308-894D-4CFBC8546043}.Release|Any CPU.ActiveCfg = Release|Any CPU - {90BAF73D-97DE-4308-894D-4CFBC8546043}.Release|Any CPU.Build.0 = Release|Any CPU {7F1E1F0D-2006-42D0-8AC0-72E40FD709B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7F1E1F0D-2006-42D0-8AC0-72E40FD709B6}.Debug|Any CPU.Build.0 = Debug|Any CPU {7F1E1F0D-2006-42D0-8AC0-72E40FD709B6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -234,6 +202,10 @@ Global {3778177C-5D1B-413E-8313-BAEFBE5B45AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {3778177C-5D1B-413E-8313-BAEFBE5B45AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {3778177C-5D1B-413E-8313-BAEFBE5B45AB}.Release|Any CPU.Build.0 = Release|Any CPU + {16A1B41B-B31E-4DC6-90F3-FFF3AFF3B61D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {16A1B41B-B31E-4DC6-90F3-FFF3AFF3B61D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16A1B41B-B31E-4DC6-90F3-FFF3AFF3B61D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {16A1B41B-B31E-4DC6-90F3-FFF3AFF3B61D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -249,24 +221,17 @@ Global {29B589AC-707B-4C53-9E88-F22888311229} = {51E807A8-E7D2-4349-B5C2-EB780D82641A} {D8778F33-AEFD-4F9F-AE08-14BF1B13B503} = {51E807A8-E7D2-4349-B5C2-EB780D82641A} {DC323AE4-1C76-4AEB-BD0E-139B47A5F4ED} = {4257692D-0F78-4D5D-9B1D-5F7D28DB58CB} - {F086A490-C28A-4DF9-A05E-8D765CF0FD62} = {DC323AE4-1C76-4AEB-BD0E-139B47A5F4ED} {F8ED371D-A8D2-43C0-9526-2B5B1026E120} = {4257692D-0F78-4D5D-9B1D-5F7D28DB58CB} {13FCD77B-95D7-4FC2-AD7F-FC557C89E08D} = {4257692D-0F78-4D5D-9B1D-5F7D28DB58CB} {4BE58296-F624-4C03-A9D2-1E5D3061000E} = {A55FBF25-CB11-4671-8CF4-76816B19A882} - {DAB96D2A-A8E0-419A-A030-A39596BC9F96} = {4257692D-0F78-4D5D-9B1D-5F7D28DB58CB} - {705137AC-A585-4DA0-80E1-A6AEDA0A8B78} = {DAB96D2A-A8E0-419A-A030-A39596BC9F96} - {9EFBA892-DA33-4E8B-BE43-06C2CD184732} = {DAB96D2A-A8E0-419A-A030-A39596BC9F96} {A55FBF25-CB11-4671-8CF4-76816B19A882} = {E795DE89-913A-4B75-B579-45FFB612EE53} - {F3EB15AF-4432-40B1-8B20-D3B257D1411E} = {E795DE89-913A-4B75-B579-45FFB612EE53} {E59C08BA-D7AE-4726-ACED-270B35104447} = {E795DE89-913A-4B75-B579-45FFB612EE53} {159E4352-BDA2-41BA-81FA-D722BDC200E5} = {E795DE89-913A-4B75-B579-45FFB612EE53} {0A86368E-D354-4998-8590-943EADEF867A} = {A55FBF25-CB11-4671-8CF4-76816B19A882} {B65B2EA2-E5DD-4A6A-894A-79B16961B50E} = {E59C08BA-D7AE-4726-ACED-270B35104447} {32B71E08-83CC-485E-9C2C-B49D078EE8B1} = {159E4352-BDA2-41BA-81FA-D722BDC200E5} - {492E6B88-37F2-4752-A146-C37B8B3B2C8F} = {F3EB15AF-4432-40B1-8B20-D3B257D1411E} {9B368812-B9E0-4B34-B532-AC90B95D1CBF} = {E59C08BA-D7AE-4726-ACED-270B35104447} {5F1ACEEE-21C2-4F25-AC1E-B7FE4C5BF2D9} = {159E4352-BDA2-41BA-81FA-D722BDC200E5} - {90BAF73D-97DE-4308-894D-4CFBC8546043} = {F3EB15AF-4432-40B1-8B20-D3B257D1411E} {C3809CC7-0AF9-4D56-B1AB-EA0E098B0ECF} = {4257692D-0F78-4D5D-9B1D-5F7D28DB58CB} {7F1E1F0D-2006-42D0-8AC0-72E40FD709B6} = {C3809CC7-0AF9-4D56-B1AB-EA0E098B0ECF} {E62041C7-7CED-415E-A320-4EB555D3223D} = {E795DE89-913A-4B75-B579-45FFB612EE53} @@ -280,6 +245,7 @@ Global {15F3218D-6C08-49D5-A7A7-D61FA1280565} = {02396B41-F62C-4CC3-8B8F-B2400BBDD7FC} {6FA4FE98-D7C9-42AD-BBBF-0B4B2383733F} = {0DC74329-B279-4FE2-940C-D7224162ED46} {F0E45EE2-0DD1-4810-B2E7-45CEE54F22AB} = {02396B41-F62C-4CC3-8B8F-B2400BBDD7FC} + {16A1B41B-B31E-4DC6-90F3-FFF3AFF3B61D} = {DC323AE4-1C76-4AEB-BD0E-139B47A5F4ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F83A5637-BD21-4AAC-9087-87AB61B4E0A8} diff --git a/src/API/API.csproj b/src/API/API.csproj index 23c6e4e..b3c0e09 100644 --- a/src/API/API.csproj +++ b/src/API/API.csproj @@ -42,8 +42,8 @@ + - diff --git a/src/API/Program.cs b/src/API/Program.cs index d90de76..3a8010f 100644 --- a/src/API/Program.cs +++ b/src/API/Program.cs @@ -1,6 +1,5 @@ using FluentValidation; using IGroceryStore.API.Configuration; -using IGroceryStore.API.Initializers; using IGroceryStore.API.Middlewares; using IGroceryStore.Shared; using IGroceryStore.Shared.Services; @@ -28,7 +27,6 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); -builder.Services.AddSingleton(); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); @@ -39,7 +37,6 @@ var app = builder.Build(); //**********************************// -System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); app.UseSwagger(); if (app.Environment.IsDevelopment()) diff --git a/src/API/appsettings.Development.json b/src/API/appsettings.Development.json index fb263b6..3377482 100644 --- a/src/API/appsettings.Development.json +++ b/src/API/appsettings.Development.json @@ -15,10 +15,6 @@ "ElasticConfiguration": { "Uri": "http://localhost:9200" }, - "Postgres": { - "ConnectionString": "User ID = postgres; Password = admin; Server = localhost; Port = 5432; Database = IGroceryStoreDb; Integrated Security = true; Pooling = true;", - "EnableSensitiveData": true - }, "AWS": { "Region": "eu-central-1" }, diff --git a/src/API/appsettings.Test.json b/src/API/appsettings.Test.json index c873bea..9b93cb6 100644 --- a/src/API/appsettings.Test.json +++ b/src/API/appsettings.Test.json @@ -15,10 +15,6 @@ "ElasticConfiguration": { "Uri": "http://localhost:9200" }, - "Postgres": { - "ConnectionString": "User ID = postgres; Password = admin; Server = localhost; Port = 5432; Database = IGroceryStoreDb; Integrated Security = true; Pooling = true;", - "EnableSensitiveData": false - }, "AWS": { "Region": "eu-central-1" }, diff --git a/src/API/appsettings.json b/src/API/appsettings.json index f0c1a51..1a0cbc4 100644 --- a/src/API/appsettings.json +++ b/src/API/appsettings.json @@ -16,10 +16,6 @@ "ElasticConfiguration": { "Uri": "foo" }, - "Postgres": { - "ConnectionString": "foo", - "EnableSensitiveData": false - }, "AWS": { "Region": "foo" }, diff --git a/src/Baskets/Baskets.Core/Baskets.Core.csproj b/src/Baskets/Baskets.Core/Baskets.Core.csproj index 2f612bc..1830fc7 100644 --- a/src/Baskets/Baskets.Core/Baskets.Core.csproj +++ b/src/Baskets/Baskets.Core/Baskets.Core.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Baskets/Baskets.Core/Subscribers/Users/AddUser.cs b/src/Baskets/Baskets.Core/Subscribers/Users/AddUser.cs index 391fd59..1a43e71 100644 --- a/src/Baskets/Baskets.Core/Subscribers/Users/AddUser.cs +++ b/src/Baskets/Baskets.Core/Subscribers/Users/AddUser.cs @@ -1,5 +1,5 @@ using IGroceryStore.Baskets.Entities; -using IGroceryStore.Users.Contracts.Events; +using IGroceryStore.Shared.Contracts; using MassTransit; using Microsoft.Extensions.Logging; using MongoDB.Driver; diff --git a/src/Notifications/Notifications/Notifications.csproj b/src/Notifications/Notifications/Notifications.csproj index bece345..e2235fd 100644 --- a/src/Notifications/Notifications/Notifications.csproj +++ b/src/Notifications/Notifications/Notifications.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/API/Initializers/PostgresInitializer.cs b/src/Products/Products.Core/Persistence/PostgresInitializer.cs similarity index 92% rename from src/API/Initializers/PostgresInitializer.cs rename to src/Products/Products.Core/Persistence/PostgresInitializer.cs index dc66160..49c8bda 100644 --- a/src/API/Initializers/PostgresInitializer.cs +++ b/src/Products/Products.Core/Persistence/PostgresInitializer.cs @@ -1,8 +1,9 @@ using System.Reflection; using IGroceryStore.Shared.Configuration; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; -namespace IGroceryStore.API.Initializers; +namespace IGroceryStore.Products.Persistence; internal sealed class PostgresInitializer { diff --git a/src/Products/Products.Core/Products.Core.csproj b/src/Products/Products.Core/Products.Core.csproj index 5e5ec80..55ac181 100644 --- a/src/Products/Products.Core/Products.Core.csproj +++ b/src/Products/Products.Core/Products.Core.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Products/Products.Core/ProductsModule.cs b/src/Products/Products.Core/ProductsModule.cs index f9de150..dd77ee6 100644 --- a/src/Products/Products.Core/ProductsModule.cs +++ b/src/Products/Products.Core/ProductsModule.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using IGroceryStore.Products.Persistence; using IGroceryStore.Products.Persistence.Contexts; using IGroceryStore.Shared; using IGroceryStore.Shared.Common; @@ -21,7 +22,9 @@ public class ProductsModule : IModule public void Register(IServiceCollection services, IConfiguration configuration) { services.RegisterHandlers(); - + services.AddSingleton(); + System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + var options = configuration.GetOptions(); services.AddDbContext(ctx => ctx.UseNpgsql(options.ConnectionString) diff --git a/src/Products/Products.Core/modulesettings.Development.json b/src/Products/Products.Core/modulesettings.Development.json index e6ecabb..1ed5f79 100644 --- a/src/Products/Products.Core/modulesettings.Development.json +++ b/src/Products/Products.Core/modulesettings.Development.json @@ -1,5 +1,9 @@ { "Products": { "ModuleEnabled": true + }, + "Postgres": { + "ConnectionString": "User ID = postgres; Password = admin; Server = localhost; Port = 5432; Database = IGroceryStoreDb; Integrated Security = true; Pooling = true;", + "EnableSensitiveData": true } } \ No newline at end of file diff --git a/src/Products/Products.Core/modulesettings.Test.json b/src/Products/Products.Core/modulesettings.Test.json index e6ecabb..6e9c0c7 100644 --- a/src/Products/Products.Core/modulesettings.Test.json +++ b/src/Products/Products.Core/modulesettings.Test.json @@ -1,5 +1,9 @@ { "Products": { "ModuleEnabled": true + }, + "Postgres": { + "ConnectionString": "User ID = postgres; Password = admin; Server = localhost; Port = 5432; Database = IGroceryStoreDb; Integrated Security = true; Pooling = true;", + "EnableSensitiveData": false } } \ No newline at end of file diff --git a/src/Products/Products.Core/modulesettings.json b/src/Products/Products.Core/modulesettings.json index 91759e0..c639ece 100644 --- a/src/Products/Products.Core/modulesettings.json +++ b/src/Products/Products.Core/modulesettings.json @@ -1,5 +1,9 @@ { "Products": { "ModuleEnabled": false + }, + "Postgres": { + "ConnectionString": "foo", + "EnableSensitiveData": false } } \ No newline at end of file diff --git a/src/Shared/Shared/Common/AuditableEntity.cs b/src/Shared/Common/AuditableEntity.cs similarity index 100% rename from src/Shared/Shared/Common/AuditableEntity.cs rename to src/Shared/Common/AuditableEntity.cs diff --git a/src/Shared/Shared/Common/IModule.cs b/src/Shared/Common/IModule.cs similarity index 100% rename from src/Shared/Shared/Common/IModule.cs rename to src/Shared/Common/IModule.cs diff --git a/src/Shared/Shared/Configuration/AppContext.cs b/src/Shared/Configuration/AppContext.cs similarity index 100% rename from src/Shared/Shared/Configuration/AppContext.cs rename to src/Shared/Configuration/AppContext.cs diff --git a/src/Shared/Shared/Configuration/AppInitializer.cs b/src/Shared/Configuration/AppInitializer.cs similarity index 100% rename from src/Shared/Shared/Configuration/AppInitializer.cs rename to src/Shared/Configuration/AppInitializer.cs diff --git a/src/Shared/Shared/Configuration/EndpointsExtensions.cs b/src/Shared/Configuration/EndpointsExtensions.cs similarity index 100% rename from src/Shared/Shared/Configuration/EndpointsExtensions.cs rename to src/Shared/Configuration/EndpointsExtensions.cs diff --git a/src/Shared/Shared/Configuration/Extensions.cs b/src/Shared/Configuration/Extensions.cs similarity index 100% rename from src/Shared/Shared/Configuration/Extensions.cs rename to src/Shared/Configuration/Extensions.cs diff --git a/src/Shared/Shared/Configuration/GroceryStoreRouteBuilder.cs b/src/Shared/Configuration/GroceryStoreRouteBuilder.cs similarity index 100% rename from src/Shared/Shared/Configuration/GroceryStoreRouteBuilder.cs rename to src/Shared/Configuration/GroceryStoreRouteBuilder.cs diff --git a/src/Shared/Shared/Constants.cs b/src/Shared/Constants.cs similarity index 100% rename from src/Shared/Shared/Constants.cs rename to src/Shared/Constants.cs diff --git a/src/Shared/Contracts/Problems.cs b/src/Shared/Contracts/Problems.cs new file mode 100644 index 0000000..ac4be34 --- /dev/null +++ b/src/Shared/Contracts/Problems.cs @@ -0,0 +1,6 @@ +namespace IGroceryStore.Shared.Contracts; + +public class Problems +{ + //TODO: use in bad request responses +} diff --git a/src/Users/Users.Contracts/Events/UserCreated.cs b/src/Shared/Contracts/UserCreated.cs similarity index 51% rename from src/Users/Users.Contracts/Events/UserCreated.cs rename to src/Shared/Contracts/UserCreated.cs index 489187e..ba65ad4 100644 --- a/src/Users/Users.Contracts/Events/UserCreated.cs +++ b/src/Shared/Contracts/UserCreated.cs @@ -1,3 +1,3 @@ -namespace IGroceryStore.Users.Contracts.Events; +namespace IGroceryStore.Shared.Contracts; -public record UserCreated(Guid UserId, string FirstName, string LastName); \ No newline at end of file +public record UserCreated(Guid UserId, string FirstName, string LastName); diff --git a/src/Shared/Shared/EndpointBuilders/Extensions.cs b/src/Shared/EndpointBuilders/Extensions.cs similarity index 100% rename from src/Shared/Shared/EndpointBuilders/Extensions.cs rename to src/Shared/EndpointBuilders/Extensions.cs diff --git a/src/Shared/Shared/EndpointBuilders/Handlers.cs b/src/Shared/EndpointBuilders/Handlers.cs similarity index 100% rename from src/Shared/Shared/EndpointBuilders/Handlers.cs rename to src/Shared/EndpointBuilders/Handlers.cs diff --git a/src/Shared/Shared/EndpointBuilders/IEndpoint.cs b/src/Shared/EndpointBuilders/IEndpoint.cs similarity index 100% rename from src/Shared/Shared/EndpointBuilders/IEndpoint.cs rename to src/Shared/EndpointBuilders/IEndpoint.cs diff --git a/src/Shared/Shared/EndpointBuilders/IGroceryStoreRouteBuilder.cs b/src/Shared/EndpointBuilders/IGroceryStoreRouteBuilder.cs similarity index 100% rename from src/Shared/Shared/EndpointBuilders/IGroceryStoreRouteBuilder.cs rename to src/Shared/EndpointBuilders/IGroceryStoreRouteBuilder.cs diff --git a/src/Shared/Shared/Exceptions/GroceryStoreException.cs b/src/Shared/Exceptions/GroceryStoreException.cs similarity index 100% rename from src/Shared/Shared/Exceptions/GroceryStoreException.cs rename to src/Shared/Exceptions/GroceryStoreException.cs diff --git a/src/Shared/Shared/Exceptions/InvalidBasketIdException.cs b/src/Shared/Exceptions/InvalidBasketIdException.cs similarity index 100% rename from src/Shared/Shared/Exceptions/InvalidBasketIdException.cs rename to src/Shared/Exceptions/InvalidBasketIdException.cs diff --git a/src/Shared/Shared/Exceptions/InvalidProductIdException.cs b/src/Shared/Exceptions/InvalidProductIdException.cs similarity index 100% rename from src/Shared/Shared/Exceptions/InvalidProductIdException.cs rename to src/Shared/Exceptions/InvalidProductIdException.cs diff --git a/src/Shared/Shared/Exceptions/InvalidProductNameException.cs b/src/Shared/Exceptions/InvalidProductNameException.cs similarity index 100% rename from src/Shared/Shared/Exceptions/InvalidProductNameException.cs rename to src/Shared/Exceptions/InvalidProductNameException.cs diff --git a/src/Shared/Shared/Exceptions/InvalidUserIdException.cs b/src/Shared/Exceptions/InvalidUserIdException.cs similarity index 100% rename from src/Shared/Shared/Exceptions/InvalidUserIdException.cs rename to src/Shared/Exceptions/InvalidUserIdException.cs diff --git a/src/Shared/Shared/Filters/ValidationFilter.cs b/src/Shared/Filters/ValidationFilter.cs similarity index 100% rename from src/Shared/Shared/Filters/ValidationFilter.cs rename to src/Shared/Filters/ValidationFilter.cs diff --git a/src/Shared/Shared/Services/CurrentUserService.cs b/src/Shared/Services/CurrentUserService.cs similarity index 100% rename from src/Shared/Shared/Services/CurrentUserService.cs rename to src/Shared/Services/CurrentUserService.cs diff --git a/src/Shared/Shared/Services/DateTimeProvider.cs b/src/Shared/Services/DateTimeProvider.cs similarity index 84% rename from src/Shared/Shared/Services/DateTimeProvider.cs rename to src/Shared/Services/DateTimeProvider.cs index f27c787..d4207cc 100644 --- a/src/Shared/Shared/Services/DateTimeProvider.cs +++ b/src/Shared/Services/DateTimeProvider.cs @@ -13,3 +13,6 @@ public sealed class DateTimeProvider : IDateTimeProvider public DateOnly NowDateOnly => DateOnly.FromDateTime(DateTime.UtcNow); public TimeOnly NowTimeOnly => TimeOnly.FromDateTime(DateTime.UtcNow); } + +public delegate DateTime UtcNow(); +public delegate DateOnly DateOnlyUtcNot(); diff --git a/src/Shared/Shared/Services/SnowflakeService.cs b/src/Shared/Services/SnowflakeService.cs similarity index 100% rename from src/Shared/Shared/Services/SnowflakeService.cs rename to src/Shared/Services/SnowflakeService.cs diff --git a/src/Shared/Shared/Settings/Extensions.cs b/src/Shared/Settings/Extensions.cs similarity index 100% rename from src/Shared/Shared/Settings/Extensions.cs rename to src/Shared/Settings/Extensions.cs diff --git a/src/Shared/Shared/Settings/ISettings.cs b/src/Shared/Settings/ISettings.cs similarity index 100% rename from src/Shared/Shared/Settings/ISettings.cs rename to src/Shared/Settings/ISettings.cs diff --git a/src/Shared/Shared/Settings/PostgresSettings.cs b/src/Shared/Settings/PostgresSettings.cs similarity index 100% rename from src/Shared/Shared/Settings/PostgresSettings.cs rename to src/Shared/Settings/PostgresSettings.cs diff --git a/src/Shared/Shared/Settings/RabbitSettings.cs b/src/Shared/Settings/RabbitSettings.cs similarity index 100% rename from src/Shared/Shared/Settings/RabbitSettings.cs rename to src/Shared/Settings/RabbitSettings.cs diff --git a/src/Shared/Shared/Shared.csproj b/src/Shared/Shared.csproj similarity index 100% rename from src/Shared/Shared/Shared.csproj rename to src/Shared/Shared.csproj diff --git a/src/Shared/Shared/ValueObjects/BasketId.cs b/src/Shared/ValueObjects/BasketId.cs similarity index 100% rename from src/Shared/Shared/ValueObjects/BasketId.cs rename to src/Shared/ValueObjects/BasketId.cs diff --git a/src/Shared/Shared/ValueObjects/ProductId.cs b/src/Shared/ValueObjects/ProductId.cs similarity index 98% rename from src/Shared/Shared/ValueObjects/ProductId.cs rename to src/Shared/ValueObjects/ProductId.cs index 842eb9f..4e9d577 100644 --- a/src/Shared/Shared/ValueObjects/ProductId.cs +++ b/src/Shared/ValueObjects/ProductId.cs @@ -14,5 +14,4 @@ public ProductId(ulong value) public static implicit operator ulong(ProductId id) => id.Value; public static implicit operator ProductId(ulong id) => new(id); - -} \ No newline at end of file +} diff --git a/src/Shared/Shared/ValueObjects/ProductName.cs b/src/Shared/ValueObjects/ProductName.cs similarity index 100% rename from src/Shared/Shared/ValueObjects/ProductName.cs rename to src/Shared/ValueObjects/ProductName.cs diff --git a/src/Shared/Shared/ValueObjects/UserId.cs b/src/Shared/ValueObjects/UserId.cs similarity index 97% rename from src/Shared/Shared/ValueObjects/UserId.cs rename to src/Shared/ValueObjects/UserId.cs index 67199df..df03d19 100644 --- a/src/Shared/Shared/ValueObjects/UserId.cs +++ b/src/Shared/ValueObjects/UserId.cs @@ -4,7 +4,6 @@ namespace IGroceryStore.Shared.ValueObjects; public record UserId { - public UserId() { } @@ -18,5 +17,4 @@ public UserId(Guid value) public static implicit operator Guid(UserId id) => id.Value; public static implicit operator UserId(Guid id) => new(id); - -} \ No newline at end of file +} diff --git a/src/Shops/Shops.Core/Shops.Core.csproj b/src/Shops/Shops.Core/Shops.Core.csproj index 84332fc..35dcfef 100644 --- a/src/Shops/Shops.Core/Shops.Core.csproj +++ b/src/Shops/Shops.Core/Shops.Core.csproj @@ -18,12 +18,13 @@ - + + diff --git a/src/Shops/Shops.Core/Subscribers/Users/AddUser.cs b/src/Shops/Shops.Core/Subscribers/Users/AddUser.cs index 60dbb29..a35f9cf 100644 --- a/src/Shops/Shops.Core/Subscribers/Users/AddUser.cs +++ b/src/Shops/Shops.Core/Subscribers/Users/AddUser.cs @@ -1,7 +1,7 @@ +using IGroceryStore.Shared.Contracts; using IGroceryStore.Shops.Exceptions; using IGroceryStore.Shops.Repositories; using IGroceryStore.Shops.Repositories.Contracts; -using IGroceryStore.Users.Contracts.Events; using MassTransit; using Microsoft.Extensions.Logging; diff --git a/src/Users/Users.Contracts/Users.Contracts.csproj b/src/Users/Users.Contracts/Users.Contracts.csproj deleted file mode 100644 index 83a5ed6..0000000 --- a/src/Users/Users.Contracts/Users.Contracts.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net7.0 - enable - enable - IGroceryStore.Users.Contracts - IGroceryStore.Users.Contracts - preview - - - - - - - diff --git a/src/Users/Users.Core/Entities/LoginMetadata.cs b/src/Users/Users.Core/Entities/LoginMetadata.cs deleted file mode 100644 index bac19d1..0000000 --- a/src/Users/Users.Core/Entities/LoginMetadata.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace IGroceryStore.Users.Entities; - -internal sealed class LoginMetadata -{ - public LoginMetadata(ushort accessFailedCount = 0, DateTime? lockoutEnd = null) - { - AccessFailedCount = accessFailedCount; - LockoutEnd = lockoutEnd; - } - - private const int MaxLoginTry = 5; - public ushort AccessFailedCount { get; private set; } - public DateTime? LockoutEnd { get; private set; } - public bool IsLocked => LockoutEnd.HasValue && LockoutEnd.Value > DateTime.UtcNow; - - internal void ReportLoginFailure() - { - AccessFailedCount++; - if (AccessFailedCount >= MaxLoginTry) - { - Lock(); - } - } - - private void Lock() => LockoutEnd = DateTime.UtcNow.AddMinutes(5); -} diff --git a/src/Users/Users.Core/Entities/Preferences.cs b/src/Users/Users.Core/Entities/Preferences.cs deleted file mode 100644 index decc397..0000000 --- a/src/Users/Users.Core/Entities/Preferences.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace IGroceryStore.Users.Entities; - -internal class Preferences -{ - public string? ColorScheme { get; set; } -} diff --git a/src/Users/Users.Core/Entities/SecurityMetadata.cs b/src/Users/Users.Core/Entities/SecurityMetadata.cs deleted file mode 100644 index cd42933..0000000 --- a/src/Users/Users.Core/Entities/SecurityMetadata.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace IGroceryStore.Users.Entities; - -internal sealed class SecurityMetadata -{ - public bool TwoFactorEnabled { get; set; } - public bool EmailConfirmed { get; set; } -} diff --git a/src/Users/Users.Core/Entities/TokenStore.cs b/src/Users/Users.Core/Entities/TokenStore.cs deleted file mode 100644 index d124910..0000000 --- a/src/Users/Users.Core/Entities/TokenStore.cs +++ /dev/null @@ -1,26 +0,0 @@ -using IGroceryStore.Users.ValueObjects; - -namespace IGroceryStore.Users.Entities; - -internal sealed class TokenStore -{ - public TokenStore() => RemoveOldRefreshTokens(); - - private readonly List _refreshTokens = new(); - public IReadOnlyCollection RefreshTokens => _refreshTokens.AsReadOnly(); - - private void RemoveOldRefreshTokens() - => _refreshTokens.RemoveAll(x => x.ExpiresAt <= DateTime.UtcNow); - - internal void AddRefreshToken(RefreshToken refreshToken) - => _refreshTokens.Add(refreshToken); - - public bool TokenExist(string token) - => _refreshTokens.Exists(x => token == x.Value); - - public bool IsJtiValid(string token, Guid jti) - => _refreshTokens.Exists(x => token == x.Value && jti == x.Jti); - - public void RemoveAllRefreshTokens() - => _refreshTokens.RemoveAll(_ => true); -} diff --git a/src/Users/Users.Core/Entities/User.cs b/src/Users/Users.Core/Entities/User.cs deleted file mode 100644 index 5293704..0000000 --- a/src/Users/Users.Core/Entities/User.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Diagnostics; -using System.Net; -using IGroceryStore.Shared.Common; -using IGroceryStore.Shared.Exceptions; -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Services; -using IGroceryStore.Users.ValueObjects; - -namespace IGroceryStore.Users.Entities; - -internal sealed class User : AuditableEntity -{ - public User(PasswordHash passwordHash) - { - PasswordHash = passwordHash; - } - - public required UserId Id { get; init; } - public required FirstName FirstName { get; set; } - public required LastName LastName { get; set; } - public required Email Email { get; set; } - public PasswordHash PasswordHash { get; private set; } - public LoginMetadata LoginMetadata { get; init; } = new(); - public TokenStore TokenStore { get; init; } = new(); - public SecurityMetadata SecurityMetadata { get; init; } = new(); - public Preferences Preferences { get; init; } = new(); - - public bool UpdatePassword(string password, string oldPassword) - { - if (!HashingService.ValidatePassword(oldPassword, PasswordHash.Value)) return false; - - PasswordHash = HashingService.HashPassword(password); - return true; - } - public bool IsPasswordCorrect(string password) - { - if (IsLocked()) throw new UnreachableException( - "This should be checked before", - new LoggingTriesExceededException()); - - if (HashingService.ValidatePassword(password, PasswordHash.Value)) return true; - LoginMetadata.ReportLoginFailure(); - return false; - } - - public bool IsLocked() => LoginMetadata.IsLocked; -} - -internal class LoggingTriesExceededException : GroceryStoreException -{ - public LoggingTriesExceededException() : base("Try again after 5 min") - { - } - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/IncorrectPasswordException.cs b/src/Users/Users.Core/Exceptions/IncorrectPasswordException.cs deleted file mode 100644 index 59ecdce..0000000 --- a/src/Users/Users.Core/Exceptions/IncorrectPasswordException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -internal class IncorrectPasswordException : GroceryStoreException -{ - public IncorrectPasswordException() : base("Incorrect password") - { - } - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/InvalidClaimsException.cs b/src/Users/Users.Core/Exceptions/InvalidClaimsException.cs deleted file mode 100644 index 6caf01a..0000000 --- a/src/Users/Users.Core/Exceptions/InvalidClaimsException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class InvalidClaimsException : GroceryStoreException -{ - public InvalidClaimsException(string message) : base(message) - { - } - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/InvalidCredentialsException.cs b/src/Users/Users.Core/Exceptions/InvalidCredentialsException.cs deleted file mode 100644 index 42530fb..0000000 --- a/src/Users/Users.Core/Exceptions/InvalidCredentialsException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class InvalidCredentialsException : GroceryStoreException -{ - public InvalidCredentialsException() : base("Invalid credentials") - { - } - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/InvalidEmailException.cs b/src/Users/Users.Core/Exceptions/InvalidEmailException.cs deleted file mode 100644 index 71a57e3..0000000 --- a/src/Users/Users.Core/Exceptions/InvalidEmailException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class InvalidEmailException : GroceryStoreException -{ - public string Email { get; } - public InvalidEmailException(string email) : base($"Invalid email: {email}") - => Email = email; - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/InvalidFirstNameException.cs b/src/Users/Users.Core/Exceptions/InvalidFirstNameException.cs deleted file mode 100644 index bcadad9..0000000 --- a/src/Users/Users.Core/Exceptions/InvalidFirstNameException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class InvalidFirstNameException : GroceryStoreException -{ - public string FirstName { get; } - public InvalidFirstNameException(string firstName) : base($"Invalid first name: {firstName}") - => FirstName = firstName; - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/InvalidLastNameException.cs b/src/Users/Users.Core/Exceptions/InvalidLastNameException.cs deleted file mode 100644 index b26eff1..0000000 --- a/src/Users/Users.Core/Exceptions/InvalidLastNameException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class InvalidLastNameException : GroceryStoreException -{ - public string LastName { get; } - public InvalidLastNameException(string lastName) : base($"Invalid last name: {lastName}") - => LastName = lastName; - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/InvalidPasswordException.cs b/src/Users/Users.Core/Exceptions/InvalidPasswordException.cs deleted file mode 100644 index dfa6336..0000000 --- a/src/Users/Users.Core/Exceptions/InvalidPasswordException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class InvalidPasswordException : GroceryStoreException -{ - public InvalidPasswordException() : base("Invalid password") - { - } - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/PasswordDoesNotMatchException.cs b/src/Users/Users.Core/Exceptions/PasswordDoesNotMatchException.cs deleted file mode 100644 index 755fc9d..0000000 --- a/src/Users/Users.Core/Exceptions/PasswordDoesNotMatchException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class PasswordDoesNotMatchException : GroceryStoreException -{ - public PasswordDoesNotMatchException() : base("Password does not match") - { - } - - public override HttpStatusCode StatusCode => HttpStatusCode.BadRequest; -} diff --git a/src/Users/Users.Core/Exceptions/UserNotFoundException.cs b/src/Users/Users.Core/Exceptions/UserNotFoundException.cs deleted file mode 100644 index bfa9c38..0000000 --- a/src/Users/Users.Core/Exceptions/UserNotFoundException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Net; -using IGroceryStore.Shared.Exceptions; - -namespace IGroceryStore.Users.Exceptions; - -public class UserNotFoundException: GroceryStoreException -{ - public Guid Id { get; } - public UserNotFoundException(Guid id) : base($"User with id {id} not found") - => Id = id; - public override HttpStatusCode StatusCode => HttpStatusCode.NotFound; -} diff --git a/src/Users/Users.Core/Features/Tokens/Login.cs b/src/Users/Users.Core/Features/Tokens/Login.cs deleted file mode 100644 index dd870a6..0000000 --- a/src/Users/Users.Core/Features/Tokens/Login.cs +++ /dev/null @@ -1,70 +0,0 @@ -using IGroceryStore.Shared.EndpointBuilders; -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.Exceptions; -using IGroceryStore.Users.Persistence.Contexts; -using IGroceryStore.Users.ReadModels; -using IGroceryStore.Users.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace IGroceryStore.Users.Features.Tokens; - -internal record LoginWithUserAgent(LoginWithUserAgent.LoginWithUserAgentBody Body) : IHttpCommand -{ - internal record LoginWithUserAgentBody(string Email, - string Password, - //TODO: does it work? - [FromHeader(Name = "User-Agent")] string UserAgent); -} -internal record LoginResult(Guid UserId, TokensReadModel Tokens); - - -public class LoginEndpoint : IEndpoint -{ - public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Users.MapPost("tokens/login") - .Produces() - .Produces(400) - .Produces(400); -} - -internal class LoginHandler : IHttpCommandHandler -{ - private readonly ITokenManager _tokenManager; - private readonly UsersDbContext _context; - - public LoginHandler(ITokenManager tokenManager, UsersDbContext context) - { - _tokenManager = tokenManager; - _context = context; - } - - public async Task HandleAsync(LoginWithUserAgent command, CancellationToken cancellationToken = default) - { - var (email, password, userAgent) = command.Body; - var user = await _context.Users.FirstOrDefaultAsync(x => x.Email == email, cancellationToken); - if (user is null) return Results.BadRequest(new InvalidCredentialsException()); - - var loginResult = user.Login(password); - - if (loginResult.IsT1) return Results.BadRequest(loginResult.AsT1); - if (!loginResult.AsT0) return Results.BadRequest(new InvalidCredentialsException()); - - var (refreshToken, jwt) = _tokenManager.GenerateRefreshToken(user); - user.TryRemoveOldRefreshToken(userAgent); - user.AddRefreshToken(new ValueObjects.RefreshToken(userAgent, refreshToken)); - - _context.Users.Update(user); - await _context.SaveChangesAsync(cancellationToken); - - var tokens = new TokensReadModel - { - AccessToken = _tokenManager.GenerateAccessToken(user), - RefreshToken = jwt - }; - - var result = new LoginResult(user.Id, tokens); - return Results.Ok(result); - } -} diff --git a/src/Users/Users.Core/Features/Tokens/Refresh.cs b/src/Users/Users.Core/Features/Tokens/Refresh.cs deleted file mode 100644 index 4f0856a..0000000 --- a/src/Users/Users.Core/Features/Tokens/Refresh.cs +++ /dev/null @@ -1,74 +0,0 @@ -using IGroceryStore.Shared; -using IGroceryStore.Shared.EndpointBuilders; -using IGroceryStore.Shared.Services; -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Exceptions; -using IGroceryStore.Users.Persistence.Contexts; -using IGroceryStore.Users.ReadModels; -using IGroceryStore.Users.Services; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; -using Audience = IGroceryStore.Shared.Constants.Tokens.Audience; - -namespace IGroceryStore.Users.Features.Tokens; - -internal record RefreshToken : IHttpCommand; - -public class RefreshEndpoint : IEndpoint -{ - public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Users.MapPut("tokens/refresh") - .RequireAuthorization(_authorizeData) - .Produces() - .Produces(400) - .Produces(404); - - private readonly IAuthorizeData[] _authorizeData = { - new AuthorizeAttribute { AuthenticationSchemes = Audience.Refresh } - }; -} - -internal class RefreshTokenHandler : IHttpCommandHandler -{ - private readonly ITokenManager _tokenManager; - private readonly UsersDbContext _context; - private readonly ICurrentUserService _currentUserService; - - public RefreshTokenHandler(ITokenManager tokenManager, - UsersDbContext context, - ICurrentUserService currentUserService) - { - _tokenManager = tokenManager; - _context = context; - _currentUserService = currentUserService; - } - - public async Task HandleAsync(RefreshToken command, CancellationToken cancellationToken = default) - { - var tokenClaim = _currentUserService.Principal?.Claims.FirstOrDefault(x => x.Type is Constants.Claims.Name.RefreshToken); - var userId = _currentUserService.UserId; - - if (userId is null) return Results.BadRequest(new InvalidClaimsException("User id not found")); - if (tokenClaim is null) return Results.BadRequest(new InvalidClaimsException("Refresh token not found")); - if (string.IsNullOrWhiteSpace(tokenClaim.Value)) return Results.BadRequest(new InvalidClaimsException("Refresh token not found")); - - var user = await _context.Users.FirstOrDefaultAsync(x => x.Id == (UserId)userId, cancellationToken); - if (user is null) return Results.NotFound(new UserNotFoundException(userId.Value)); - - if (!user.TokenExist(tokenClaim.Value)) return Results.BadRequest(new InvalidClaimsException("Refresh token not found")); - var (refreshToken, jwt) = _tokenManager.GenerateRefreshToken(user); - user.UpdateRefreshToken(tokenClaim.Value, refreshToken); - - _context.Users.Update(user); - await _context.SaveChangesAsync(cancellationToken); - - var result = new TokensReadModel - { - AccessToken = _tokenManager.GenerateAccessToken(user), - RefreshToken = jwt - }; - return Results.Ok(result); - } -} diff --git a/src/Users/Users.Core/Features/Users/GetUser.cs b/src/Users/Users.Core/Features/Users/GetUser.cs deleted file mode 100644 index 824118c..0000000 --- a/src/Users/Users.Core/Features/Users/GetUser.cs +++ /dev/null @@ -1,44 +0,0 @@ -using IGroceryStore.Shared.EndpointBuilders; -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Exceptions; -using IGroceryStore.Users.Persistence.Contexts; -using IGroceryStore.Users.ReadModels; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; - -namespace IGroceryStore.Users.Features.Users; - -internal record GetUser(Guid Id) : IHttpQuery; - -public class GetUserEndpoint : IEndpoint -{ - public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Users.MapGet("{id}") - .Produces() - .Produces(404) - .Produces(401) - .WithName(nameof(GetUser)); -} - - -internal class GetUserHttpHandler : IHttpQueryHandler -{ - private readonly UsersDbContext _dbContext; - - public GetUserHttpHandler(UsersDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task HandleAsync(GetUser query, CancellationToken cancellationToken) - { - var user = await _dbContext.Users - .FirstOrDefaultAsync(x => x.Id == new UserId(query.Id), cancellationToken); - if (user is null) return Results.NotFound(new UserNotFoundException(query.Id)); - - var result = new UserReadModel(user.Id, user.FirstName, user.LastName, user.Email); - return Results.Ok(result); - } -} - diff --git a/src/Users/Users.Core/Features/Users/GetUsers.cs b/src/Users/Users.Core/Features/Users/GetUsers.cs deleted file mode 100644 index 045b008..0000000 --- a/src/Users/Users.Core/Features/Users/GetUsers.cs +++ /dev/null @@ -1,41 +0,0 @@ -using IGroceryStore.Shared.EndpointBuilders; -using IGroceryStore.Users.Persistence.Contexts; -using IGroceryStore.Users.ReadModels; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.EntityFrameworkCore; - -namespace IGroceryStore.Users.Features.Users; - -internal record GetUsers : IHttpQuery; -internal record UsersReadModel(IEnumerable Users, int Count); - - -public class GetUsersEndpoint : IEndpoint -{ - public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Users.MapGet("") - .Produces() - .Produces(401); -} - -internal class GetUsersHandler : IHttpQueryHandler -{ - private readonly UsersDbContext _dbContext; - - public GetUsersHandler(UsersDbContext dbContext) - { - _dbContext = dbContext; - } - - public async Task HandleAsync(GetUsers query, CancellationToken cancellationToken) - { - var users = await _dbContext.Users - .Select(x => new UserReadModel(x.Id, x.FirstName, x.LastName, x.Email)) - .AsNoTracking() - .ToListAsync(cancellationToken); - - var result = new UsersReadModel(users, users.Count); - return Results.Ok(result); - } -} diff --git a/src/Users/Users.Core/Features/Users/Register.cs b/src/Users/Users.Core/Features/Users/Register.cs deleted file mode 100644 index 3de6b27..0000000 --- a/src/Users/Users.Core/Features/Users/Register.cs +++ /dev/null @@ -1,91 +0,0 @@ -using FluentValidation; -using FluentValidation.Results; -using IGroceryStore.Shared.EndpointBuilders; -using IGroceryStore.Shared.Filters; -using IGroceryStore.Users.Contracts.Events; -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.Persistence.Mongo; -using MassTransit; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using ValidationResult = FluentValidation.Results.ValidationResult; - -namespace IGroceryStore.Users.Features.Users; - -internal record Register(Register.RegisterBody Body) : IHttpCommand -{ - public record RegisterBody(string Email, - string Password, - string ConfirmPassword, - string FirstName, - string LastName); -} - -public class RegisterUserEndpoint : IEndpoint -{ - public void RegisterEndpoint(IGroceryStoreRouteBuilder builder) => - builder.Users.MapPost("register") - .Produces(202) - .Produces(400) - .Produces(400) - .AllowAnonymous() - .AddEndpointFilter>() - .WithName(nameof(Register)); -} - -internal class RegisterHandler : IHttpCommandHandler -{ - private readonly IUserRepository _repository; - private readonly IBus _bus; - - public RegisterHandler(IUserRepository repository, IBus bus) - { - _repository = repository; - _bus = bus; - } - - public async Task HandleAsync(Register command, CancellationToken cancellationToken) - { - var (email, password, _, firstName, lastName) = command.Body; - - var user = new User(password) - { - Id = Guid.NewGuid(), - FirstName = firstName, - LastName = lastName, - Email = email - }; - - await _repository.AddAsync(user, cancellationToken); - await _bus.Publish(new UserCreated(user.Id, firstName, lastName), cancellationToken); - return Results.AcceptedAtRoute(nameof(GetUser),new {Id = user.Id.Value}); - } -} - -internal class CreateProductValidator : AbstractValidator -{ - public CreateProductValidator(IUserRepository repository) - { - RuleFor(x => x.Body.Password) - .NotEmpty() - .MinimumLength(8); - - RuleFor(x => x.Body.ConfirmPassword) - .Equal(x => x.Body.Password); - - RuleFor(x => x.Body.Email) - .NotEmpty() - .EmailAddress() - .CustomAsync(async (email, context, cancellationToken) => - { - if (!await repository.ExistByEmailAsync(email, cancellationToken)) return; - context.AddFailure(new ValidationFailure(nameof(Register.Body.Email), "Email already exists")); - }); - - RuleFor(x => x.Body.FirstName) - .NotEmpty(); - - RuleFor(x => x.Body.LastName) - .NotEmpty(); - } -} diff --git a/src/Users/Users.Core/Persistence/Configurations/UserDbConfiguration.cs b/src/Users/Users.Core/Persistence/Configurations/UserDbConfiguration.cs deleted file mode 100644 index c4b6436..0000000 --- a/src/Users/Users.Core/Persistence/Configurations/UserDbConfiguration.cs +++ /dev/null @@ -1,49 +0,0 @@ -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.ValueObjects; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace IGroceryStore.Users.Persistence.Configurations; - -internal sealed class UserDbConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - var passwordConverter = new ValueConverter(l => l.Value, - l => new PasswordHash(l)); - - builder.HasKey(x => x.Id); - builder.Property(x => x.Id) - .HasConversion(x => x.Value, x => new UserId(x)); - - builder.Property(x => x.FirstName) - .HasConversion(x => x.Value, x => new FirstName(x)); - builder.Property(x => x.Email) - .HasConversion(x => x.Value, x => new Email(x)); - builder.Property(x => x.LastName) - .HasConversion(x => x.Value, x => new LastName(x)); - - builder - .Property(typeof(List), "_refreshTokens") - .HasColumnName("RefreshTokens") - .IsRequired(false) - .HasColumnType("jsonb"); - - builder - .Property(typeof(PasswordHash), "_passwordHash") - .HasConversion(passwordConverter) - .HasColumnName("PasswordHash"); - - - builder - .Property(typeof(ushort), "_accessFailedCount") - .HasColumnName("AccessFailedCount"); - - builder - .Property(typeof(DateTime), "_lockoutEnd") - .HasColumnName("LockoutEnd"); - - } -} diff --git a/src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs b/src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs deleted file mode 100644 index c2b5320..0000000 --- a/src/Users/Users.Core/Persistence/Contexts/UsersDbContext.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Reflection; -using IGroceryStore.Shared.Common; -using IGroceryStore.Shared.Services; -using IGroceryStore.Users.Entities; -using Microsoft.EntityFrameworkCore; - -namespace IGroceryStore.Users.Persistence.Contexts; - -internal class UsersDbContext : DbContext -{ - private readonly ICurrentUserService _currentUserService; - private readonly IDateTimeProvider _dateTimeProvider; - - public UsersDbContext(DbContextOptions options, - ICurrentUserService currentUserService, - IDateTimeProvider dateTimeProvider) - : base(options) - { - _currentUserService = currentUserService; - _dateTimeProvider = dateTimeProvider; - } - - public DbSet Users => Set(); - - public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) - { - foreach (var entry in ChangeTracker.Entries()) - { - switch (entry.State) - { - case EntityState.Added: - entry.Entity.CreatedBy = _currentUserService.UserId; - entry.Entity.Created = _dateTimeProvider.Now; - break; - - case EntityState.Modified: - entry.Entity.LastModifiedBy = _currentUserService.UserId; - entry.Entity.LastModified = _dateTimeProvider.Now; - break; - } - } - - return await base.SaveChangesAsync(cancellationToken); - } - - protected override void OnModelCreating(ModelBuilder builder) - { - if (Database.IsRelational()) builder.HasDefaultSchema("IGroceryStore.Users"); - - builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); - - base.OnModelCreating(builder); - } -} diff --git a/src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs b/src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs deleted file mode 100644 index dfa4438..0000000 --- a/src/Users/Users.Core/Persistence/Mongo/DbModels/DbModels.cs +++ /dev/null @@ -1,27 +0,0 @@ -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.ValueObjects; - -namespace IGroceryStore.Users.Persistence.Mongo.DbModels; - -internal record UserDbModel -{ - public required string Id { get; init; } - public required string Email { get; init; } - public required string FirstName { get; init; } - public required string LastName { get; init; } - public required string PasswordHash { get; init; } - - public required LoginMetadataDbModel LoginMetadata { get; init; } = new(); - public Preferences Preferences { get; init; } = new(); - public TokenStore TokenStore { get; init; } = new(); - public SecurityMetadata SecurityMetadata { get; init; } = new(); -} - -internal class LoginMetadataDbModel -{ - public ushort AccessFailedCount { get; init; } - public DateTime? LockoutEnd { get; init; } -} - - - diff --git a/src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs b/src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs deleted file mode 100644 index 9abdef1..0000000 --- a/src/Users/Users.Core/Persistence/Mongo/UsersRepository.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Diagnostics; -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Entities; -using IGroceryStore.Users.Persistence.Mongo.DbModels; -using IGroceryStore.Users.ValueObjects; -using MongoDB.Driver; - -namespace IGroceryStore.Users.Persistence.Mongo; - -internal interface IUserRepository -{ - Task GetAsync(UserId id, CancellationToken cancellationToken); - Task GetAsync(Email email, CancellationToken cancellationToken); - Task ExistByEmailAsync(string email, CancellationToken cancellationToken); - Task AddAsync(User user, CancellationToken cancellationToken); - Task UpdateAsync(User user, CancellationToken cancellationToken); -} - -internal class UsersRepository : IUserRepository -{ - private readonly IMongoCollection _usersCollection; - - public UsersRepository(IMongoCollection usersCollection) - { - _usersCollection = usersCollection; - } - - public async Task GetAsync(UserId id, CancellationToken cancellationToken) - { - var model = await _usersCollection - .Find(x => x.Id == id.Value.ToString()) - .FirstOrDefaultAsync(); - - throw new NotImplementedException(); - } - - public Task GetAsync(Email email, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public async Task ExistByEmailAsync(string email, CancellationToken cancellationToken) - { - var documentsCount = await _usersCollection - .Find(x => x.Email == email) - .CountDocumentsAsync(cancellationToken); - return documentsCount > 0; - } - - public async Task AddAsync(User user, CancellationToken cancellationToken) - { - if (await ExistByEmailAsync(user.Email, cancellationToken)) - throw new UnreachableException("This should be validated on handler level"); - - // var userDbModel = new UserDbModel - // { - // Id = user.Id.ToString(), - // FirstName = user.FirstName, - // LastName = user.LastName, - // Email = user.Email, - // - // }; - throw new NotImplementedException(); - - //await _usersCollection.InsertOneAsync(userDbModel); - } - - public Task UpdateAsync(User user, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } -} diff --git a/src/Users/Users.Core/ReadModels/TokensReadModel.cs b/src/Users/Users.Core/ReadModels/TokensReadModel.cs deleted file mode 100644 index a035727..0000000 --- a/src/Users/Users.Core/ReadModels/TokensReadModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace IGroceryStore.Users.ReadModels -{ - public class TokensReadModel - { - public required string AccessToken { get; init; } - public required string RefreshToken { get; init; } - } -} diff --git a/src/Users/Users.Core/ReadModels/UserReadModel.cs b/src/Users/Users.Core/ReadModels/UserReadModel.cs deleted file mode 100644 index 0b7a59d..0000000 --- a/src/Users/Users.Core/ReadModels/UserReadModel.cs +++ /dev/null @@ -1,2 +0,0 @@ -namespace IGroceryStore.Users.ReadModels; -public record UserReadModel(Guid Id, string FirstName, string LastName, string Email); diff --git a/src/Users/Users.Core/Services/HashingService.cs b/src/Users/Users.Core/Services/HashingService.cs deleted file mode 100644 index bb395c4..0000000 --- a/src/Users/Users.Core/Services/HashingService.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace IGroceryStore.Users.Services; - -public static class HashingService -{ - private static string GetRandomSalt() - { - return BCrypt.Net.BCrypt.GenerateSalt(8); - } - - public static string HashPassword(string password) - { - return BCrypt.Net.BCrypt.HashPassword(password, GetRandomSalt()); - } - - public static bool ValidatePassword(string password, string correctHash) - { - return BCrypt.Net.BCrypt.Verify(password, correctHash); - } -} diff --git a/src/Users/Users.Core/Services/ITokenManager.cs b/src/Users/Users.Core/Services/ITokenManager.cs deleted file mode 100644 index 59b4a45..0000000 --- a/src/Users/Users.Core/Services/ITokenManager.cs +++ /dev/null @@ -1,10 +0,0 @@ -using IGroceryStore.Users.Entities; - -namespace IGroceryStore.Users.Services; - -public interface ITokenManager -{ - string GenerateAccessToken(User user); - IDictionary VerifyToken(string token); - (string refreshToken, string jwt) GenerateRefreshToken(User user); -} diff --git a/src/Users/Users.Core/Users.Core.csproj b/src/Users/Users.Core/Users.Core.csproj deleted file mode 100644 index 8b6d782..0000000 --- a/src/Users/Users.Core/Users.Core.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net7.0 - enable - enable - IGroceryStore.Users - IGroceryStore.Users - preview - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Users/Users.Core/UsersModule.cs b/src/Users/Users.Core/UsersModule.cs deleted file mode 100644 index 665f779..0000000 --- a/src/Users/Users.Core/UsersModule.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Diagnostics; -using IGroceryStore.Shared; -using IGroceryStore.Shared.Common; -using IGroceryStore.Shared.Configuration; -using IGroceryStore.Shared.Settings; -using IGroceryStore.Users.Persistence.Contexts; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace IGroceryStore.Users; - -public class UsersModule : IModule -{ - public string Name => Source.Name; - public static ActivitySource Source { get; } = new("Users", "1.0.0.0"); - - public void Register(IServiceCollection services, IConfiguration configuration) - { - services.RegisterHandlers(); - - var options = configuration.GetOptions(); - services.AddDbContext(ctx => - ctx.UseNpgsql(options.ConnectionString) - .EnableSensitiveDataLogging(options.EnableSensitiveData)); - } - - public void Use(IApplicationBuilder app) - { - } - - public void Expose(IEndpointRouteBuilder endpoints) - { - endpoints.MapGet($"/api/health/{Name.ToLower()}", () => $"{Name} module is healthy") - .WithTags(Constants.SwaggerTags.HealthChecks); - - endpoints.RegisterEndpoints(); - } -} diff --git a/src/Users/Users.Core/ValueObjects/Email.cs b/src/Users/Users.Core/ValueObjects/Email.cs deleted file mode 100644 index fb91e5f..0000000 --- a/src/Users/Users.Core/ValueObjects/Email.cs +++ /dev/null @@ -1,20 +0,0 @@ -using IGroceryStore.Users.Exceptions; - -namespace IGroceryStore.Users.ValueObjects; - -public record Email -{ - public string Value { get; } - - public Email(string value) - { - if (string.IsNullOrEmpty(value)) - { - throw new InvalidEmailException(value); - } - Value = value; - } - - public static implicit operator string(Email email) => email.Value; - public static implicit operator Email(string value) => new(value); -} diff --git a/src/Users/Users.Core/ValueObjects/FirstName.cs b/src/Users/Users.Core/ValueObjects/FirstName.cs deleted file mode 100644 index b526647..0000000 --- a/src/Users/Users.Core/ValueObjects/FirstName.cs +++ /dev/null @@ -1,20 +0,0 @@ -using IGroceryStore.Users.Exceptions; - -namespace IGroceryStore.Users.ValueObjects; - -public record FirstName -{ - public string Value { get; } - - public FirstName(string value) - { - if (string.IsNullOrEmpty(value)) - { - throw new InvalidFirstNameException(value); - } - Value = value; - } - - public static implicit operator string(FirstName lastName) => lastName.Value; - public static implicit operator FirstName(string value) => new(value); -} diff --git a/src/Users/Users.Core/ValueObjects/LastName.cs b/src/Users/Users.Core/ValueObjects/LastName.cs deleted file mode 100644 index a8a96b1..0000000 --- a/src/Users/Users.Core/ValueObjects/LastName.cs +++ /dev/null @@ -1,21 +0,0 @@ -using IGroceryStore.Users.Exceptions; - -namespace IGroceryStore.Users.ValueObjects; - -public record LastName -{ - public string Value { get; } - - public LastName(string value) - { - if (string.IsNullOrEmpty(value)) - { - throw new InvalidLastNameException(value); - } - Value = value; - } - - public static implicit operator string(LastName lastName) => lastName.Value; - public static implicit operator LastName(string value) => new(value); - -} diff --git a/src/Users/Users.Core/ValueObjects/PasswordHash.cs b/src/Users/Users.Core/ValueObjects/PasswordHash.cs deleted file mode 100644 index 0929b3f..0000000 --- a/src/Users/Users.Core/ValueObjects/PasswordHash.cs +++ /dev/null @@ -1,11 +0,0 @@ -using IGroceryStore.Users.Exceptions; - -namespace IGroceryStore.Users.ValueObjects; - -internal sealed record PasswordHash(string Value) -{ - public string Value { get; } = Value ?? throw new InvalidPasswordException(); - - public static implicit operator string(PasswordHash passwordHash) => passwordHash.Value; - public static implicit operator PasswordHash(string value) => new(value); -} diff --git a/src/Users/Users.Core/ValueObjects/RefreshToken.cs b/src/Users/Users.Core/ValueObjects/RefreshToken.cs deleted file mode 100644 index e7fc680..0000000 --- a/src/Users/Users.Core/ValueObjects/RefreshToken.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace IGroceryStore.Users.ValueObjects; - -internal sealed record RefreshToken(string Value, DateTime ExpiresAt, Guid Jti); diff --git a/src/Users/Users.Core/modulesettings.Development.json b/src/Users/Users.Core/modulesettings.Development.json deleted file mode 100644 index 197ec00..0000000 --- a/src/Users/Users.Core/modulesettings.Development.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Users": { - "ModuleEnabled": false - } -} \ No newline at end of file diff --git a/src/Users/Users.Core/modulesettings.Test.json b/src/Users/Users.Core/modulesettings.Test.json deleted file mode 100644 index 77db69a..0000000 --- a/src/Users/Users.Core/modulesettings.Test.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Users": { - "ModuleEnabled": true - } -} \ No newline at end of file diff --git a/src/Users/Users.Core/modulesettings.json b/src/Users/Users.Core/modulesettings.json deleted file mode 100644 index 197ec00..0000000 --- a/src/Users/Users.Core/modulesettings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Users": { - "ModuleEnabled": false - } -} \ No newline at end of file diff --git a/src/Worker/Worker.csproj b/src/Worker/Worker.csproj index 57bf2ec..68b93fc 100644 --- a/src/Worker/Worker.csproj +++ b/src/Worker/Worker.csproj @@ -14,6 +14,7 @@ + diff --git a/tests/Shops/Shops.IntegrationTests/Test.cs b/tests/Shops/Shops.IntegrationTests/Test.cs new file mode 100644 index 0000000..5dba310 --- /dev/null +++ b/tests/Shops/Shops.IntegrationTests/Test.cs @@ -0,0 +1,71 @@ +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using IGroceryStore.Shops.Repositories; + +public sealed class LocalDynamoDbFixture : IAsyncLifetime +{ + private readonly TestcontainersContainer _testContainer; + + public LocalDynamoDbFixture() + { + _testContainer = new TestcontainersBuilder() + .WithImage("amazon/dynamodb-local:latest") + .WithPortBinding(8000) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(8000)) + .Build(); + } + + public IShopsRepository SystemUnderTest { get; private set; } = default!; + + public async Task InitializeAsync() + { + await _testContainer.StartAsync(); + + var credentials = new BasicAWSCredentials("FAKE", "FAKE"); + var dynamoDbConfig = new AmazonDynamoDBConfig + { + ServiceURL = $"http://localhost:{_testContainer.GetMappedPublicPort(8000)}", + AuthenticationRegion = "eu-central-1" + }; + var dynamoDb = new AmazonDynamoDBClient(credentials, dynamoDbConfig); + + SystemUnderTest = new ShopsRepository(dynamoDb); + + await CreateTablesAsync(dynamoDb); + } + + public async Task DisposeAsync() + { + await _testContainer.DisposeAsync(); + } + + private async Task CreateTablesAsync(IAmazonDynamoDB dynamoDb) + { + var tableRequest = new CreateTableRequest + { + TableName = "user_trades", + ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = 1, WriteCapacityUnits = 1 }, + KeySchema = new List + { + new() + { + AttributeName = "pk", KeyType = KeyType.HASH // Partition key + }, + new() + { + AttributeName = "sk", KeyType = KeyType.RANGE // Sort key + } + }, + AttributeDefinitions = new List + { + new() { AttributeName = "pk", AttributeType = ScalarAttributeType.S }, + new() { AttributeName = "sk", AttributeType = ScalarAttributeType.S } + } + }; + + await dynamoDb.CreateTableAsync(tableRequest); + } +} diff --git a/tests/Users/Users.IntegrationTests/DbConfigExtensions.cs b/tests/Users/Users.IntegrationTests/DbConfigExtensions.cs deleted file mode 100644 index b4fb687..0000000 --- a/tests/Users/Users.IntegrationTests/DbConfigExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DotNet.Testcontainers.Containers; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; - -namespace IGroceryStore.Users.IntegrationTests; - -public static class DbConfigExtensions -{ - public static void CleanDbContextOptions(this IServiceCollection services) - where T : DbContext - { - var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); - if (descriptor != null) services.Remove(descriptor); - services.RemoveAll(typeof(T)); - } - - public static void AddPostgresContext(this IServiceCollection services, TestcontainerDatabase dbContainer) - where T : DbContext - { - services.AddDbContext(ctx => - ctx.UseNpgsql(dbContainer.ConnectionString)); - } -} diff --git a/tests/Users/Users.IntegrationTests/TestUsers.cs b/tests/Users/Users.IntegrationTests/TestUsers.cs deleted file mode 100644 index ca1b5d5..0000000 --- a/tests/Users/Users.IntegrationTests/TestUsers.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Bogus; -using IGroceryStore.Users.Features.Users; - -namespace IGroceryStore.Users.IntegrationTests; - -internal static class TestUsers -{ - private static readonly Faker UserGenerator = new RegisterFaker(); - - public static readonly List Registers = UserGenerator.Generate(10); - public static readonly Register Register = Registers.First(); - - private sealed class RegisterFaker : Faker - { - public RegisterFaker() - { - CustomInstantiator(ResolveConstructor); - } - - private Register ResolveConstructor(Faker faker) - { - var password = Guid.NewGuid().ToString(); - - var body = new Register.RegisterBody( - faker.Person.Email, - password, - password, - faker.Person.FirstName, - faker.Person.LastName); - - return new Register(body); - } - } -} diff --git a/tests/Users/Users.IntegrationTests/UserApiFactory.cs b/tests/Users/Users.IntegrationTests/UserApiFactory.cs deleted file mode 100644 index 1bad542..0000000 --- a/tests/Users/Users.IntegrationTests/UserApiFactory.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Data.Common; -using System.Security.Claims; -using Bogus; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Configurations; -using DotNet.Testcontainers.Containers; -using IGroceryStore.API; -using IGroceryStore.API.Initializers; -using IGroceryStore.Products.Persistence.Contexts; -using IGroceryStore.Shared; -using IGroceryStore.Shared.Services; -using IGroceryStore.Shared.Tests.Auth; -using IGroceryStore.Users.Contracts.Events; -using IGroceryStore.Users.Persistence.Contexts; -using MassTransit; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.JsonWebTokens; -using Npgsql; -using Respawn; - -namespace IGroceryStore.Users.IntegrationTests; - -public class UserApiFactory : WebApplicationFactory, IAsyncLifetime -{ - private readonly MockUser _user; - private Respawner _respawner = default!; - private DbConnection _dbConnection = default!; - private readonly TestcontainerDatabase _dbContainer = - new TestcontainersBuilder() - .WithDatabase(new PostgreSqlTestcontainerConfiguration - { - Database = Guid.NewGuid().ToString(), - Username = "postgres", - Password = "postgres" - }) - .WithAutoRemove(true) - .WithCleanUp(true) - .Build(); - - public UserApiFactory() - { - _user = new MockUser(new Claim(Constants.Claims.Name.UserId, "1"), - new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString())); - Randomizer.Seed = new Random(420); - VerifierSettings.ScrubInlineGuids(); - } - - public HttpClient HttpClient { get; private set; } = default!; - - protected override void ConfigureWebHost(IWebHostBuilder builder) - { - builder.ConfigureLogging(logging => { logging.ClearProviders(); }); - - builder.UseEnvironment(EnvironmentService.TestEnvironment); - - builder.ConfigureServices(services => - { - services.AddMassTransitTestHarness(x => - { - x.AddHandler(context => context.ConsumeCompleted); - }); - - - }); - - builder.ConfigureTestServices(services => - { - // we need all because we are running migrations in app startup - services.CleanDbContextOptions(); - services.CleanDbContextOptions(); - - services.AddPostgresContext(_dbContainer); - services.AddPostgresContext(_dbContainer); - - services.AddTestAuthentication(); - - services.AddSingleton(_ => _user); - }); - } - - public async Task InitializeAsync() - { - await _dbContainer.StartAsync(); - _dbConnection = new NpgsqlConnection(_dbContainer.ConnectionString); - HttpClient = CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - await InitializeRespawner(); - } - - public async Task ResetDatabaseAsync() => await _respawner.ResetAsync(_dbConnection); - - private async Task InitializeRespawner() - { - await _dbConnection.OpenAsync(); - _respawner = await Respawner.CreateAsync(_dbConnection, new RespawnerOptions - { - DbAdapter = DbAdapter.Postgres, - SchemasToInclude = new[] { "IGroceryStore.Users" }, - }); - } - - async Task IAsyncLifetime.DisposeAsync() - { - await _dbContainer.DisposeAsync(); - } -} diff --git a/tests/Users/Users.IntegrationTests/UserCollection.cs b/tests/Users/Users.IntegrationTests/UserCollection.cs deleted file mode 100644 index ea80962..0000000 --- a/tests/Users/Users.IntegrationTests/UserCollection.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace IGroceryStore.Users.IntegrationTests; - -[CollectionDefinition("UserCollection")] -public class UserCollection : ICollectionFixture -{ -} diff --git a/tests/Users/Users.IntegrationTests/UserTestHelper.cs b/tests/Users/Users.IntegrationTests/UserTestHelper.cs deleted file mode 100644 index 840dc26..0000000 --- a/tests/Users/Users.IntegrationTests/UserTestHelper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using FluentAssertions; -using IGroceryStore.API; -using IGroceryStore.Shared.ValueObjects; -using IGroceryStore.Users.Persistence.Contexts; -using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; - -namespace IGroceryStore.Users.IntegrationTests; - -public static class UserTestHelper -{ - internal static async Task RemoveUserById(this WebApplicationFactory apiFactory, Guid id) where T: class, IApiMarker - { - using var scope = apiFactory.Services.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - - var user = await context.Users - .Where(x => x.Id == new UserId(id)) - .FirstOrDefaultAsync(); - - user.Should().NotBeNull(); - if (user is not null) - { - context.Remove(user); - await context.SaveChangesAsync(); - } - } -} diff --git a/tests/Users/Users.IntegrationTests/Users.IntegrationTests.csproj b/tests/Users/Users.IntegrationTests/Users.IntegrationTests.csproj deleted file mode 100644 index 60e1945..0000000 --- a/tests/Users/Users.IntegrationTests/Users.IntegrationTests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net7.0 - enable - enable - false - preview - IGroceryStore.Users.IntegrationTests - IGroceryStore.Users.IntegrationTests - - - - - - - - - - - - - - RegisterTests.cs - - - - diff --git a/tests/Users/Users.IntegrationTests/Users/GetUserTests.GetUser_ReturnsUser_WhenUserExists.verified.txt b/tests/Users/Users.IntegrationTests/Users/GetUserTests.GetUser_ReturnsUser_WhenUserExists.verified.txt deleted file mode 100644 index ce13cda..0000000 --- a/tests/Users/Users.IntegrationTests/Users/GetUserTests.GetUser_ReturnsUser_WhenUserExists.verified.txt +++ /dev/null @@ -1,33 +0,0 @@ -{ - response: { - Version: 1.1, - Content: { - Headers: [ - { - Content-Type: [ - application/json; charset=utf-8 - ] - } - ] - }, - StatusCode: OK, - ReasonPhrase: OK, - Headers: [], - TrailingHeaders: [], - RequestMessage: { - Version: 1.1, - Method: { - Method: GET - }, - RequestUri: http://localhost/api/users/Guid_1, - Headers: [] - }, - IsSuccessStatusCode: true - }, - user: { - Id: Guid_1, - FirstName: Timothy, - LastName: Heathcote, - Email: Timothy14@yahoo.com - } -} \ No newline at end of file diff --git a/tests/Users/Users.IntegrationTests/Users/GetUserTests.cs b/tests/Users/Users.IntegrationTests/Users/GetUserTests.cs deleted file mode 100644 index dbaebf1..0000000 --- a/tests/Users/Users.IntegrationTests/Users/GetUserTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Security.Claims; -using FluentAssertions; -using IGroceryStore.Shared; -using IGroceryStore.Shared.Tests.Auth; -using IGroceryStore.Users.ReadModels; -using Microsoft.AspNetCore.TestHost; -using Microsoft.IdentityModel.JsonWebTokens; - -namespace IGroceryStore.Users.IntegrationTests.Users; - -[UsesVerify] -[Collection("UserCollection")] -public class GetUserTests : IAsyncLifetime -{ - private readonly HttpClient _client; - private readonly Func _resetDatabase; - - public GetUserTests(UserApiFactory apiFactory) - { - _client = apiFactory.HttpClient; - _resetDatabase = apiFactory.ResetDatabaseAsync; - apiFactory - .WithWebHostBuilder(builder => - builder.ConfigureTestServices(services => - { - services.RegisterUser(new[] - { - new Claim(Constants.Claims.Name.UserId, "1"), - new Claim(JwtRegisteredClaimNames.Exp, - DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString()) - }); - })); // override authorized user; - } - - [Fact] - public async Task GetUser_ReturnsUser_WhenUserExists() - { - // Arrange - var registerRequest = TestUsers.Register; - var responseWithUserLocation = await _client.PostAsJsonAsync("api/users/register", registerRequest.Body); - responseWithUserLocation.StatusCode.Should().Be(HttpStatusCode.Accepted); - - // Act - var response = await _client.GetAsync(responseWithUserLocation.Headers.Location); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - var user = await response.Content.ReadFromJsonAsync(); - user.Should().NotBeNull(); - - response.RequestMessage!.RequestUri.Should().Be($"http://localhost/api/users/{user!.Id}"); - // TODO: Something is wrong with scrubber - response.RequestMessage!.RequestUri = new Uri(response.RequestMessage.RequestUri! - .ToString() - .Replace($"{user.Id}", "Guid_1")); - await Verify(new { response, user }); - } - - - public Task InitializeAsync() => Task.CompletedTask; - public Task DisposeAsync() => _resetDatabase(); -} diff --git a/tests/Users/Users.IntegrationTests/Users/RegisterTests.Register_CreatesUser_WhenDataIsValid.verified.txt b/tests/Users/Users.IntegrationTests/Users/RegisterTests.Register_CreatesUser_WhenDataIsValid.verified.txt deleted file mode 100644 index dbfe403..0000000 --- a/tests/Users/Users.IntegrationTests/Users/RegisterTests.Register_CreatesUser_WhenDataIsValid.verified.txt +++ /dev/null @@ -1,48 +0,0 @@ -{ - Version: 1.1, - Content: { - Headers: [] - }, - StatusCode: Accepted, - ReasonPhrase: Accepted, - Headers: [ - { - Location: [ - http://localhost/api/users/Guid_1 - ] - } - ], - TrailingHeaders: [], - RequestMessage: { - Version: 1.1, - Content: { - ObjectType: Register.RegisterBody, - Value: { - Email: Timothy14@yahoo.com, - Password: Guid_2, - ConfirmPassword: Guid_2, - FirstName: Timothy, - LastName: Heathcote - }, - Headers: [ - { - Content-Type: [ - application/json; charset=utf-8 - ] - } - ] - }, - Method: { - Method: POST - }, - RequestUri: http://localhost/api/users/register, - Headers: [ - { - Transfer-Encoding: [ - chunked - ] - } - ] - }, - IsSuccessStatusCode: true -} \ No newline at end of file diff --git a/tests/Users/Users.IntegrationTests/Users/RegisterTests.cs b/tests/Users/Users.IntegrationTests/Users/RegisterTests.cs deleted file mode 100644 index 41a2875..0000000 --- a/tests/Users/Users.IntegrationTests/Users/RegisterTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using FluentAssertions; -using MassTransit.Testing; - -namespace IGroceryStore.Users.IntegrationTests.Users; - -[UsesVerify] -[Collection("UserCollection")] -public class RegisterTests : IAsyncLifetime -{ - private readonly HttpClient _client; - private readonly Func _resetDatabase; - //TODO: Add check for harness - private readonly ITestHarness _testHarness; - - public RegisterTests(UserApiFactory apiFactory) - { - _resetDatabase = apiFactory.ResetDatabaseAsync; - _testHarness = apiFactory.Services.GetTestHarness(); - _client = apiFactory.HttpClient; - } - - [Fact] - public async Task Register_CreatesUser_WhenDataIsValid() - { - // Arrange - var registerRequest = TestUsers.Register; - - // Act - var response = await _client.PostAsJsonAsync("api/users/register", registerRequest.Body); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.Accepted); - await Verify(response); - } - - public Task InitializeAsync() => Task.CompletedTask; - public Task DisposeAsync() => _resetDatabase(); -} diff --git a/tests/Users/Users.UnitTests/Users.UnitTests.csproj b/tests/Users/Users.UnitTests/Users.UnitTests.csproj deleted file mode 100644 index 6316e6f..0000000 --- a/tests/Users/Users.UnitTests/Users.UnitTests.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net7.0 - enable - enable - false - preview - IGroceryStore.Users.UnitTests - IGroceryStore.Users.UnitTests - - - - - - - - From 427cf186dd05347b98cd697a217444a9db1afda1 Mon Sep 17 00:00:00 2001 From: Adrian Franczak <44712992+Nairda015@users.noreply.github.com> Date: Mon, 5 Dec 2022 01:21:58 +0100 Subject: [PATCH 3/4] add tests for baskets --- IGroceryStore.sln | 6 -- requests/brands.http | 6 -- requests/users.http | 11 --- src/API/API.csproj | 1 + .../AuthenticationConfiguration.cs | 1 - src/API/Configuration/AwsConfiguration.cs | 24 +++++++ src/Shared/Contracts/IMessage.cs | 6 ++ src/Shared/Contracts/UserCreated.cs | 24 ++++++- src/Shared/SNS/MessageExtensions.cs | 31 +++++++++ src/Shared/SNS/SnsPublisher.cs | 40 +++++++++++ src/Shared/SQS/HandlerExtensions.cs | 21 ++++++ src/Shared/SQS/IMessageHandler.cs | 9 +++ src/Shared/SQS/MessageDispatcher.cs | 45 ++++++++++++ src/Shared/SQS/SqsConsumerService.cs | 68 +++++++++++++++++++ src/Shared/Shared.csproj | 2 + .../Baskets/AddBasketTests.cs | 34 ++++++++++ .../Configuration/BasketApiFactory.cs | 1 + .../Configuration/BasketsCollection.cs | 8 +++ .../Configuration/UserTestHelper.cs | 20 ++++++ tests/Shared/Auth/AuthConstants.cs | 7 -- .../Auth/AuthServiceCollectionExtensions.cs | 6 +- tests/Shared/Auth/MockUser.cs | 21 +++--- tests/Shared/Auth/TestAuthHandler.cs | 4 +- tests/Shared/Shared.csproj | 1 - tests/Shared/TestConstants.cs | 16 +++++ tools/docker/docker-compose.yml | 4 +- 26 files changed, 366 insertions(+), 51 deletions(-) delete mode 100644 requests/brands.http delete mode 100644 requests/users.http create mode 100644 src/Shared/Contracts/IMessage.cs create mode 100644 src/Shared/SNS/MessageExtensions.cs create mode 100644 src/Shared/SNS/SnsPublisher.cs create mode 100644 src/Shared/SQS/HandlerExtensions.cs create mode 100644 src/Shared/SQS/IMessageHandler.cs create mode 100644 src/Shared/SQS/MessageDispatcher.cs create mode 100644 src/Shared/SQS/SqsConsumerService.cs create mode 100644 tests/Baskets/Baskets.IntegrationTests/Baskets/AddBasketTests.cs create mode 100644 tests/Baskets/Baskets.IntegrationTests/Configuration/BasketApiFactory.cs create mode 100644 tests/Baskets/Baskets.IntegrationTests/Configuration/BasketsCollection.cs create mode 100644 tests/Baskets/Baskets.IntegrationTests/Configuration/UserTestHelper.cs delete mode 100644 tests/Shared/Auth/AuthConstants.cs create mode 100644 tests/Shared/TestConstants.cs diff --git a/IGroceryStore.sln b/IGroceryStore.sln index e32c534..1c532ea 100644 --- a/IGroceryStore.sln +++ b/IGroceryStore.sln @@ -49,12 +49,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Products.UnitTests", "tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shops.UnitTests", "tests\Shops\Shops.UnitTests\Shops.UnitTests.csproj", "{5F1ACEEE-21C2-4F25-AC1E-B7FE4C5BF2D9}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "requests", "requests", "{02241B0B-8C92-40C1-A3AC-F7AC27EDD788}" - ProjectSection(SolutionItems) = preProject - requests\brands.http = requests\brands.http - requests\users.http = requests\users.http - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Notifications", "Notifications", "{C3809CC7-0AF9-4D56-B1AB-EA0E098B0ECF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Notifications", "src\Notifications\Notifications\Notifications.csproj", "{7F1E1F0D-2006-42D0-8AC0-72E40FD709B6}" diff --git a/requests/brands.http b/requests/brands.http deleted file mode 100644 index ccd3f56..0000000 --- a/requests/brands.http +++ /dev/null @@ -1,6 +0,0 @@ -PUT http://localhost:5000/api/brands/1 -Content-Type: application/json -{ - "name" : "updated-name" -} - diff --git a/requests/users.http b/requests/users.http deleted file mode 100644 index 7250520..0000000 --- a/requests/users.http +++ /dev/null @@ -1,11 +0,0 @@ -POST http://localhost:5000/api/users/register -Content-Type: application/json - -{ - "email": "random@email.com", - "password": "randomPassword", - "confirmPassword": "randomPassword", - "firstName": "randomName", - "lastName": "randomLastName" -} - diff --git a/src/API/API.csproj b/src/API/API.csproj index b3c0e09..1aaef00 100644 --- a/src/API/API.csproj +++ b/src/API/API.csproj @@ -12,6 +12,7 @@ + diff --git a/src/API/Configuration/AuthenticationConfiguration.cs b/src/API/Configuration/AuthenticationConfiguration.cs index 40c9c73..0b1ef60 100644 --- a/src/API/Configuration/AuthenticationConfiguration.cs +++ b/src/API/Configuration/AuthenticationConfiguration.cs @@ -32,7 +32,6 @@ public static void ConfigureAuthentication(this WebApplicationBuilder builder) o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(o => { - //Is this validated automatically by asp.net core? o.Authority = options.ValidIssuer; o.Audience = options.ValidAudience; }); diff --git a/src/API/Configuration/AwsConfiguration.cs b/src/API/Configuration/AwsConfiguration.cs index 8b5e4f2..35fa6bc 100644 --- a/src/API/Configuration/AwsConfiguration.cs +++ b/src/API/Configuration/AwsConfiguration.cs @@ -1,4 +1,7 @@ +using Amazon; +using Amazon.SQS; using IGroceryStore.API.Initializers; +using IGroceryStore.Shared.SQS; namespace IGroceryStore.API.Configuration; @@ -10,5 +13,26 @@ public static void ConfigureSystemManager(this WebApplicationBuilder builder) { builder.Configuration.AddSystemsManager("/Production/IGroceryStore", TimeSpan.FromSeconds(30)); } + + builder.Services.AddHostedService(); + builder.Services.AddSingleton(_ => new AmazonSQSClient(RegionEndpoint.EUWest2)); + + builder.Services.AddSingleton(); + + builder.Services.AddMessageHandlers(); + + // var client = new AmazonSimpleNotificationServiceClient(RegionEndpoint.EUWest2); + // + // var publisher = new SnsPublisher(client); + // + // var customerUpdated = new CustomerUpdated + // { + // Id = 1, + // FullName = "Nick Chapsas", + // LifetimeSpent = 420 + // }; + // + // await publisher.PublishAsync("", + // customerUpdated); } } diff --git a/src/Shared/Contracts/IMessage.cs b/src/Shared/Contracts/IMessage.cs new file mode 100644 index 0000000..40be5b5 --- /dev/null +++ b/src/Shared/Contracts/IMessage.cs @@ -0,0 +1,6 @@ +namespace IGroceryStore.Shared.Contracts; + +public interface IMessage +{ + public string MessageTypeName { get; } +} diff --git a/src/Shared/Contracts/UserCreated.cs b/src/Shared/Contracts/UserCreated.cs index ba65ad4..2c8e1ec 100644 --- a/src/Shared/Contracts/UserCreated.cs +++ b/src/Shared/Contracts/UserCreated.cs @@ -1,3 +1,25 @@ +using System.Text.Json.Serialization; + namespace IGroceryStore.Shared.Contracts; -public record UserCreated(Guid UserId, string FirstName, string LastName); +public class UserCreated : IMessage +{ + [JsonPropertyName("id")] + public required Guid UserId { get; init; } + + [JsonPropertyName("firstName")] + public required string FirstName { get; init; } + + [JsonPropertyName("lastName")] + public required string LastName { get; init; } + + [Newtonsoft.Json.JsonIgnore] + public string MessageTypeName => nameof(UserCreated); + + public void Deconstruct(out Guid userId, out string firstName, out string lastName) + { + userId = this.UserId; + firstName = this.FirstName; + lastName = this.LastName; + } +} diff --git a/src/Shared/SNS/MessageExtensions.cs b/src/Shared/SNS/MessageExtensions.cs new file mode 100644 index 0000000..9c46cb1 --- /dev/null +++ b/src/Shared/SNS/MessageExtensions.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using Amazon.SimpleNotificationService.Model; +using IGroceryStore.Shared.Common; +using IGroceryStore.Shared.Contracts; + +namespace IGroceryStore.Shared.SNS; + +public static class MessageExtensions +{ + public static Dictionary ToMessageAttributeDictionary( + this T item) where T : IMessage + { + var document = JsonSerializer.SerializeToDocument(item); + var objectEnumerator = document.RootElement.EnumerateObject(); + var dictionary = new Dictionary(); + foreach (var jsonProperty in objectEnumerator) + { + dictionary.Add(jsonProperty.Name, new MessageAttributeValue + { + StringValue = jsonProperty.Value.ToString(), + DataType = jsonProperty.Value.ValueKind switch + { + JsonValueKind.Number => "Number", + JsonValueKind.String => "String", + _ => throw new NotSupportedException("This is a demo, sorry") + } + }); + } + return dictionary; + } +} diff --git a/src/Shared/SNS/SnsPublisher.cs b/src/Shared/SNS/SnsPublisher.cs new file mode 100644 index 0000000..35c20b4 --- /dev/null +++ b/src/Shared/SNS/SnsPublisher.cs @@ -0,0 +1,40 @@ +using System.Text.Json; +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using IGroceryStore.Shared.Common; +using IGroceryStore.Shared.Contracts; + +namespace IGroceryStore.Shared.SNS; + +public class SnsPublisher +{ + private readonly IAmazonSimpleNotificationService _sns; + + public SnsPublisher(IAmazonSimpleNotificationService sns) + { + _sns = sns; + } + + public async Task PublishAsync(string topicArn, TMessage message) + where TMessage : IMessage + { + var request = new PublishRequest + { + TopicArn = topicArn, + Message = JsonSerializer.Serialize(message) + }; + request.MessageAttributes.Add(nameof(IMessage.MessageTypeName), + new MessageAttributeValue + { + DataType = "String", + StringValue = message.MessageTypeName + }); + + foreach (var attribute in message.ToMessageAttributeDictionary()) + { + request.MessageAttributes.Add(attribute.Key, attribute.Value); + } + + await _sns.PublishAsync(request); + } +} diff --git a/src/Shared/SQS/HandlerExtensions.cs b/src/Shared/SQS/HandlerExtensions.cs new file mode 100644 index 0000000..0759038 --- /dev/null +++ b/src/Shared/SQS/HandlerExtensions.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace IGroceryStore.Shared.SQS; + +public static class HandlerExtensions +{ + public static IServiceCollection AddMessageHandlers(this IServiceCollection services) + { + var handlers = Assembly.GetExecutingAssembly().DefinedTypes + .Where(x => typeof(IMessageHandler).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); + + foreach (var handler in handlers) + { + var handlerType = handler.AsType(); + services.AddScoped(handlerType); + } + + return services; + } +} diff --git a/src/Shared/SQS/IMessageHandler.cs b/src/Shared/SQS/IMessageHandler.cs new file mode 100644 index 0000000..c2e31cb --- /dev/null +++ b/src/Shared/SQS/IMessageHandler.cs @@ -0,0 +1,9 @@ +using IGroceryStore.Shared.Contracts; + +namespace IGroceryStore.Shared.SQS; + +public interface IMessageHandler +{ + public Task HandleAsync(IMessage message); + public static abstract Type MessageType { get; } +} diff --git a/src/Shared/SQS/MessageDispatcher.cs b/src/Shared/SQS/MessageDispatcher.cs new file mode 100644 index 0000000..ded98ba --- /dev/null +++ b/src/Shared/SQS/MessageDispatcher.cs @@ -0,0 +1,45 @@ +using System.Reflection; +using IGroceryStore.Shared.Common; +using IGroceryStore.Shared.Contracts; +using Microsoft.Extensions.DependencyInjection; + +namespace IGroceryStore.Shared.SQS; + +public class MessageDispatcher +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly Dictionary _messageMappings; + private readonly Dictionary> _handlers; + + public MessageDispatcher(IServiceScopeFactory scopeFactory) + { + _scopeFactory = scopeFactory; + _messageMappings = typeof(IMessage).Assembly.DefinedTypes + .Where(x => typeof(IMessage).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract) + .ToDictionary(info => info.Name, info => info.AsType()); + + _handlers = Assembly.GetExecutingAssembly().DefinedTypes + .Where(x => typeof(IMessageHandler).IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract) + .ToDictionary>( + info => ((Type)info.GetProperty(nameof(IMessageHandler.MessageType))!.GetValue(null)!).Name, + info => provider => (IMessageHandler)provider.GetRequiredService(info.AsType())); + } + + public async Task DispatchAsync(TMessage message) + where TMessage : IMessage + { + using var scope = _scopeFactory.CreateScope(); + var handler = _handlers[message.MessageTypeName](scope.ServiceProvider); + await handler.HandleAsync(message); + } + + public bool CanHandleMessageType(string messageTypeName) + { + return _handlers.ContainsKey(messageTypeName); + } + + public Type? GetMessageTypeByName(string messageTypeName) + { + return _messageMappings.GetValueOrDefault(messageTypeName); + } +} diff --git a/src/Shared/SQS/SqsConsumerService.cs b/src/Shared/SQS/SqsConsumerService.cs new file mode 100644 index 0000000..469cce7 --- /dev/null +++ b/src/Shared/SQS/SqsConsumerService.cs @@ -0,0 +1,68 @@ +using System.Net; +using System.Text.Json; +using Amazon.SQS; +using Amazon.SQS.Model; +using IGroceryStore.Shared.Common; +using IGroceryStore.Shared.Contracts; +using Microsoft.Extensions.Hosting; + +namespace IGroceryStore.Shared.SQS; + +public class SqsConsumerService : BackgroundService +{ + private readonly IAmazonSQS _sqs; + private readonly MessageDispatcher _dispatcher; + private readonly string _queueName = Environment.GetEnvironmentVariable("QUEUE_NAME")!; + private readonly List _messageAttributeNames = new() { "All" }; + + public SqsConsumerService(IAmazonSQS sqs, MessageDispatcher dispatcher) + { + _sqs = sqs; + _dispatcher = dispatcher; + } + + protected override async Task ExecuteAsync(CancellationToken ct) + { + var queueUrl = await _sqs.GetQueueUrlAsync(_queueName, ct); + var receiveRequest = new ReceiveMessageRequest + { + QueueUrl = queueUrl.QueueUrl, + MessageAttributeNames = _messageAttributeNames, + AttributeNames = _messageAttributeNames + }; + + while (!ct.IsCancellationRequested) + { + var messageResponse = await _sqs.ReceiveMessageAsync(receiveRequest, ct); + if (messageResponse.HttpStatusCode != HttpStatusCode.OK) + { + //Do some logging or handling? + continue; + } + + foreach (var message in messageResponse.Messages) + { + var messageTypeName = message.MessageAttributes + .GetValueOrDefault(nameof(IMessage.MessageTypeName))?.StringValue; + + if (messageTypeName is null) + { + //Normally send to DLQ + await _sqs.DeleteMessageAsync(queueUrl.QueueUrl, message.ReceiptHandle, ct); + continue; + } + + if (!_dispatcher.CanHandleMessageType(messageTypeName)) + { + continue; + } + + var messageType = _dispatcher.GetMessageTypeByName(messageTypeName)!; + var messageAsType = (IMessage)JsonSerializer.Deserialize(message.Body, messageType)!; + + await _dispatcher.DispatchAsync(messageAsType); + await _sqs.DeleteMessageAsync(queueUrl.QueueUrl, message.ReceiptHandle, ct); + } + } + } +} diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index 9a12f55..7cb178f 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -14,6 +14,8 @@ + + diff --git a/tests/Baskets/Baskets.IntegrationTests/Baskets/AddBasketTests.cs b/tests/Baskets/Baskets.IntegrationTests/Baskets/AddBasketTests.cs new file mode 100644 index 0000000..a0d55eb --- /dev/null +++ b/tests/Baskets/Baskets.IntegrationTests/Baskets/AddBasketTests.cs @@ -0,0 +1,34 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json; +using IGroceryStore.Baskets.Features.Baskets; +using IGroceryStore.Baskets.IntegrationTests.Configuration; +using IGroceryStore.Shared.Tests; + +namespace IGroceryStore.Baskets.IntegrationTests.Baskets; + +[Collection(TestConstants.Collections.Baskets)] +public class AddBasketTests : BasketApiFactory +{ + [Fact] + public async Task AddBasket_Returns200OK_WhenValidRequestReceived() + { + // Arrange + var commandBody = new AddBasket.AddBasketBody("Test Basket"); + + var requestContent = new StringContent( + JsonSerializer.Serialize(commandBody), + Encoding.UTF8, + "application/json"); + + // Act + var response = await HttpClient.PostAsJsonAsync("api/users/register", commandBody); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var responseContent = await response.Content.ReadAsStringAsync(); + var responseObject = JsonSerializer.Deserialize(responseContent); + Assert.NotEqual(Guid.Empty, responseObject); + } +} diff --git a/tests/Baskets/Baskets.IntegrationTests/Configuration/BasketApiFactory.cs b/tests/Baskets/Baskets.IntegrationTests/Configuration/BasketApiFactory.cs new file mode 100644 index 0000000..c5bab66 --- /dev/null +++ b/tests/Baskets/Baskets.IntegrationTests/Configuration/BasketApiFactory.cs @@ -0,0 +1 @@ +using System.Security.Claims; using Bogus; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Configurations; using DotNet.Testcontainers.Containers; using IGroceryStore.API; using IGroceryStore.API.Initializers; using IGroceryStore.Shared.Contracts; using IGroceryStore.Shared.Tests.Auth; using MassTransit; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; namespace IGroceryStore.Baskets.IntegrationTests.Configuration; public class BasketApiFactory : WebApplicationFactory, IAsyncLifetime { private readonly MockUser _user; private readonly TestcontainerDatabase _mongoContainer = new TestcontainersBuilder() .WithDatabase(new MongoDbTestcontainerConfiguration { Database = Guid.NewGuid().ToString(), Username = "admin", Password = "admin", }) .WithAutoRemove(true) .WithCleanUp(true) .Build(); private readonly IDockerContainer _eventStoreContainer = new TestcontainersBuilder() .WithImage("ghcr.io/eventstore/eventstore:21.10.0-alpha-arm64v8") .WithWaitStrategy(Wait.ForUnixContainer()) .WithAutoRemove(true) .WithCleanUp(true) .Build(); protected BasketApiFactory() { Randomizer.Seed = new Random(420); VerifierSettings.ScrubInlineGuids(); _user = new MockUser( new Claim(Shared.Constants.Claims.Name.UserId, "1"), new Claim(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddSeconds(2137).ToUnixTimeSeconds().ToString())); } public HttpClient HttpClient { get; private set; } = default!; protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.ConfigureLogging(logging => { logging.ClearProviders(); }); builder.UseEnvironment(EnvironmentService.TestEnvironment); builder.ConfigureServices(services => { services.AddMassTransitTestHarness(x => { x.AddHandler(context => context.ConsumeCompleted); }); }); builder.ConfigureTestServices(services => { services.AddTestAuthentication(); services.AddSingleton(_ => _user); }); } public async Task InitializeAsync() { await _mongoContainer.StartAsync(); await _eventStoreContainer.StartAsync(); HttpClient = CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); } async Task IAsyncLifetime.DisposeAsync() { await _eventStoreContainer.DisposeAsync(); await _mongoContainer.DisposeAsync(); } } \ No newline at end of file diff --git a/tests/Baskets/Baskets.IntegrationTests/Configuration/BasketsCollection.cs b/tests/Baskets/Baskets.IntegrationTests/Configuration/BasketsCollection.cs new file mode 100644 index 0000000..63c7ef6 --- /dev/null +++ b/tests/Baskets/Baskets.IntegrationTests/Configuration/BasketsCollection.cs @@ -0,0 +1,8 @@ +using IGroceryStore.Shared.Tests; + +namespace IGroceryStore.Baskets.IntegrationTests.Configuration; + +[CollectionDefinition(TestConstants.Collections.Baskets)] +public class BasketsCollection : ICollectionFixture +{ +} diff --git a/tests/Baskets/Baskets.IntegrationTests/Configuration/UserTestHelper.cs b/tests/Baskets/Baskets.IntegrationTests/Configuration/UserTestHelper.cs new file mode 100644 index 0000000..8227d40 --- /dev/null +++ b/tests/Baskets/Baskets.IntegrationTests/Configuration/UserTestHelper.cs @@ -0,0 +1,20 @@ +using IGroceryStore.API; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; + +namespace IGroceryStore.Baskets.IntegrationTests.Configuration; + +// public static class UserTestHelper +// { +// internal static async Task RemoveUserById(this WebApplicationFactory apiFactory, Guid id) where T: class, IApiMarker +// { +// using var scope = apiFactory.Services.CreateScope(); +// var collection = scope.ServiceProvider.GetRequiredService>(); +// +// var user = await collection.Find(x => x.Id == id).FirstOrDefaultAsync(); +// +// user.Should().NotBeNull(); +// if (user is not null) await collection.DeleteOneAsync(x => x.Id == id); +// } +// } diff --git a/tests/Shared/Auth/AuthConstants.cs b/tests/Shared/Auth/AuthConstants.cs deleted file mode 100644 index 83b6eb6..0000000 --- a/tests/Shared/Auth/AuthConstants.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace IGroceryStore.Shared.Tests.Auth -{ - public static class AuthConstants - { - public const string Scheme = "TestAuth"; - } -} diff --git a/tests/Shared/Auth/AuthServiceCollectionExtensions.cs b/tests/Shared/Auth/AuthServiceCollectionExtensions.cs index 9853d5e..a8b523a 100644 --- a/tests/Shared/Auth/AuthServiceCollectionExtensions.cs +++ b/tests/Shared/Auth/AuthServiceCollectionExtensions.cs @@ -11,13 +11,13 @@ public static IServiceCollection AddTestAuthentication(this IServiceCollection s { services.AddAuthorization(options => { - options.DefaultPolicy = new AuthorizationPolicyBuilder(AuthConstants.Scheme) + options.DefaultPolicy = new AuthorizationPolicyBuilder(TestConstants.Auth.Scheme) .RequireAuthenticatedUser() .Build(); }); - services.AddAuthentication(AuthConstants.Scheme) - .AddScheme(AuthConstants.Scheme, options => { }); + services.AddAuthentication(TestConstants.Auth.Scheme) + .AddScheme(TestConstants.Auth.Scheme, options => { }); return services; } diff --git a/tests/Shared/Auth/MockUser.cs b/tests/Shared/Auth/MockUser.cs index 4994ed8..f51ccad 100644 --- a/tests/Shared/Auth/MockUser.cs +++ b/tests/Shared/Auth/MockUser.cs @@ -1,15 +1,14 @@ using System.Security.Claims; -namespace IGroceryStore.Shared.Tests.Auth +namespace IGroceryStore.Shared.Tests.Auth; + +public interface IMockUser +{ + List Claims { get; } +} + +public class MockUser : IMockUser { - public interface IMockUser - { - List Claims { get; } - } - public class MockUser : IMockUser - { - public List Claims { get; private set; } = new(); - public MockUser(params Claim[] claims) - => Claims = claims.ToList(); - } + public List Claims { get; } + public MockUser(params Claim[] claims) => Claims = claims.ToList(); } diff --git a/tests/Shared/Auth/TestAuthHandler.cs b/tests/Shared/Auth/TestAuthHandler.cs index 4cacaf5..b73af61 100644 --- a/tests/Shared/Auth/TestAuthHandler.cs +++ b/tests/Shared/Auth/TestAuthHandler.cs @@ -26,9 +26,9 @@ protected override Task HandleAuthenticateAsync() if (_mockAuthUser.Claims.Count == 0) return Task.FromResult(AuthenticateResult.Fail("Mock auth user not configured.")); - var identity = new ClaimsIdentity(_mockAuthUser.Claims, AuthConstants.Scheme); + var identity = new ClaimsIdentity(_mockAuthUser.Claims, TestConstants.Auth.Scheme); var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, AuthConstants.Scheme); + var ticket = new AuthenticationTicket(principal, TestConstants.Auth.Scheme); var result = AuthenticateResult.Success(ticket); return Task.FromResult(result); diff --git a/tests/Shared/Shared.csproj b/tests/Shared/Shared.csproj index b988457..484f236 100644 --- a/tests/Shared/Shared.csproj +++ b/tests/Shared/Shared.csproj @@ -16,7 +16,6 @@ - diff --git a/tests/Shared/TestConstants.cs b/tests/Shared/TestConstants.cs new file mode 100644 index 0000000..6db6b92 --- /dev/null +++ b/tests/Shared/TestConstants.cs @@ -0,0 +1,16 @@ +namespace IGroceryStore.Shared.Tests; + +public static class TestConstants +{ + public static class Auth + { + public const string Scheme = "TestAuth"; + } + + public static class Collections + { + public const string Products = "Products"; + public const string Baskets = "Baskets"; + public const string Shops = "Shops"; + } +} diff --git a/tools/docker/docker-compose.yml b/tools/docker/docker-compose.yml index 62b6c46..8885626 100644 --- a/tools/docker/docker-compose.yml +++ b/tools/docker/docker-compose.yml @@ -24,7 +24,7 @@ services: postgres: image: postgres:latest container_name: postgres - profiles: [ "all", "webapi", "users", "products" ] + profiles: [ "all", "webapi", "products" ] volumes: - ../volumes/postgres:/var/lib/postgresql/data environment: @@ -74,7 +74,7 @@ services: ports: - 8000:8000 volumes: - - ./docker/dynamodb:/home/dynamodblocal/data + - ../volumes/dynamodb:/home/dynamodblocal/data working_dir: /home/dynamodblocal # observability From c3e5971619d678f46c5416975eec82c803dff00c Mon Sep 17 00:00:00 2001 From: Adrian Franczak <44712992+Nairda015@users.noreply.github.com> Date: Tue, 10 Jan 2023 00:39:20 +0100 Subject: [PATCH 4/4] remove OneOf --- src/Shared/Shared.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index 7cb178f..c20c174 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -30,7 +30,6 @@ -