diff --git a/src/HelloShop.ApiService/Extensions/OpenApiConfigureOptions.cs b/src/HelloShop.ApiService/Extensions/OpenApiConfigureOptions.cs index 221a2d3..15cdae8 100644 --- a/src/HelloShop.ApiService/Extensions/OpenApiConfigureOptions.cs +++ b/src/HelloShop.ApiService/Extensions/OpenApiConfigureOptions.cs @@ -21,7 +21,8 @@ public class OpenApiConfigureOptions(IConfiguredServiceEndPointResolver serviceR try { - HttpResponseMessage response = httpClient.GetAsync(uriBuilder.Uri).GetAwaiter().GetResult(); + HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri) { Version = new Version(2, 0) }; + HttpResponseMessage response = httpClient.SendAsync(request).GetAwaiter().GetResult(); if (response.IsSuccessStatusCode) { urlDescriptors.Add(new UrlDescriptor diff --git a/src/HelloShop.ApiService/HelloShop.ApiService.csproj b/src/HelloShop.ApiService/HelloShop.ApiService.csproj index f6b7233..66e34c5 100644 --- a/src/HelloShop.ApiService/HelloShop.ApiService.csproj +++ b/src/HelloShop.ApiService/HelloShop.ApiService.csproj @@ -9,6 +9,6 @@ - + \ No newline at end of file diff --git a/src/HelloShop.AppHost/HelloShop.AppHost.csproj b/src/HelloShop.AppHost/HelloShop.AppHost.csproj index 2a4cfa8..0b4dd96 100644 --- a/src/HelloShop.AppHost/HelloShop.AppHost.csproj +++ b/src/HelloShop.AppHost/HelloShop.AppHost.csproj @@ -15,6 +15,6 @@ - + \ No newline at end of file diff --git a/src/HelloShop.BasketService/AutoMapper/BasketsMapConfiguration.cs b/src/HelloShop.BasketService/AutoMapper/BasketsMapConfiguration.cs new file mode 100644 index 0000000..75d3627 --- /dev/null +++ b/src/HelloShop.BasketService/AutoMapper/BasketsMapConfiguration.cs @@ -0,0 +1,19 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using AutoMapper; +using HelloShop.BasketService.Entities; +using HelloShop.BasketService.Protos; + +namespace HelloShop.BasketService.AutoMapper +{ + public class BasketsMapConfiguration : Profile + { + public BasketsMapConfiguration() + { + CreateMap(); + CreateMap().ReverseMap(); + CreateMap(); + } + } +} diff --git a/src/HelloShop.BasketService/Entities/BasketItem.cs b/src/HelloShop.BasketService/Entities/BasketItem.cs new file mode 100644 index 0000000..408bcb5 --- /dev/null +++ b/src/HelloShop.BasketService/Entities/BasketItem.cs @@ -0,0 +1,12 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +namespace HelloShop.BasketService.Entities +{ + public class BasketItem + { + public int ProductId { get; set; } + + public int Quantity { get; set; } + } +} diff --git a/src/HelloShop.BasketService/Entities/CustomerBasket.cs b/src/HelloShop.BasketService/Entities/CustomerBasket.cs new file mode 100644 index 0000000..dec2512 --- /dev/null +++ b/src/HelloShop.BasketService/Entities/CustomerBasket.cs @@ -0,0 +1,12 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +namespace HelloShop.BasketService.Entities +{ + public class CustomerBasket + { + public int BuyerId { get; set; } + + public List Items { get; set; } = []; + } +} diff --git a/src/HelloShop.BasketService/HelloShop.BasketService.csproj b/src/HelloShop.BasketService/HelloShop.BasketService.csproj index f7f620c..f555e61 100644 --- a/src/HelloShop.BasketService/HelloShop.BasketService.csproj +++ b/src/HelloShop.BasketService/HelloShop.BasketService.csproj @@ -1,16 +1,24 @@ - - - net8.0 - enable - enable - - - - - - - - - - + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/HelloShop.BasketService/Program.cs b/src/HelloShop.BasketService/Program.cs index c23d283..e0f29d5 100644 --- a/src/HelloShop.BasketService/Program.cs +++ b/src/HelloShop.BasketService/Program.cs @@ -1,22 +1,61 @@ // Copyright (c) HelloShop Corporation. All rights reserved. // See the license file in the project root for more information. +using Calzolari.Grpc.AspNetCore.Validation; +using HelloShop.BasketService.Repositories; using HelloShop.BasketService.Services; using HelloShop.ServiceDefaults.Extensions; +using Microsoft.IdentityModel.Tokens; +using System.Text; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -// Add services to the container. -builder.Services.AddGrpc(); +// Add extensions services to the container. + +const string issuerSigningKey = HelloShop.ServiceDefaults.Constants.IdentityConstants.IssuerSigningKey; + +builder.Services.AddAuthentication().AddJwtBearer(options => +{ + options.TokenValidationParameters.ValidateIssuer = false; + options.TokenValidationParameters.ValidateAudience = false; + options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.Default.GetBytes(issuerSigningKey)); +}); + +builder.Services.AddHttpContextAccessor(); +builder.Services.AddDistributedMemoryCache(); +builder.Services.AddSingleton(); + +builder.Services.AddGrpc(options => options.EnableMessageValidation()).AddJsonTranscoding(); +builder.Services.AddGrpcSwagger(); +builder.Services.AddOpenApi(); + +builder.Services.AddDataSeedingProviders(); +builder.Services.AddCustomLocalization(); +builder.Services.AddModelMapper().AddModelValidator(); +builder.Services.AddLocalization().AddPermissionDefinitions(); +builder.Services.AddAuthorization().AddRemotePermissionChecker().AddCustomAuthorization(); +builder.Services.AddGrpcValidation(); +builder.Services.AddCors(); +// End addd extensions services to the container. var app = builder.Build(); +app.UseCors(options => options.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader()); + app.MapDefaultEndpoints(); // Configure the HTTP request pipeline. -app.MapGrpcService(); -app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); +app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client.").WithTags("Welcome"); +// Configure extensions request pipeline. +app.UseAuthentication().UseAuthorization(); +app.MapGrpcService(); +app.MapGrpcService(); +app.UseDataSeedingProviders(); +app.UseCustomLocalization(); +app.UseOpenApi(); +app.MapGroup("api/Permissions").MapPermissionDefinitions("Permissions"); +// End configure extensions request pipeline. app.Run(); diff --git a/src/HelloShop.BasketService/Properties/launchSettings.json b/src/HelloShop.BasketService/Properties/launchSettings.json index 4938073..eae0e29 100644 --- a/src/HelloShop.BasketService/Properties/launchSettings.json +++ b/src/HelloShop.BasketService/Properties/launchSettings.json @@ -4,7 +4,8 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "http://localhost:8004", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -13,7 +14,8 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": false, + "launchBrowser": true, + "launchUrl": "swagger", "applicationUrl": "https://localhost:8104;http://localhost:8004", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/src/HelloShop.BasketService/Protos/basket.proto b/src/HelloShop.BasketService/Protos/basket.proto new file mode 100644 index 0000000..b09d21e --- /dev/null +++ b/src/HelloShop.BasketService/Protos/basket.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +import "google/api/annotations.proto"; + +option csharp_namespace = "HelloShop.BasketService.Protos"; + +package basket; + +service Basket { + rpc GetBasket(google.protobuf.Empty) returns (CustomerBasketResponse) { + option (google.api.http) = { + get: "/api/basket" + }; + } + + rpc UpdateBasket(UpdateBasketRequest) returns (CustomerBasketResponse) { + option (google.api.http) = { + put: "/api/basket" + body: "*" + }; + } + + rpc DeleteBasket(google.protobuf.Empty) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete: "/api/basket" + }; + } +} + +message CustomerBasketResponse { + repeated BasketListItem items = 1; +} + +message BasketListItem { + int32 product_id = 1; + int32 quantity = 2; +} + +message UpdateBasketRequest { + repeated BasketListItem items = 1; +} \ No newline at end of file diff --git a/src/HelloShop.BasketService/Protos/greet.proto b/src/HelloShop.BasketService/Protos/greet.proto index b8334fb..72eed6f 100644 --- a/src/HelloShop.BasketService/Protos/greet.proto +++ b/src/HelloShop.BasketService/Protos/greet.proto @@ -1,13 +1,20 @@ syntax = "proto3"; -option csharp_namespace = "HelloShop.BasketService"; +option csharp_namespace = "HelloShop.BasketService.Protos"; + +import "google/api/annotations.proto"; package greet; // The greeting service definition. service Greeter { // Sends a greeting - rpc SayHello (HelloRequest) returns (HelloReply); + rpc SayHello (HelloRequest) returns (HelloReply){ + option (google.api.http) = { + post: "/api/greet" + body: "*" + }; + } } // The request message containing the user's name. diff --git a/src/HelloShop.BasketService/Repositories/DistributedCacheBasketRepository.cs b/src/HelloShop.BasketService/Repositories/DistributedCacheBasketRepository.cs new file mode 100644 index 0000000..135609f --- /dev/null +++ b/src/HelloShop.BasketService/Repositories/DistributedCacheBasketRepository.cs @@ -0,0 +1,28 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using HelloShop.BasketService.Entities; +using Microsoft.Extensions.Caching.Distributed; + +namespace HelloShop.BasketService.Repositories +{ + public class DistributedCacheBasketRepository(IDistributedCache cache) : IBasketRepository + { + private const string BasketKeyPrefix = "basket"; + + private static string GetBasketKey(int customerId) => $"{BasketKeyPrefix}:{customerId}"; + + public async Task DeleteBasketAsync(int customerId, CancellationToken token = default) => await cache.RemoveAsync(GetBasketKey(customerId), token); + + public async Task GetBasketAsync(int customerId, CancellationToken token = default) => await cache.GetObjectAsync(GetBasketKey(customerId), token); + + public async Task UpdateBasketAsync(CustomerBasket basket, CancellationToken token = default) + { + DistributedCacheEntryOptions options = new() { SlidingExpiration = TimeSpan.MaxValue }; + + await cache.SetObjectAsync(GetBasketKey(basket.BuyerId), basket, options, token); + + return await GetBasketAsync(basket.BuyerId, token); + } + } +} diff --git a/src/HelloShop.BasketService/Repositories/IBasketRepository.cs b/src/HelloShop.BasketService/Repositories/IBasketRepository.cs new file mode 100644 index 0000000..835a026 --- /dev/null +++ b/src/HelloShop.BasketService/Repositories/IBasketRepository.cs @@ -0,0 +1,16 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using HelloShop.BasketService.Entities; + +namespace HelloShop.BasketService.Repositories +{ + public interface IBasketRepository + { + Task GetBasketAsync(int customerId, CancellationToken token = default); + + Task UpdateBasketAsync(CustomerBasket basket, CancellationToken token = default); + + Task DeleteBasketAsync(int customerId, CancellationToken token = default); + } +} diff --git a/src/HelloShop.BasketService/Services/CustomerBasketService.cs b/src/HelloShop.BasketService/Services/CustomerBasketService.cs new file mode 100644 index 0000000..5d7739d --- /dev/null +++ b/src/HelloShop.BasketService/Services/CustomerBasketService.cs @@ -0,0 +1,78 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using AutoMapper; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using HelloShop.BasketService.Entities; + + +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using HelloShop.BasketService.Protos; +using HelloShop.BasketService.Repositories; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; + +namespace HelloShop.BasketService.Services +{ + [Authorize] + public class CustomerBasketService(IBasketRepository repository, ILogger logger, IMapper mapper) : Basket.BasketBase + { + public override async Task GetBasket(Empty request, ServerCallContext context) + { + string? nameIdentifier = context.GetHttpContext().User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + if (!int.TryParse(nameIdentifier, out int userId)) + { + throw new RpcException(new Status(StatusCode.Unauthenticated, "The caller is not authenticated.")); + } + + logger.LogDebug("Begin GetBasketById call from method {Method} for basket id {Id}", context.Method, userId); + + CustomerBasket? basket = await repository.GetBasketAsync(userId, context.CancellationToken); + + if (basket is not null) + { + mapper.Map(basket); + } + + return new(); + } + + public override async Task UpdateBasket(UpdateBasketRequest request, ServerCallContext context) + { + string? nameIdentifier = context.GetHttpContext().User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + if (!int.TryParse(nameIdentifier, out int userId)) + { + throw new RpcException(new Status(StatusCode.Unauthenticated, "The caller is not authenticated.")); + } + + logger.LogDebug("Begin UpdateBasket call from method {Method} for basket id {Id}", context.Method, userId); + + CustomerBasket customerBasket = mapper.Map(request, opts => opts.AfterMap((src, dest) => dest.BuyerId = userId)); + + CustomerBasket? result = await repository.UpdateBasketAsync(customerBasket, context.CancellationToken); + + result = result ?? throw new RpcException(new Status(StatusCode.NotFound, $"Basket with buyer id {userId} does not exist")); + + return mapper.Map(result); + } + + public override async Task DeleteBasket(Empty request, ServerCallContext context) + { + string? nameIdentifier = context.GetHttpContext().User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + if (!int.TryParse(nameIdentifier, out int userId)) + { + throw new RpcException(new Status(StatusCode.Unauthenticated, "The caller is not authenticated.")); + } + + await repository.DeleteBasketAsync(userId, context.CancellationToken); + + return new(); + } + } +} diff --git a/src/HelloShop.BasketService/Services/GreeterService.cs b/src/HelloShop.BasketService/Services/GreeterService.cs index 7722524..643df76 100644 --- a/src/HelloShop.BasketService/Services/GreeterService.cs +++ b/src/HelloShop.BasketService/Services/GreeterService.cs @@ -2,17 +2,12 @@ // See the license file in the project root for more information. using Grpc.Core; +using HelloShop.BasketService.Protos; namespace HelloShop.BasketService.Services; public class GreeterService : Greeter.GreeterBase { - private readonly ILogger _logger; - public GreeterService(ILogger logger) - { - _logger = logger; - } - public override Task SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply diff --git a/src/HelloShop.BasketService/Validations/Baskets/BasketListItemValidator.cs b/src/HelloShop.BasketService/Validations/Baskets/BasketListItemValidator.cs new file mode 100644 index 0000000..ca5e69a --- /dev/null +++ b/src/HelloShop.BasketService/Validations/Baskets/BasketListItemValidator.cs @@ -0,0 +1,18 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using FluentValidation; +using FluentValidation.Validators; +using HelloShop.BasketService.Protos; + +namespace HelloShop.BasketService.Validations.Baskets +{ + public class BasketListItemValidator : AbstractValidator + { + public BasketListItemValidator() + { + RuleFor(x => x.ProductId).GreaterThan(0); + RuleFor(x => x.Quantity).GreaterThan(0); + } + } +} \ No newline at end of file diff --git a/src/HelloShop.BasketService/Validations/Baskets/UpdateBasketRequestValidator.cs b/src/HelloShop.BasketService/Validations/Baskets/UpdateBasketRequestValidator.cs new file mode 100644 index 0000000..9c1839c --- /dev/null +++ b/src/HelloShop.BasketService/Validations/Baskets/UpdateBasketRequestValidator.cs @@ -0,0 +1,17 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using FluentValidation; +using HelloShop.BasketService.Protos; + +namespace HelloShop.BasketService.Validations.Baskets +{ + public class UpdateBasketRequestValidator : AbstractValidator + { + public UpdateBasketRequestValidator() + { + RuleFor(x => x.Items).NotNull(); + RuleForEach(x => x.Items).SetValidator(new BasketListItemValidator()); + } + } +} diff --git a/src/HelloShop.BasketService/Validations/Greeters/HelloRequestValidator.cs b/src/HelloShop.BasketService/Validations/Greeters/HelloRequestValidator.cs new file mode 100644 index 0000000..ce3dd3b --- /dev/null +++ b/src/HelloShop.BasketService/Validations/Greeters/HelloRequestValidator.cs @@ -0,0 +1,16 @@ +// Copyright (c) HelloShop Corporation. All rights reserved. +// See the license file in the project root for more information. + +using FluentValidation; +using HelloShop.BasketService.Protos; + +namespace HelloShop.BasketService.Validations.Greeters +{ + public class HelloRequestValidator : AbstractValidator + { + public HelloRequestValidator() + { + RuleFor(x => x.Name).NotNull().NotEmpty().Length(3, 20); + } + } +} diff --git a/src/HelloShop.BasketService/appsettings.json b/src/HelloShop.BasketService/appsettings.json index ec04bc1..f097952 100644 --- a/src/HelloShop.BasketService/appsettings.json +++ b/src/HelloShop.BasketService/appsettings.json @@ -5,5 +5,10 @@ "Microsoft.AspNetCore": "Warning" } }, + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + }, "AllowedHosts": "*" } \ No newline at end of file diff --git a/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj b/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj index 5659c69..63578c4 100644 --- a/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj +++ b/src/HelloShop.HybridApp/HelloShop.HybridApp.csproj @@ -58,9 +58,9 @@ - - - + + + diff --git a/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj b/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj index a8605af..b610f84 100644 --- a/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj +++ b/src/HelloShop.IdentityService/HelloShop.IdentityService.csproj @@ -8,9 +8,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/HelloShop.ProductService/HelloShop.ProductService.csproj b/src/HelloShop.ProductService/HelloShop.ProductService.csproj index 7a3fbcb..af10394 100644 --- a/src/HelloShop.ProductService/HelloShop.ProductService.csproj +++ b/src/HelloShop.ProductService/HelloShop.ProductService.csproj @@ -8,8 +8,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/HelloShop.ServiceDefaults/Extensions/OpenApiExtensions.cs b/src/HelloShop.ServiceDefaults/Extensions/OpenApiExtensions.cs index 7d5ab3f..df7eb41 100644 --- a/src/HelloShop.ServiceDefaults/Extensions/OpenApiExtensions.cs +++ b/src/HelloShop.ServiceDefaults/Extensions/OpenApiExtensions.cs @@ -22,7 +22,7 @@ namespace HelloShop.ServiceDefaults.Extensions services.Configure(options => { - options.DocumentTitle = Assembly.GetExecutingAssembly().GetName().Name; + options.DocumentTitle = Assembly.GetEntryAssembly()?.GetName().Name; options.InjectStylesheet("/ServiceDefaults/Resources/OpenApi/Custom.css"); }); diff --git a/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj b/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj index 033d269..0c30ba5 100644 --- a/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj +++ b/src/HelloShop.ServiceDefaults/HelloShop.ServiceDefaults.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj b/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj index 2706853..5396ee6 100644 --- a/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj +++ b/tests/HelloShop.FunctionalTests/HelloShop.FunctionalTests.csproj @@ -9,14 +9,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj b/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj index 6489c44..b1e92fe 100644 --- a/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj +++ b/tests/HelloShop.ProductService.FunctionalTests/HelloShop.ProductService.FunctionalTests.csproj @@ -14,11 +14,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj b/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj index d3cf251..1e39637 100644 --- a/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj +++ b/tests/HelloShop.ProductService.UnitTests/HelloShop.ProductService.UnitTests.csproj @@ -14,11 +14,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive