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 @@
-