diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index de173fa..7b639eb 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -194,6 +194,9 @@ jobs: environment: dev permissions: contents: read + outputs: + CONFIG_VERSION: ${{ steps.plan-upload.outputs.configuration_version_id }} + steps: - name: Checkout uses: actions/checkout@v4 @@ -215,8 +218,9 @@ jobs: db_user = "${{ secrets.BMB_MYSQL_USER }}" db_pwd = "${{ secrets.BMB_MYSQL_PASSWORD }}" rds_cluster_identifier = "${{ vars.BMB_MYSQL_CLUSTER }}" - access_key_id = "${{ secrets.AWS_ACCESS_KEY_ID }}" - secret_access_key = "${{ secrets.AWS_SECRET_ACCESS_KEY }}" + api_access_key_id = "${{ secrets.AWS_API_ACCESS_KEY_ID }}" + api_secret_access_key = "${{ secrets.AWS_API_SECRET_ACCESS_KEY }}" + user_pool_name = "${{ vars.BMB_USER_POOL_NAME }}" EOF - name: Upload Configuration @@ -239,4 +243,18 @@ jobs: id: apply with: run: ${{ steps.apply-run.outputs.run_id }} - comment: "Confirmed from GitHub Actions CI ${{ github.sha }}" \ No newline at end of file + comment: "Confirmed from GitHub Actions CI ${{ github.sha }}" + + destroy-plan: + name: "Create terraform destroy plan" + needs: [create-app] + runs-on: ubuntu-latest + + steps: + - name: Create plan + uses: hashicorp/tfc-workflows-github/actions/create-run@v1.3.1 + id: destroy-plan + with: + workspace: ${{ env.TF_WORKSPACE }} + configuration_version: ${{ NEEDS.create-app.outputs.CONFIG_VERSION }} + is_destroy: true \ No newline at end of file diff --git a/.github/workflows/terraform-plan.yaml b/.github/workflows/terraform-plan.yaml index ad65408..73e7fde 100644 --- a/.github/workflows/terraform-plan.yaml +++ b/.github/workflows/terraform-plan.yaml @@ -36,8 +36,9 @@ jobs: db_user = "${{ secrets.BMB_MYSQL_USER }}" db_pwd = "${{ secrets.BMB_MYSQL_PASSWORD }}" rds_cluster_identifier = "${{ vars.BMB_MYSQL_CLUSTER }}" - access_key_id = "${{ secrets.AWS_ACCESS_KEY_ID }}" - secret_access_key = "${{ secrets.AWS_SECRET_ACCESS_KEY }}" + api_access_key_id = "${{ secrets.AWS_API_ACCESS_KEY_ID }}" + api_secret_access_key = "${{ secrets.AWS_API_SECRET_ACCESS_KEY }}" + user_pool_name = "${{ vars.BMB_USER_POOL_NAME }}" EOF - name: Upload Configuration diff --git a/FIAP.TechChallenge-P3.png b/FIAP.TechChallenge-P3.png new file mode 100644 index 0000000..20f736d Binary files /dev/null and b/FIAP.TechChallenge-P3.png differ diff --git a/FIAP.TechChallenge.ByteMeBurger.sln b/FIAP.TechChallenge.ByteMeBurger.sln index b496a28..7b2ff80 100644 --- a/FIAP.TechChallenge.ByteMeBurger.sln +++ b/FIAP.TechChallenge.ByteMeBurger.sln @@ -89,6 +89,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "terraform", "terraform", "{ tf\variables.tf = tf\variables.tf EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway", "src\FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway\FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj", "{36FD200F-16AB-44A6-A103-E5EABD6EA263}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test", "tests\FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test\FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test.csproj", "{46B22CD7-BB91-4E80-870F-28DA476ED6A3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -118,6 +122,8 @@ Global {9E8287AF-C7A6-46A6-96D8-8460642ADE4B} = {5EC029C6-230A-4782-9B62-CB475C7D05F8} {93545102-0476-48CB-8074-5C71AC81E040} = {C244E1D6-E1EB-4314-9F39-BB59FA1F7C71} {B2898AA3-86A6-4AF1-B27F-A3706B2B0557} = {2F192BA8-59FC-4B0C-B59D-511EFA89F428} + {36FD200F-16AB-44A6-A103-E5EABD6EA263} = {5EC029C6-230A-4782-9B62-CB475C7D05F8} + {46B22CD7-BB91-4E80-870F-28DA476ED6A3} = {C244E1D6-E1EB-4314-9F39-BB59FA1F7C71} EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B8F244B5-F703-441C-8A8A-C720F605709C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -188,5 +194,13 @@ Global {93545102-0476-48CB-8074-5C71AC81E040}.Debug|Any CPU.Build.0 = Debug|Any CPU {93545102-0476-48CB-8074-5C71AC81E040}.Release|Any CPU.ActiveCfg = Release|Any CPU {93545102-0476-48CB-8074-5C71AC81E040}.Release|Any CPU.Build.0 = Release|Any CPU + {36FD200F-16AB-44A6-A103-E5EABD6EA263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36FD200F-16AB-44A6-A103-E5EABD6EA263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36FD200F-16AB-44A6-A103-E5EABD6EA263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36FD200F-16AB-44A6-A103-E5EABD6EA263}.Release|Any CPU.Build.0 = Release|Any CPU + {46B22CD7-BB91-4E80-870F-28DA476ED6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {46B22CD7-BB91-4E80-870F-28DA476ED6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46B22CD7-BB91-4E80-870F-28DA476ED6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {46B22CD7-BB91-4E80-870F-28DA476ED6A3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Api/Auth/BmbRoles.cs b/src/FIAP.TechChallenge.ByteMeBurger.Api/Auth/BmbRoles.cs new file mode 100644 index 0000000..b26e8c0 --- /dev/null +++ b/src/FIAP.TechChallenge.ByteMeBurger.Api/Auth/BmbRoles.cs @@ -0,0 +1,8 @@ +namespace FIAP.TechChallenge.ByteMeBurger.Api.Auth; + +public static class BmbRoles +{ + public const string Admin = "admin"; + public const string Kitchen = "kitchen"; + public const string Customer = "customer"; +} diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/CustomersController.cs b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/CustomersController.cs index 0037b08..0ef16c4 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/CustomersController.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/CustomersController.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using FIAP.TechChallenge.ByteMeBurger.Api.Auth; using FIAP.TechChallenge.ByteMeBurger.Api.Model.Customers; using FIAP.TechChallenge.ByteMeBurger.Controllers.Contracts; using FIAP.TechChallenge.ByteMeBurger.Controllers.Dto; @@ -12,7 +13,6 @@ namespace FIAP.TechChallenge.ByteMeBurger.Api.Controllers; /// /// Customer service (port implementation). /// Logger -[Obsolete("To be migrated to different project")] [Route("api/[controller]")] [Produces("application/json")] [Consumes("application/json")] @@ -28,7 +28,7 @@ public class CustomersController(ICustomerService customerService, ILoggerCancellation token /// Customer [HttpGet] - [Authorize(Roles = "admin")] + [Authorize(Roles = BmbRoles.Admin)] public async Task> Get([FromQuery] [MaxLength(14)] string cpf, CancellationToken cancellationToken) { @@ -51,6 +51,7 @@ public async Task> Get([FromQuery] [MaxLength(14)] str /// Cancellation token /// Customer [HttpPost] + [Authorize(Roles = BmbRoles.Customer)] public async Task> Post([FromBody] CreateCustomerRequest createCustomerRequest, CancellationToken cancellationToken) { diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/OrdersController.cs b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/OrdersController.cs index 3130cbf..cd1e84f 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/OrdersController.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/OrdersController.cs @@ -52,6 +52,7 @@ public async Task> Post( /// Cancellation token /// Orders list [HttpGet] + [Authorize(Roles = $"{BmbRoles.Admin},{BmbRoles.Kitchen}")] public async Task>> Get(bool listAll, CancellationToken cancellationToken) { @@ -71,6 +72,7 @@ public async Task>> Get(bool l /// Cancellation token. /// Order details [HttpGet("{id:guid}")] + [Authorize(Roles = $"{BmbRoles.Admin},{BmbRoles.Kitchen}")] public async Task> Get(Guid id, CancellationToken cancellationToken) { logger.LogInformation("Getting order with ID: {OrderId}", id); @@ -98,6 +100,7 @@ public async Task> Get(Guid id, CancellationToken c /// Cancellation token [Route("{id:guid}/status")] [HttpPatch] + [Authorize(Roles = $"{BmbRoles.Admin}")] public async Task> Patch(Guid id, [FromBody] UpdateOrderStatusRequest command, CancellationToken cancellationToken) diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/PaymentsController.cs b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/PaymentsController.cs index 6b595cd..260a98e 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/PaymentsController.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/PaymentsController.cs @@ -1,3 +1,4 @@ +using FIAP.TechChallenge.ByteMeBurger.Api.Auth; using FIAP.TechChallenge.ByteMeBurger.Api.Model.Payment; using FIAP.TechChallenge.ByteMeBurger.Controllers.Contracts; using FIAP.TechChallenge.ByteMeBurger.Controllers.Dto; @@ -14,7 +15,7 @@ namespace FIAP.TechChallenge.ByteMeBurger.Api.Controllers; [ApiConventionType(typeof(DefaultApiConventions))] [Produces("application/json")] [Consumes("application/json")] -[Authorize] +[Authorize(Roles = BmbRoles.Admin)] public class PaymentsController : ControllerBase { private readonly IPaymentService _paymentService; diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/ProductsController.cs b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/ProductsController.cs index d395724..be54f31 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/ProductsController.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Api/Controllers/ProductsController.cs @@ -1,3 +1,4 @@ +using FIAP.TechChallenge.ByteMeBurger.Api.Auth; using FIAP.TechChallenge.ByteMeBurger.Api.Model.Products; using FIAP.TechChallenge.ByteMeBurger.Controllers.Contracts; using FIAP.TechChallenge.ByteMeBurger.Controllers.Dto; @@ -17,7 +18,7 @@ namespace FIAP.TechChallenge.ByteMeBurger.Api.Controllers; [ApiConventionType(typeof(DefaultApiConventions))] [Produces("application/json")] [Consumes("application/json")] -[Authorize] +[Authorize(Roles = BmbRoles.Admin)] public class ProductsController(IProductService productService, ILogger logger) : ControllerBase { diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Api/DomainEventsHandler.cs b/src/FIAP.TechChallenge.ByteMeBurger.Api/DomainEventsHandler.cs index 25cb56f..9d1913f 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Api/DomainEventsHandler.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Api/DomainEventsHandler.cs @@ -40,7 +40,7 @@ public DomainEventsHandler(ILogger logger, HybridCache cach private void OnCustomerRegistered(object? sender, CustomerRegistered e) { _logger.LogInformation("New Customer registered: {@Customer}", e.Payload); - _logger.LogInformation("Sending email to customer: {CustomerName}", e.Payload.Name); + _publisher.PublishAsync(e).ConfigureAwait(false); } private void OnOrderStatusChanged(object? sender, OrderStatusChanged e) @@ -78,6 +78,7 @@ private void OnProductDeleted(object? sender, ProductDeleted e) private void OnProductCreated(object? sender, ProductCreated e) { _logger.LogInformation("Product created: {@Product}", e.Payload); + _publisher.PublishAsync(e).ConfigureAwait(false); } private void OnPaymentCreated(object? sender, PaymentCreated e) diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Api/appsettings.Development.json b/src/FIAP.TechChallenge.ByteMeBurger.Api/appsettings.Development.json index e726935..8e77f07 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Api/appsettings.Development.json +++ b/src/FIAP.TechChallenge.ByteMeBurger.Api/appsettings.Development.json @@ -32,6 +32,14 @@ "ClientSecret": "", "ClientId": "" }, + "CognitoSettings": { + "UserPoolId": "", + "UserPoolClientId": "", + "Enabled": true, + "Region": "us-east-1", + "ClientSecret": "", + "ClientId": "" + }, "JwtOptions": { "Issuer": "https://localhost:7000", "Audience": "https://localhost:7000", diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoClientFactory.cs b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoClientFactory.cs new file mode 100644 index 0000000..b7ec35d --- /dev/null +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoClientFactory.cs @@ -0,0 +1,17 @@ +using Amazon; +using Amazon.CognitoIdentityProvider; +using Amazon.Runtime; +using FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Factory; +using Microsoft.Extensions.Options; + +namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway; + +public class CognitoClientFactory(IOptions settings) : ICognitoClientFactory +{ + public IAmazonCognitoIdentityProvider CreateClient() + { + return new AmazonCognitoIdentityProviderClient( + new BasicAWSCredentials(settings.Value.ClientId, settings.Value.ClientSecret), + RegionEndpoint.GetBySystemName(settings.Value.Region)); + } +} diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoSettings.cs b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoSettings.cs new file mode 100644 index 0000000..c33c08e --- /dev/null +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoSettings.cs @@ -0,0 +1,38 @@ +namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway; + +/// +/// Cognito User Pool settings +/// +public class CognitoSettings +{ + /// + /// User Pool Id + /// + public string UserPoolId { get; set; } = string.Empty; + + + /// + /// Client Id + /// + public string UserPoolClientId { get; set; } = string.Empty; + + /// + /// Enabled + /// + public bool Enabled { get; set; } = false; + + /// + /// AWS Region + /// + public string Region { get; set; } = string.Empty; + + /// + /// AWS Secret Id + /// + public string ClientSecret { get; set; } = string.Empty; + + /// + /// AWS Client Id + /// + public string ClientId { get; set; } = string.Empty; +} diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoUserManager.cs b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoUserManager.cs new file mode 100644 index 0000000..cf96717 --- /dev/null +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/CognitoUserManager.cs @@ -0,0 +1,122 @@ +using System.Security.Cryptography; +using FIAP.TechChallenge.ByteMeBurger.Domain.Entities; +using FIAP.TechChallenge.ByteMeBurger.Domain.Interfaces; +using Amazon.CognitoIdentityProvider; +using Amazon.CognitoIdentityProvider.Model; +using FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Factory; +using FIAP.TechChallenge.ByteMeBurger.Domain.Base; +using Microsoft.Extensions.Options; + +namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway; + +public class CognitoUserManager : ICustomerRepository +{ + private readonly IAmazonCognitoIdentityProvider _cognitoClient; + private readonly string _userPoolId; + private readonly string _clientId; + + public CognitoUserManager(ICognitoClientFactory cognitoClientFactory, IOptions settings) + { + _cognitoClient = cognitoClientFactory.CreateClient(); + _userPoolId = settings.Value.UserPoolId; + _clientId = settings.Value.UserPoolClientId; + } + + public async Task FindByCpfAsync(string cpf) + { + try + { + var response = await _cognitoClient.AdminGetUserAsync(new AdminGetUserRequest + { + UserPoolId = _userPoolId, + Username = cpf + }); + + var email = response.UserAttributes.First(attr => attr.Name == "email").Value; + var name = response.UserAttributes.First(attr => attr.Name == "name").Value; + var sub = response.UserAttributes.First(attr => attr.Name == "sub").Value; + var customer = new Customer(Guid.Parse(sub), cpf, name, email); + + return customer; + } + catch (UserNotFoundException) + { + return null; + } + catch (Exception ex) + { + Console.WriteLine($"Error fetching user: {ex.Message}"); + throw; + } + } + + public async Task CreateAsync(Customer customer) + { + try + { + var signUpResponse = await _cognitoClient.AdminCreateUserAsync(new AdminCreateUserRequest() + { + Username = customer.Cpf, + UserPoolId = _userPoolId, + UserAttributes = + { + new AttributeType { Name = "email", Value = customer.Email }, + new AttributeType { Name = "name", Value = customer.Name } + } + }); + + customer.Id = Guid.Parse(signUpResponse.User.Attributes.First(a=>a.Name is "sub").Value); + return customer; + } + catch (UsernameExistsException ex) + { + Console.WriteLine($"Error registering user: {ex.Message}"); + throw new DomainException("There's already a customer using the provided CPF value."); + } + catch (Exception ex) + { + Console.WriteLine($"Error registering user: {ex.Message}"); + throw; + } + } + + private static string GenerateRandomPassword(int length) + { + using var rng = RandomNumberGenerator.Create(); + var characterSets = new[] + { + "abcdefghijklmnopqrstuvwxyz", + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "1234567890", + "!@#$%^&*()" + }; + var allChars = string.Concat(characterSets); + var passwordChars = new char[length]; + // Ensure the password contains at least one character from each character set + for (int i = 0; i < characterSets.Length && i < length; i++) + { + passwordChars[i] = GetRandomChar(characterSets[i], rng); + } + // Fill the rest of the password with random characters + for (int i = characterSets.Length; i < length; i++) + { + passwordChars[i] = GetRandomChar(allChars, rng); + } + // Shuffle the password to ensure randomness + passwordChars = passwordChars.OrderBy(_ => GetRandomInt(rng, int.MaxValue)).ToArray(); + return new string(passwordChars); + } + private static char GetRandomChar(string chars, RandomNumberGenerator rng) + { + var bytes = new byte[4]; + rng.GetBytes(bytes); + var index = BitConverter.ToUInt32(bytes, 0) % chars.Length; + return chars[(int)index]; + } + private static int GetRandomInt(RandomNumberGenerator rng, int max) + { + var bytes = new byte[4]; + rng.GetBytes(bytes); + return (int)(BitConverter.ToUInt32(bytes, 0) % max); + } +} diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj new file mode 100644 index 0000000..6c97a87 --- /dev/null +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/Factory/ICognitoClientFactory.cs b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/Factory/ICognitoClientFactory.cs new file mode 100644 index 0000000..50a51b0 --- /dev/null +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/Factory/ICognitoClientFactory.cs @@ -0,0 +1,8 @@ +using Amazon.CognitoIdentityProvider; + +namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Factory; + +public interface ICognitoClientFactory +{ + IAmazonCognitoIdentityProvider CreateClient(); +} diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/ServiceExtensions.cs b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/ServiceExtensions.cs new file mode 100644 index 0000000..c275525 --- /dev/null +++ b/src/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway/ServiceExtensions.cs @@ -0,0 +1,30 @@ +using System.Diagnostics.CodeAnalysis; +using FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Factory; +using FIAP.TechChallenge.ByteMeBurger.Domain.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway; + +[ExcludeFromCodeCoverage] +public class CognitoSettingsSetup(IConfiguration configuration) : IConfigureOptions +{ + public void Configure(CognitoSettings options) + { + configuration + .GetSection(nameof(CognitoSettings)) + .Bind(options); + } +} + +[ExcludeFromCodeCoverage] +public static class ServiceExtensions +{ + public static void ConfigureCognito(this IServiceCollection services) + { + services.ConfigureOptions(); + services.AddSingleton(); + services.AddScoped(); + } +} diff --git a/src/FIAP.TechChallenge.ByteMeBurger.DI/FIAP.TechChallenge.ByteMeBurger.DI.csproj b/src/FIAP.TechChallenge.ByteMeBurger.DI/FIAP.TechChallenge.ByteMeBurger.DI.csproj index 246e253..24447ab 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.DI/FIAP.TechChallenge.ByteMeBurger.DI.csproj +++ b/src/FIAP.TechChallenge.ByteMeBurger.DI/FIAP.TechChallenge.ByteMeBurger.DI.csproj @@ -13,6 +13,7 @@ + diff --git a/src/FIAP.TechChallenge.ByteMeBurger.DI/ServiceCollectionsExtensions.cs b/src/FIAP.TechChallenge.ByteMeBurger.DI/ServiceCollectionsExtensions.cs index 8ea190c..58aacaa 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.DI/ServiceCollectionsExtensions.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.DI/ServiceCollectionsExtensions.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using FIAP.TechChallenge.ByteMeBurger.Application; +using FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway; using FIAP.TechChallenge.ByteMeBurger.Controllers; using FIAP.TechChallenge.ByteMeBurger.Domain.Interfaces; using FIAP.TechChallenge.ByteMeBurger.FakePayment.Gateway; @@ -17,6 +18,7 @@ public static class ServiceCollectionsExtensions { public static void IoCSetup(this IServiceCollection serviceCollection, IConfiguration configuration) { + serviceCollection.ConfigureCognito(); serviceCollection.ConfigurePersistenceApp(configuration); ConfigurePaymentGateway(serviceCollection); ConfigHybridCache(serviceCollection, configuration); diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/ServiceExtensions.cs b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/ServiceExtensions.cs index c0cd5f6..8aa53db 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Persistence/ServiceExtensions.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Persistence/ServiceExtensions.cs @@ -16,10 +16,9 @@ public static void ConfigurePersistenceApp(this IServiceCollection services, ICo { if (string.IsNullOrWhiteSpace(configuration.GetConnectionString("MySql"))) { - services.AddScoped() - .AddScoped(_ => new InMemoryCustomerRepository([])) - .AddScoped(_ => new InMemoryProductRepository([])) - .AddScoped(); + services.AddSingleton() + .AddSingleton(_ => new InMemoryProductRepository([])) + .AddSingleton(); } else { @@ -34,7 +33,6 @@ public static void ConfigurePersistenceApp(this IServiceCollection services, ICo }); services.AddScoped() - .AddScoped() .AddScoped() .AddScoped(); } diff --git a/src/FIAP.TechChallenge.ByteMeBurger.Publisher.Sqs/SqsService.cs b/src/FIAP.TechChallenge.ByteMeBurger.Publisher.Sqs/SqsService.cs index fc0e226..4773844 100644 --- a/src/FIAP.TechChallenge.ByteMeBurger.Publisher.Sqs/SqsService.cs +++ b/src/FIAP.TechChallenge.ByteMeBurger.Publisher.Sqs/SqsService.cs @@ -19,6 +19,16 @@ public async Task PublishAsync(DomainEvent @event) var queueUrl = await client.GetQueueUrlAsync(sqsSettingsOptions.Value.QueueName); var request = new SendMessageRequest { + MessageAttributes = new Dictionary + { + { + "EventType", new MessageAttributeValue + { + DataType = "String", + StringValue = @event.GetType().Name + } + } + }, MessageBody = JsonSerializer.Serialize(@event.Payload), QueueUrl = queueUrl.QueueUrl }; diff --git a/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoClientFactoryTests.cs b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoClientFactoryTests.cs new file mode 100644 index 0000000..b123b62 --- /dev/null +++ b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoClientFactoryTests.cs @@ -0,0 +1,32 @@ +using Amazon.CognitoIdentityProvider; +using FluentAssertions; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; +using Moq; + +namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test; + +[TestSubject(typeof(CognitoClientFactory))] +public class CognitoClientFactoryTests +{ + [Fact] + public void CreateClient_WithValidSettings_ShouldReturnAmazonCognitoIdentityProviderClient () + { + // Arrange + var settingsMock = new Mock>(); + settingsMock.Setup(s => s.Value).Returns(new CognitoSettings + { + ClientId = "test-client-id", + ClientSecret = "test-client-secret", + Region = "us-west-2" + }); + + var factory = new CognitoClientFactory(settingsMock.Object); + + // Act + var client = factory.CreateClient(); + + // Assert + client.Should().NotBeNull().And.BeOfType(); + } +} diff --git a/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoUserManagerTest.cs b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoUserManagerTest.cs new file mode 100644 index 0000000..e8a451d --- /dev/null +++ b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/CognitoUserManagerTest.cs @@ -0,0 +1,108 @@ +using Amazon.CognitoIdentityProvider; +using Amazon.CognitoIdentityProvider.Model; +using FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Factory; +using FIAP.TechChallenge.ByteMeBurger.Domain.Entities; +using FluentAssertions; +using FluentAssertions.Execution; +using JetBrains.Annotations; +using Microsoft.Extensions.Options; +using Moq; + +namespace FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test; + +[TestSubject(typeof(CognitoUserManager))] +public class CognitoUserManagerTest +{ + private readonly Mock _cognitoClientMock; + private readonly CognitoUserManager _userManager; + + public CognitoUserManagerTest() + { + // Arrange + var mockFactory = new Mock(); + _cognitoClientMock = new Mock(); + var settings = Options.Create(new CognitoSettings + { UserPoolId = "testPoolId", UserPoolClientId = "testClientId" }); + + mockFactory.Setup(f => f.CreateClient()).Returns(_cognitoClientMock.Object); + + _userManager = new CognitoUserManager(mockFactory.Object, settings); + } + + [Fact] + public async Task FindByCpfAsync_ShouldReturnCustomer_WhenUserExists() + { + // Arrange + var cpf = "28642827041"; + var response = new AdminGetUserResponse + { + UserAttributes = + [ + new AttributeType { Name = "email", Value = "test@example.com" }, + new AttributeType { Name = "name", Value = "Test User" }, + new AttributeType { Name = "sub", Value = Guid.NewGuid().ToString() } + ] + }; + + _cognitoClientMock.Setup(c => c.AdminGetUserAsync(It.IsAny(), default)) + .ReturnsAsync(response); + + // Act + var result = await _userManager.FindByCpfAsync(cpf); + + // Assert + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Cpf.Value.Should().Be(cpf); + result.Email.Should().Be("test@example.com"); + result.Name.Should().Be("Test User"); + result.Id.Should().NotBeEmpty(); + } + } + + [Fact] + public async Task FindByCpfAsync_ShouldReturnNull_WhenUserNotFound() + { + // Arrange + const string cpf = "123456789"; + + _cognitoClientMock.Setup(c => c.AdminGetUserAsync(It.IsAny(), default)) + .ThrowsAsync(new UserNotFoundException("User not found")); + + // Act + var result = await _userManager.FindByCpfAsync(cpf); + + // Assert + result.Should().BeNull(); + } + + [Fact] + public async Task CreateAsync_ShouldReturnCustomer_WhenUserIsCreated() + { + // Arrange + var customer = new Customer(Guid.NewGuid(), "28642827041", "Test User", "test@example.com"); + + _cognitoClientMock.Setup(c => c.AdminCreateUserAsync(It.IsAny(), default)) + .ReturnsAsync(new AdminCreateUserResponse + { + User = new UserType + { + Attributes = [new AttributeType() { Name = "sub", Value = customer.Id.ToString() }] + } + }); + + // Act + var result = await _userManager.CreateAsync(customer); + + // Assert + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.Cpf.Should().Be(customer.Cpf); + result.Name.Should().Be(customer.Name); + result.Email.Should().Be(customer.Email); + result.Id.Should().NotBeEmpty(); + } + } +} diff --git a/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test.csproj b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test.csproj new file mode 100644 index 0000000..c48efbc --- /dev/null +++ b/tests/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test/FIAP.TechChallenge.ByteMeBurger.Cognito.Gateway.Test.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + diff --git a/tf/main.tf b/tf/main.tf index 1b13482..aff8078 100644 --- a/tf/main.tf +++ b/tf/main.tf @@ -19,6 +19,18 @@ data "aws_eks_cluster" "techchallenge_cluster" { # api_id = tolist(data.aws_apigatewayv2_apis.api_id.ids)[0] # } +############################## +# COGNITO USER POOL +############################## + +data "aws_cognito_user_pools" "user_pool" { + name = var.user_pool_name +} + +data "aws_cognito_user_pool_clients" "api_client" { + user_pool_id = data.aws_cognito_user_pools.user_pool.ids[0] +} + ############################## # SQS ############################## @@ -39,11 +51,13 @@ data "aws_rds_cluster" "example" { } locals { - connection_string = "Server=${data.aws_rds_cluster.example.endpoint};Database=${data.aws_rds_cluster.example.database_name};Uid=${var.db_user};Pwd=${var.db_pwd};Port=${data.aws_rds_cluster.example.port};" - jwt_issuer = var.jwt_issuer - jwt_aud = var.jwt_aud - docker_image = var.api_docker_image - events_queue_name = aws_sqs_queue.bmb-events.name + connection_string = "Server=${data.aws_rds_cluster.example.endpoint};Database=${data.aws_rds_cluster.example.database_name};Uid=${var.db_user};Pwd=${var.db_pwd};Port=${data.aws_rds_cluster.example.port};" + jwt_issuer = var.jwt_issuer + jwt_aud = var.jwt_aud + docker_image = var.api_docker_image + events_queue_name = aws_sqs_queue.bmb-events.name + cognito_user_pool_id = data.aws_cognito_user_pools.user_pool.ids[0] + cognito_user_pool_client_id = data.aws_cognito_user_pool_clients.api_client.client_ids[0] } @@ -81,8 +95,14 @@ resource "kubernetes_config_map_v1" "config_map_api" { "SqsSettings__QueueName" = local.events_queue_name "SqsSettings__Enabled" = true "SqsSettings__Region" = "us-east-1" - "SqsSettings__ClientId" = var.access_key_id - "SqsSettings__ClientSecret" = var.secret_access_key + "SqsSettings__ClientId" = var.api_access_key_id + "SqsSettings__ClientSecret" = var.api_secret_access_key + "CognitoSettings__UserPoolId" = local.cognito_user_pool_id + "CognitoSettings__UserPoolClientId" = local.cognito_user_pool_client_id + "CognitoSettings__Enabled" = true + "CognitoSettings__Region" = "us-east-1" + "CognitoSettings__ClientId" = var.api_access_key_id + "CognitoSettings__ClientSecret" = var.api_secret_access_key } } diff --git a/tf/output.tf b/tf/output.tf index d429e7f..2cc9c3c 100644 --- a/tf/output.tf +++ b/tf/output.tf @@ -2,3 +2,12 @@ output "eks_cluster" { value = data.aws_eks_cluster.techchallenge_cluster sensitive = true } + +output "userpool_id" { + value = local.cognito_user_pool_id + +} + +output "api_client_id" { + value = local.cognito_user_pool_client_id +} diff --git a/tf/variables.tf b/tf/variables.tf index 96eb07f..bf8678c 100644 --- a/tf/variables.tf +++ b/tf/variables.tf @@ -74,14 +74,19 @@ variable "db_pwd" { default = "db_password" } -variable "access_key_id" { +variable "api_access_key_id" { type = string nullable = false sensitive = true } -variable "secret_access_key" { +variable "api_secret_access_key" { type = string nullable = false sensitive = true } + +variable "user_pool_name" { + type = string + default = "bmb-users-pool-local" +}