From 93831f24f27986010ee67d4e3571b19c46f23f1f Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi <01174367@pw.edu.pl> Date: Sun, 17 Mar 2024 21:28:20 +0100 Subject: [PATCH 1/5] (#24) (#19) modifying the blazor component for the sign-in --- .../Areas/Identity/IdentityService.cs | 2 +- MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs | 1 + .../HttpClients/CustomHttpClient.cs | 121 +++++++++++------- .../src/MiniSpace.Web/MiniSpace.Web.csproj | 7 +- .../src/MiniSpace.Web/Pages/Greeting.razor | 25 ++++ .../src/MiniSpace.Web/Pages/SignIn.razor | 20 ++- .../src/MiniSpace.Web/appsettings.json | 9 +- 7 files changed, 134 insertions(+), 51 deletions(-) create mode 100644 MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs index b2686d7ab..6b6d7014e 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs @@ -20,7 +20,7 @@ public Task GetAccountAsync(string jwt) } public Task SignUpAsync(string email, string password, string role = "user") - => _httpClient.PostAsync("identity/sign-up", new {email, password, role}); + => _httpClient.PostAsync("identity-service/sign-up", new {email, password, role}); public Task SignInAsync(string email, string password) => _httpClient.PostAsync("identity/sign-in", new {email, password}); diff --git a/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs b/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs index 92e754084..dd5b69029 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/DTO/JwtDto.cs @@ -4,6 +4,7 @@ public class JwtDto { public string AccessToken { get; set; } public string Role { get; set; } + public string RefreshToken { get; set; } public long Expires { get; set; } } } \ No newline at end of file diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs index 47aa7ca1d..86764c98d 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs @@ -2,7 +2,8 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Text.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Polly; @@ -11,17 +12,16 @@ namespace MiniSpace.Web.HttpClients { public class CustomHttpClient : IHttpClient { - private static readonly JsonSerializerOptions JsonSerializerOptions = new JsonSerializerOptions + private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - PropertyNameCaseInsensitive = true, - IgnoreNullValues = true + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore }; - + private readonly HttpClient _client; private readonly HttpClientOptions _options; private readonly ILogger _logger; - + public CustomHttpClient(HttpClient client, HttpClientOptions options, ILogger logger) { _client = client; @@ -29,25 +29,34 @@ public CustomHttpClient(HttpClient client, HttpClientOptions options, ILogger _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + + public void SetAccessToken(string token) + { + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); + } public async Task GetAsync(string uri) { var (success, content) = await TryExecuteAsync(uri, client => client.GetAsync(uri)); - return !success ? default : Parse(content); + return !success ? default : JsonConvert.DeserializeObject(content, JsonSerializerSettings); } public Task PostAsync(string uri, T request) - => TryExecuteAsync(uri, client => client.PostAsync(uri, GetPayload(request))); + { + var jsonPayload = JsonConvert.SerializeObject(request, JsonSerializerSettings); + _logger.LogDebug($"Sending HTTP POST request to URI: {uri} with payload: {jsonPayload}"); + return TryExecuteAsync(uri, client => client.PostAsync(uri, GetPayload(request))); + } public async Task PostAsync(string uri, TRequest request) { + var jsonPayload = JsonConvert.SerializeObject(request, JsonSerializerSettings); + _logger.LogDebug($"Sending HTTP POST request to URI: {uri} with payload: {jsonPayload}"); + var (success, content) = await TryExecuteAsync(uri, client => client.PostAsync(uri, GetPayload(request))); - return !success ? default : Parse(content); + return !success ? default : JsonConvert.DeserializeObject(content, JsonSerializerSettings); } public Task PutAsync(string uri, T request) @@ -57,42 +66,64 @@ public async Task PutAsync(string uri, TRequest requ { var (success, content) = await TryExecuteAsync(uri, client => client.PutAsync(uri, GetPayload(request))); - return !success ? default : Parse(content); + return !success ? default : JsonConvert.DeserializeObject(content, JsonSerializerSettings); } public Task DeleteAsync(string uri) => TryExecuteAsync(uri, client => client.DeleteAsync(uri)); - + private static StringContent GetPayload(T request) - => new StringContent(JsonSerializer.Serialize(request, JsonSerializerOptions), Encoding.UTF8, - "application/json"); - - private Task<(bool success, string content)> TryExecuteAsync(string uri, - Func> client) - => Policy.Handle() - .WaitAndRetryAsync(_options.Retries, r => TimeSpan.FromSeconds(Math.Pow(2, r))) - .ExecuteAsync(async () => +{ + var json = JsonConvert.SerializeObject(request, JsonSerializerSettings); + // Set content type with charset parameter + return new StringContent(json, Encoding.UTF8, "text/plain"); +} + + + + private Task<(bool success, string content)> TryExecuteAsync(string uri, Func> client) + => Policy.Handle() + .WaitAndRetryAsync(_options.Retries, r => TimeSpan.FromSeconds(Math.Pow(2, r))) + .ExecuteAsync(async () => + { + // Check if the BaseAddress is set and if the uri is not a full URL + if (_client.BaseAddress != null && !Uri.IsWellFormedUriString(uri, UriKind.Absolute)) + { + // If the uri does not start with a slash, add it to ensure proper URL construction + if (!uri.StartsWith("/")) uri = "/" + uri; + // Combine BaseAddress with the uri + uri = new Uri(_client.BaseAddress, uri).ToString(); + } + else if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute)) + { + // If the uri is not a full URL and there's no BaseAddress, log an error or handle accordingly + _logger.LogError($"The provided URI '{uri}' is not a valid absolute URL and no BaseAddress is set."); + return default; + } + + _logger.LogDebug($"Sending HTTP request to URI: {uri}"); + using (var response = await client(_client)) + { + if (response.IsSuccessStatusCode) + { + _logger.LogDebug($"Received a valid response to HTTP request from URI: {uri}" + + $"{Environment.NewLine}{response}"); + + return (true, await response.Content.ReadAsStringAsync()); + } + + if (!response.IsSuccessStatusCode) { - uri = uri.StartsWith("http://") ? uri : $"http://{uri}"; - _logger.LogDebug($"Sending HTTP request to URI: {uri}"); - using (var response = await client(_client)) - { - if (response.IsSuccessStatusCode) - { - _logger.LogDebug($"Received a valid response to HTTP request from URI: {uri}" + - $"{Environment.NewLine}{response}"); - - return (true, await response.Content.ReadAsStringAsync()); - } - - _logger.LogError($"Received an invalid response to HTTP request from URI: {uri}" + - $"{Environment.NewLine}{response}"); - - return default; - } - }); - - private static T Parse(string content) - => string.IsNullOrWhiteSpace(content) ? default : JsonSerializer.Deserialize(content, JsonSerializerOptions); + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogError($"Error response from server: {errorContent}"); + } + + _logger.LogError($"Received an invalid response to HTTP request from URI: {uri}" + + $"{Environment.NewLine}{response}"); + + return default; + } + }); + } -} \ No newline at end of file +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj index 098b98ffe..a37667483 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj +++ b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj @@ -1,11 +1,14 @@  - netcoreapp3.1 - + net6.0 + + + + diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor new file mode 100644 index 000000000..84256dfcf --- /dev/null +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/Greeting.razor @@ -0,0 +1,25 @@ +@page "/greeting" +@using Microsoft.AspNetCore.Components.Authorization +@inject AuthenticationStateProvider AuthenticationStateProvider + +

Greeting

+ + +@if (userName != null) +{ +

Hello, @userName!

+} +else +{ +

Loading...

+} + +@code { + private string userName; + + protected override async Task OnInitializedAsync() + { + var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); + userName = authState.User.Identity.Name; + } +} diff --git a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor b/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor index 172f6467e..e27c6a3d2 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor +++ b/MiniSpace.Web/src/MiniSpace.Web/Pages/SignIn.razor @@ -2,9 +2,18 @@ @using MiniSpace.Web.Models.Identity @using MiniSpace.Web.Areas.Identity @inject IIdentityService IdentityService +@inject NavigationManager NavigationManager +

