diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 1a8d9c284..cfff01732 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -112,12 +112,10 @@ services: environment: - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_URLS=http://0.0.0.0:80 - - CatalogUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5101 - - OrderingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5102 - IdentityUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5105 #Local: You need to open your local dev-machine firewall at range 5100-5105. at range 5100-5105. - - BasketUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5103 - MarketingUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5110 - LocationsUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5109 + - PurchaseUrl=http://${ESHOP_EXTERNAL_DNS_NAME_OR_IP}:5200 - CatalogUrlHC=http://catalog.api/hc - OrderingUrlHC=http://ordering.api/hc - IdentityUrlHC=http://identity.api/hc #Local: Use ${ESHOP_PROD_EXTERNAL_DNS_NAME_OR_IP}, if using external IP or DNS name from browser. diff --git a/src/Apigw/OcelotApiGw/Startup.cs b/src/Apigw/OcelotApiGw/Startup.cs index 9c7eb18ef..01ffe5139 100644 --- a/src/Apigw/OcelotApiGw/Startup.cs +++ b/src/Apigw/OcelotApiGw/Startup.cs @@ -29,6 +29,15 @@ namespace OcelotApiGw var identityUrl = _cfg.GetValue("IdentityUrl"); var authenticationProviderKey = "IdentityApiKey"; + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + services.AddAuthentication() .AddJwtBearer(authenticationProviderKey, x => { @@ -68,6 +77,8 @@ namespace OcelotApiGw loggerFactory.AddConsole(_cfg.GetSection("Logging")); + app.UseCors("CorsPolicy"); + app.UseOcelot().Wait(); } } diff --git a/src/BFFs/PurchaseBff/Config/UrlsConfig.cs b/src/BFFs/PurchaseBff/Config/UrlsConfig.cs index bca4db9ff..58f344aac 100644 --- a/src/BFFs/PurchaseBff/Config/UrlsConfig.cs +++ b/src/BFFs/PurchaseBff/Config/UrlsConfig.cs @@ -10,6 +10,7 @@ namespace PurchaseBff.Config public class CatalogOperations { public static string GetItemById(int id) => $"/api/v1/catalog/items/{id}"; + public static string GetItemsById(IEnumerable ids) => $"/api/v1/catalog/items?ids={string.Join(',', ids)}"; } public class BasketOperations diff --git a/src/BFFs/PurchaseBff/Controllers/BasketController.cs b/src/BFFs/PurchaseBff/Controllers/BasketController.cs index 17dbf6ff5..ac8620623 100644 --- a/src/BFFs/PurchaseBff/Controllers/BasketController.cs +++ b/src/BFFs/PurchaseBff/Controllers/BasketController.cs @@ -21,6 +21,49 @@ namespace PurchaseBff.Controllers _basket = basketService; } + [HttpPost] + [HttpPut] + public async Task UpdateAllBasket([FromBody] UpdateBasketRequest data) + { + + if (data.Items == null || !data.Items.Any()) + { + return BadRequest("Need to pass at least one basket line"); + } + + // Retrieve the current basket + var currentBasket = await _basket.GetById(data.BuyerId); + if (currentBasket == null) + { + currentBasket = new BasketData(data.BuyerId); + } + + var catalogItems = await _catalog.GetCatalogItems(data.Items.Select(x => x.ProductId)); + var newBasket = new BasketData(data.BuyerId); + + foreach (var bitem in data.Items) + { + var catalogItem = catalogItems.SingleOrDefault(ci => ci.Id == bitem.ProductId); + if (catalogItem == null) + { + return BadRequest($"Basket refers to a non-existing catalog item ({bitem.ProductId})"); + } + + newBasket.Items.Add(new BasketDataItem() + { + Id = bitem.Id, + ProductId = catalogItem.Id.ToString(), + ProductName = catalogItem.Name, + PictureUrl = catalogItem.PictureUri, + UnitPrice = catalogItem.Price, + Quantity = bitem.Quantity + }); + } + + await _basket.Update(newBasket); + return Ok(newBasket); + } + [HttpPut] [Route("items")] public async Task UpdateQuantities([FromBody] UpdateBasketItemsRequest data) diff --git a/src/BFFs/PurchaseBff/Models/UpdateBasketRequest.cs b/src/BFFs/PurchaseBff/Models/UpdateBasketRequest.cs new file mode 100644 index 000000000..a6813c788 --- /dev/null +++ b/src/BFFs/PurchaseBff/Models/UpdateBasketRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace PurchaseBff.Models +{ + public class UpdateBasketRequest + { + public string BuyerId { get; set; } + + public IEnumerable Items { get; set; } + } + + public class UpdateBasketRequestItemData + { + public string Id { get; set; } // Basket id + public int ProductId { get; set; } // Catalog item id + public int Quantity { get; set; } // Quantity + } +} diff --git a/src/BFFs/PurchaseBff/Properties/launchSettings.json b/src/BFFs/PurchaseBff/Properties/launchSettings.json index d6c88d211..925e70b0d 100644 --- a/src/BFFs/PurchaseBff/Properties/launchSettings.json +++ b/src/BFFs/PurchaseBff/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:58243/", + "applicationUrl": "http://localhost:57425/", "sslPort": 0 } }, diff --git a/src/BFFs/PurchaseBff/Services/CatalogService.cs b/src/BFFs/PurchaseBff/Services/CatalogService.cs index 071c162b4..7030da747 100644 --- a/src/BFFs/PurchaseBff/Services/CatalogService.cs +++ b/src/BFFs/PurchaseBff/Services/CatalogService.cs @@ -31,5 +31,13 @@ namespace PurchaseBff.Services var item = JsonConvert.DeserializeObject(data); return item; } + + public async Task> GetCatalogItems(IEnumerable ids) + { + var data = await _apiClient.GetStringAsync(_urls.Catalog + UrlsConfig.CatalogOperations.GetItemsById(ids)); + var item = JsonConvert.DeserializeObject(data); + return item; + + } } } diff --git a/src/BFFs/PurchaseBff/Services/ICatalogService.cs b/src/BFFs/PurchaseBff/Services/ICatalogService.cs index f7caaf6e3..b51c0f0cd 100644 --- a/src/BFFs/PurchaseBff/Services/ICatalogService.cs +++ b/src/BFFs/PurchaseBff/Services/ICatalogService.cs @@ -9,5 +9,6 @@ namespace PurchaseBff.Services public interface ICatalogService { Task GetCatalogItem(int id); + Task> GetCatalogItems(IEnumerable ids); } } diff --git a/src/Services/Basket/Basket.API/Properties/launchSettings.json b/src/Services/Basket/Basket.API/Properties/launchSettings.json index bce29a595..7d1c959e3 100644 --- a/src/Services/Basket/Basket.API/Properties/launchSettings.json +++ b/src/Services/Basket/Basket.API/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:50920/", + "applicationUrl": "http://localhost:57423/", "sslPort": 0 } }, diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs index 0d224fc68..71f700762 100644 --- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs +++ b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs @@ -32,11 +32,16 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers // GET api/v1/[controller]/items[?pageSize=3&pageIndex=10] [HttpGet] - [Route("[action]")] + [Route("items")] [ProducesResponseType(typeof(PaginatedItemsViewModel), (int)HttpStatusCode.OK)] - public async Task Items([FromQuery]int pageSize = 10, [FromQuery]int pageIndex = 0) - + [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] + public async Task Items([FromQuery]int pageSize = 10, [FromQuery]int pageIndex = 0, [FromQuery] string ids = null) { + if (!string.IsNullOrEmpty(ids)) + { + return GetItemsByIds(ids); + } + var totalItems = await _catalogContext.CatalogItems .LongCountAsync(); @@ -54,6 +59,23 @@ namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers return Ok(model); } + private IActionResult GetItemsByIds(string ids) + { + var numIds = ids.Split(',') + .Select(id => (Ok: int.TryParse(id, out int x), Value: x)); + if (!numIds.All(nid => nid.Ok)) + { + return BadRequest("ids value invalid. Must be comma-separated list of numbers"); + } + + var idsToSelect = numIds.Select(id => id.Value); + var items = _catalogContext.CatalogItems.Where(ci => idsToSelect.Contains(ci.Id)).ToList(); + + items = ChangeUriPlaceholder(items); + return Ok(items); + + } + [HttpGet] [Route("items/{id:int}")] [ProducesResponseType((int)HttpStatusCode.NotFound)] diff --git a/src/Services/Catalog/Catalog.API/Properties/launchSettings.json b/src/Services/Catalog/Catalog.API/Properties/launchSettings.json index a0b19b3bf..2b21ca280 100644 --- a/src/Services/Catalog/Catalog.API/Properties/launchSettings.json +++ b/src/Services/Catalog/Catalog.API/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:57623/", + "applicationUrl": "http://localhost:57424/", "sslPort": 0 } }, diff --git a/src/Web/WebSPA/AppSettings.cs b/src/Web/WebSPA/AppSettings.cs index fd254c3ae..c3fbd3b80 100644 --- a/src/Web/WebSPA/AppSettings.cs +++ b/src/Web/WebSPA/AppSettings.cs @@ -7,13 +7,13 @@ namespace eShopOnContainers.WebSPA { public class AppSettings { - public string BaseUrl { get; set; } - public string CatalogUrl { get; set; } - public string OrderingUrl { get; set; } public string IdentityUrl { get; set; } public string BasketUrl { get; set; } public string MarketingUrl { get; set; } + + public string PurchaseUrl { get; set; } + public string ActivateCampaignDetailFunction { get; set; } - public bool UseCustomizationData { get; set; } + public bool UseCustomizationData { get; set; } } } diff --git a/src/Web/WebSPA/Client/modules/basket/basket.service.ts b/src/Web/WebSPA/Client/modules/basket/basket.service.ts index 864915541..da422d5ad 100644 --- a/src/Web/WebSPA/Client/modules/basket/basket.service.ts +++ b/src/Web/WebSPA/Client/modules/basket/basket.service.ts @@ -23,6 +23,7 @@ import { Subject } from 'rxjs/Subject'; @Injectable() export class BasketService { private basketUrl: string = ''; + private purchaseUrl: string = ''; basket: IBasket = { buyerId: '', items: [] @@ -40,12 +41,14 @@ export class BasketService { if (this.authService.UserData) { this.basket.buyerId = this.authService.UserData.sub; if (this.configurationService.isReady) { - this.basketUrl = this.configurationService.serverSettings.basketUrl; + this.basketUrl = this.configurationService.serverSettings.purchaseUrl; + this.purchaseUrl = this.configurationService.serverSettings.purchaseUrl; this.loadData(); } else { this.configurationService.settingsLoaded$.subscribe(x => { - this.basketUrl = this.configurationService.serverSettings.basketUrl; + this.basketUrl = this.configurationService.serverSettings.purchaseUrl; + this.purchaseUrl = this.configurationService.serverSettings.purchaseUrl; this.loadData(); }); } @@ -63,7 +66,7 @@ export class BasketService { } setBasket(basket): Observable { - let url = this.basketUrl + '/api/v1/basket/'; + let url = this.purchaseUrl + '/purchase-bff/api/v1/basket/'; this.basket = basket; return this.service.post(url, basket).map((response: Response) => { return true; @@ -71,7 +74,7 @@ export class BasketService { } setBasketCheckout(basketCheckout): Observable { - let url = this.basketUrl + '/api/v1/basket/checkout'; + let url = this.basketUrl + '/purchase-bff/api/v1/b/basket/checkout'; return this.service.postWithId(url, basketCheckout).map((response: Response) => { this.basketEvents.orderCreated(); return true; @@ -79,7 +82,7 @@ export class BasketService { } getBasket(): Observable { - let url = this.basketUrl + '/api/v1/basket/' + this.basket.buyerId; + let url = this.basketUrl + '/api/v1/b/basket/' + this.basket.buyerId; return this.service.get(url).map((response: Response) => { if (response.status === 204) { return null; diff --git a/src/Web/WebSPA/Client/modules/catalog/catalog.service.ts b/src/Web/WebSPA/Client/modules/catalog/catalog.service.ts index bdcb9d664..672146c9e 100644 --- a/src/Web/WebSPA/Client/modules/catalog/catalog.service.ts +++ b/src/Web/WebSPA/Client/modules/catalog/catalog.service.ts @@ -21,9 +21,9 @@ export class CatalogService { constructor(private service: DataService, private configurationService: ConfigurationService) { this.configurationService.settingsLoaded$.subscribe(x => { - this.catalogUrl = this.configurationService.serverSettings.catalogUrl + '/api/v1/catalog/items'; - this.brandUrl = this.configurationService.serverSettings.catalogUrl + '/api/v1/catalog/catalogbrands'; - this.typesUrl = this.configurationService.serverSettings.catalogUrl + '/api/v1/catalog/catalogtypes'; + this.catalogUrl = this.configurationService.serverSettings.purchaseUrl + '/purchase-bff/api/v1/c/catalog/items'; + this.brandUrl = this.configurationService.serverSettings.purchaseUrl + '/purchase-bff/api/v1/c/catalog/catalogbrands'; + this.typesUrl = this.configurationService.serverSettings.purchaseUrl + '/purchase-bff/api/v1/c/catalog/catalogtypes'; }); } diff --git a/src/Web/WebSPA/Client/modules/orders/orders.service.ts b/src/Web/WebSPA/Client/modules/orders/orders.service.ts index 5eda7c8ce..8d96602a1 100644 --- a/src/Web/WebSPA/Client/modules/orders/orders.service.ts +++ b/src/Web/WebSPA/Client/modules/orders/orders.service.ts @@ -22,14 +22,14 @@ export class OrdersService { constructor(private service: DataService, private basketService: BasketWrapperService, private identityService: SecurityService, private configurationService: ConfigurationService) { if (this.configurationService.isReady) - this.ordersUrl = this.configurationService.serverSettings.orderingUrl; + this.ordersUrl = this.configurationService.serverSettings.purchaseUrl; else - this.configurationService.settingsLoaded$.subscribe(x => this.ordersUrl = this.configurationService.serverSettings.orderingUrl); + this.configurationService.settingsLoaded$.subscribe(x => this.ordersUrl = this.configurationService.serverSettings.purchaseUrl); } getOrders(): Observable { - let url = this.ordersUrl + '/api/v1/orders'; + let url = this.ordersUrl + '/purchase-bff/api/v1/o/orders'; return this.service.get(url).map((response: Response) => { return response.json(); @@ -37,7 +37,7 @@ export class OrdersService { } getOrder(id: number): Observable { - let url = this.ordersUrl + '/api/v1/orders/' + id; + let url = this.ordersUrl + '/purchase-bff/api/v1/o/orders/' + id; return this.service.get(url).map((response: Response) => { return response.json(); diff --git a/src/Web/WebSPA/Client/modules/shared/models/configuration.model.ts b/src/Web/WebSPA/Client/modules/shared/models/configuration.model.ts index f22b28a3e..49950e9d6 100644 --- a/src/Web/WebSPA/Client/modules/shared/models/configuration.model.ts +++ b/src/Web/WebSPA/Client/modules/shared/models/configuration.model.ts @@ -1,8 +1,6 @@ export interface IConfiguration { - catalogUrl: string, - orderingUrl: string, identityUrl: string, - basketUrl: string, marketingUrl: string, + purchaseUrl: string, activateCampaignDetailFunction: boolean } \ No newline at end of file diff --git a/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts b/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts index d41786859..ea95275e0 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/configuration.service.ts @@ -28,11 +28,9 @@ export class ConfigurationService { console.log('server settings loaded'); this.serverSettings = response.json(); console.log(this.serverSettings); - this.storageService.store('basketUrl', this.serverSettings.basketUrl); - this.storageService.store('catalogUrl', this.serverSettings.catalogUrl); this.storageService.store('identityUrl', this.serverSettings.identityUrl); - this.storageService.store('orderingUrl', this.serverSettings.orderingUrl); this.storageService.store('marketingUrl', this.serverSettings.marketingUrl); + this.storageService.store('purchaseUrl', this.serverSettings.purchaseUrl); this.storageService.store('activateCampaignDetailFunction', this.serverSettings.activateCampaignDetailFunction); this.isReady = true; this.settingsLoadedSource.next(); diff --git a/src/Web/WebSPA/Client/modules/shared/services/security.service.ts b/src/Web/WebSPA/Client/modules/shared/services/security.service.ts index 483ea4ae4..2b580b5cd 100644 --- a/src/Web/WebSPA/Client/modules/shared/services/security.service.ts +++ b/src/Web/WebSPA/Client/modules/shared/services/security.service.ts @@ -82,7 +82,7 @@ export class SecurityService { let client_id = 'js'; let redirect_uri = location.origin + '/'; let response_type = 'id_token token'; - let scope = 'openid profile orders basket marketing locations'; + let scope = 'openid profile orders basket marketing locations purchasebff'; let nonce = 'N' + Math.random() + '' + Date.now(); let state = Date.now() + '' + Math.random(); diff --git a/src/Web/WebSPA/Properties/launchSettings.json b/src/Web/WebSPA/Properties/launchSettings.json index 9ae65426a..bf0fc44d4 100644 --- a/src/Web/WebSPA/Properties/launchSettings.json +++ b/src/Web/WebSPA/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:50921/", + "applicationUrl": "http://localhost:64923/", "sslPort": 0 } }, diff --git a/src/Web/WebSPA/appsettings.json b/src/Web/WebSPA/appsettings.json index 7b1930f84..75f17ac35 100644 --- a/src/Web/WebSPA/appsettings.json +++ b/src/Web/WebSPA/appsettings.json @@ -1,10 +1,8 @@ { - "CatalogUrl": "http://localhost:5101", - "OrderingUrl": "http://localhost:5102", - "BasketUrl": "http://localhost:5103", "IdentityUrl": "http://localhost:5105", "MarketingUrl": "http://localhost:5110", "CallBackUrl": "http://localhost:5104/", + "PurchaseUrl": "http://localhost:5200", "UseCustomizationData": true, "IsClusterEnv": "False", "ActivateCampaignDetailFunction": true,