Login

+@if (showError) +{ +
+ Failed to sign in. Please check your credentials and try again. +
+} + @@ -24,10 +33,19 @@ @code { private SignInModel signInModel = new SignInModel(); + private bool showError = false; private async Task HandleSignIn() { var jwtDto = await IdentityService.SignInAsync(signInModel.Email, signInModel.Password); - // Implement logic after successful sign-in, such as navigating to a dashboard or showing a success message + if (jwtDto != null && !string.IsNullOrEmpty(jwtDto.AccessToken)) + { + NavigationManager.NavigateTo("/greeting"); + } + else + { + showError = true; + StateHasChanged(); // Force the component to re-render + } } } diff --git a/MiniSpace.Web/src/MiniSpace.Web/appsettings.json b/MiniSpace.Web/src/MiniSpace.Web/appsettings.json index c58c711b9..c9ec0bf68 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/appsettings.json +++ b/MiniSpace.Web/src/MiniSpace.Web/appsettings.json @@ -7,9 +7,14 @@ } }, "AllowedHosts": "*", - "ApiGatewayUrl": "http://api-gateway/", "HttpClientOptions": { - "ApiUrl": "http://api-gateway/", + "ApiUrl": "http://localhost:5000", "Retries": 3 + }, + "Services": { + "IdentityService": { + "BaseUrl": "http://localhost:5004" + } } + } From 63f27d56cb281caf2b194342f03f0e6aa83d94ce Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi <01174367@pw.edu.pl> Date: Sun, 17 Mar 2024 21:31:10 +0100 Subject: [PATCH 2/5] (#24) (#19) modifying the endpoint for sign-up in --- .../src/MiniSpace.Web/Areas/Identity/IdentityService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs index 6b6d7014e..b2686d7ab 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/Areas/Identity/IdentityService.cs @@ -20,7 +20,7 @@ public Task GetAccountAsync(string jwt) } public Task SignUpAsync(string email, string password, string role = "user") - => _httpClient.PostAsync("identity-service/sign-up", new {email, password, role}); + => _httpClient.PostAsync("identity/sign-up", new {email, password, role}); public Task SignInAsync(string email, string password) => _httpClient.PostAsync("identity/sign-in", new {email, password}); From ca77f41b491cf44c8111f58baf5ab460bc29523f Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi <01174367@pw.edu.pl> Date: Sun, 17 Mar 2024 21:33:19 +0100 Subject: [PATCH 3/5] (#24) (#19) modify the ntrada endpoint --- .../src/MiniSpace.APIGateway/ntrada-async.yml | 3 +-- .../MiniSpace.APIGateway/ntrada.docker.yml | 2 +- .../src/MiniSpace.APIGateway/ntrada.yml | 24 ++++++++++--------- .../appsettings.json | 8 +++---- .../HttpClients/CustomHttpClient.cs | 5 +--- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml index 83af8339a..3bc4ebb7b 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada-async.yml @@ -127,8 +127,7 @@ modules: auth: false use: downstream downstream: identity-service/sign-in - responseHeaders: - content-type: application/json + services: identity-service: diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml index a03939ead..c77d6aca1 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.docker.yml @@ -18,7 +18,7 @@ generateTraceId: true useLocalUrl: false loadBalancer: enabled: false - url: fabio:9999 + url: faibo:9999 extensions: customErrors: diff --git a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml index b3d743a58..10f0d3548 100644 --- a/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml +++ b/MiniSpace.APIGateway/src/MiniSpace.APIGateway/ntrada.yml @@ -18,7 +18,7 @@ generateTraceId: true useLocalUrl: true loadBalancer: enabled: false - url: localhost:9999 + url: faibo:9999 extensions: customErrors: @@ -27,11 +27,13 @@ extensions: cors: allowCredentials: true allowedOrigins: - - '*' + - 'http://localhost:5606' allowedMethods: - - post - - put - - delete + - GET + - POST + - PUT + - DELETE + - OPTIONS allowedHeaders: - '*' exposedHeaders: @@ -42,10 +44,10 @@ extensions: jwt: issuerSigningKey: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij - validIssuer: pacco + validIssuer: minispace validateAudience: false - validateIssuer: true - validateLifetime: true + validateIssuer: false + validateLifetime: false swagger: name: MiniSpace @@ -73,7 +75,7 @@ modules: identity: - path: identity + path: /identity routes: - upstream: /users/{userId} method: GET @@ -103,8 +105,8 @@ modules: use: downstream downstream: identity-service/sign-in auth: false - responseHeaders: - content-type: application/json + + services: identity-service: diff --git a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json index da7a73cbb..6835aef77 100644 --- a/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json +++ b/MiniSpace.Services.Identity/src/MiniSpace.Services.Identity.Api/appsettings.json @@ -10,7 +10,7 @@ "service": "identity-service", "address": "docker.for.win.localhost", "port": "5004", - "pingEnabled": true, + "pingEnabled": false, "pingEndpoint": "ping", "pingInterval": 3, "removeAfterInterval": 3 @@ -37,10 +37,10 @@ }, "issuerSigningKey": "eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij", "expiryMinutes": 60, - "issuer": "pacco", + "issuer": "minispace", "validateAudience": false, "validateIssuer": false, - "validateLifetime": true, + "validateLifetime": false, "allowAnonymousEndpoints": ["/sign-in", "/sign-up"] }, "logger": { @@ -93,7 +93,7 @@ "influxEnabled": false, "prometheusEnabled": true, "influxUrl": "http://localhost:8086", - "database": "pacco", + "database": "minispace", "env": "local", "interval": 5 }, diff --git a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs index 86764c98d..74612dd59 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs +++ b/MiniSpace.Web/src/MiniSpace.Web/HttpClients/CustomHttpClient.cs @@ -86,17 +86,14 @@ private static StringContent GetPayload(T request) .WaitAndRetryAsync(_options.Retries, r => TimeSpan.FromSeconds(Math.Pow(2, r))) .ExecuteAsync(async () => { - // Check if the BaseAddress is set and if the uri is not a full URL if (_client.BaseAddress != null && !Uri.IsWellFormedUriString(uri, UriKind.Absolute)) { - // If the uri does not start with a slash, add it to ensure proper URL construction if (!uri.StartsWith("/")) uri = "/" + uri; - // Combine BaseAddress with the uri + uri = new Uri(_client.BaseAddress, uri).ToString(); } else if (!Uri.IsWellFormedUriString(uri, UriKind.Absolute)) { - // If the uri is not a full URL and there's no BaseAddress, log an error or handle accordingly _logger.LogError($"The provided URI '{uri}' is not a valid absolute URL and no BaseAddress is set."); return default; } From 85725716613801082bfa3029fd2330962c940890 Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi <01174367@pw.edu.pl> Date: Sun, 17 Mar 2024 21:54:56 +0100 Subject: [PATCH 4/5] (#19) Dockerfile for the web application with targetting framerok update --- .../scripts/dockerize-tag-push.sh | 0 .../scripts/dockerize-tag-push.sh | 0 MiniSpace.Web/Dockerfile | 22 ++++++++++++++----- MiniSpace.Web/scripts/dockerize-tag-push.sh | 0 MiniSpace/scripts/dockerize-all.sh | 0 5 files changed, 16 insertions(+), 6 deletions(-) mode change 100644 => 100755 MiniSpace.APIGateway/scripts/dockerize-tag-push.sh mode change 100644 => 100755 MiniSpace.Services.Identity/scripts/dockerize-tag-push.sh mode change 100644 => 100755 MiniSpace.Web/scripts/dockerize-tag-push.sh mode change 100644 => 100755 MiniSpace/scripts/dockerize-all.sh diff --git a/MiniSpace.APIGateway/scripts/dockerize-tag-push.sh b/MiniSpace.APIGateway/scripts/dockerize-tag-push.sh old mode 100644 new mode 100755 diff --git a/MiniSpace.Services.Identity/scripts/dockerize-tag-push.sh b/MiniSpace.Services.Identity/scripts/dockerize-tag-push.sh old mode 100644 new mode 100755 diff --git a/MiniSpace.Web/Dockerfile b/MiniSpace.Web/Dockerfile index 6c3d0ea83..faced5f3d 100644 --- a/MiniSpace.Web/Dockerfile +++ b/MiniSpace.Web/Dockerfile @@ -1,11 +1,21 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build +# Use the .NET 6 SDK for building the project +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /app + +# Copy everything and publish the release COPY . . -RUN dotnet publish src/MiniSpace.Web -c release -o out +RUN dotnet publish src/MiniSpace.Web -c Release -o out -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 +# Use the .NET 6 runtime for the final image +FROM mcr.microsoft.com/dotnet/aspnet:8.0 WORKDIR /app + +# Copy the published app from the build stage COPY --from=build /app/out . -ENV ASPNETCORE_URLS http://*:80 -ENV ASPNETCORE_ENVIRONMENT docker -ENTRYPOINT dotnet MiniSpace.Web.dll \ No newline at end of file + +# Set environment variables +ENV ASPNETCORE_URLS=http://*:80 +ENV ASPNETCORE_ENVIRONMENT=docker + +# Start the application +ENTRYPOINT ["dotnet", "MiniSpace.Web.dll"] diff --git a/MiniSpace.Web/scripts/dockerize-tag-push.sh b/MiniSpace.Web/scripts/dockerize-tag-push.sh old mode 100644 new mode 100755 diff --git a/MiniSpace/scripts/dockerize-all.sh b/MiniSpace/scripts/dockerize-all.sh old mode 100644 new mode 100755 From 9dabab394b51dcbdf6e98d7fe23d9a063480ef22 Mon Sep 17 00:00:00 2001 From: Andrii Voznesenskyi <01174367@pw.edu.pl> Date: Sun, 17 Mar 2024 22:17:22 +0100 Subject: [PATCH 5/5] (#19) modifying the settings to the web for the correct apigatway-blazor connection --- MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj | 2 +- MiniSpace.Web/src/MiniSpace.Web/appsettings.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj index a37667483..6b457aa04 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj +++ b/MiniSpace.Web/src/MiniSpace.Web/MiniSpace.Web.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 diff --git a/MiniSpace.Web/src/MiniSpace.Web/appsettings.json b/MiniSpace.Web/src/MiniSpace.Web/appsettings.json index c9ec0bf68..f1d6787a7 100644 --- a/MiniSpace.Web/src/MiniSpace.Web/appsettings.json +++ b/MiniSpace.Web/src/MiniSpace.Web/appsettings.json @@ -8,7 +8,7 @@ }, "AllowedHosts": "*", "HttpClientOptions": { - "ApiUrl": "http://localhost:5000", + "ApiUrl": "http://api-gateway", "Retries": 3 }, "Services": {