From 0a9396ad3daf835ae5578f0d33e9f3c80e4c4114 Mon Sep 17 00:00:00 2001 From: Adriano Serighelli Date: Wed, 17 Dec 2025 18:02:22 -0300 Subject: [PATCH] Refatora endpoints /api e modulariza processamento de faturas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refatoração dos endpoints PATCH e PUT em /api para melhor separação entre health check e processamento de invoices. Modularização do fluxo de processamento de faturas, melhorias no tratamento de arquivos PDF e leitura de requisições. Ajuste no construtor da classe Fatura para receber JsonElement. Logging e tratamento de erros aprimorados. Alteração do endpoint Kestrel para permitir acesso externo. Remoção de código duplicado e ajustes em PathBase. --- Faturas/Fatura.cs | 30 +- Webhook 4docs/Program.cs | 577 ++++++++++++++++++++------------- Webhook 4docs/appsettings.json | 4 +- 3 files changed, 369 insertions(+), 242 deletions(-) diff --git a/Faturas/Fatura.cs b/Faturas/Fatura.cs index 36354f2..6bead12 100644 --- a/Faturas/Fatura.cs +++ b/Faturas/Fatura.cs @@ -34,28 +34,24 @@ namespace Faturas /// Identificador da fatura no serviço 4docs. /// Caminho completo para o arquivo PDF da fatura local. /// Instância de HttpClient usada para realizar chamadas HTTP à API. - public Fatura(string id, string fatura_path, HttpClient httpClient) + public Fatura(string id, JsonElement fatura_Parsed, string fatura_path) { // Utilizado para gerar novo token // this.token = Req_token(HttpClient).ToString(); - HttpResponseMessage fatura_response = GetStatus(httpClient, Token, id); - if (fatura_response.IsSuccessStatusCode) + faturaParsed = fatura_Parsed; + Agrupada = faturaParsed.TryGetProperty("multiple", out var _); + Status = faturaParsed.GetProperty("newStatus").GetString(); + Arquivo = new FileInfo(fatura_path); + this.id = id; + + if (faturaParsed.TryGetProperty("external", out var _)) { - faturaParsed = JsonDocument.Parse(fatura_response.Content.ReadAsStringAsync().Result).RootElement; - Agrupada = faturaParsed.TryGetProperty("multiple", out var _); - Status = faturaParsed.GetProperty("status").GetString(); - Arquivo = new FileInfo(fatura_path); - this.id = id; + pagina = JsonDocument.Parse(faturaParsed.GetProperty("external").GetString() !).RootElement.GetProperty("origin")[0].GetProperty("page").GetInt32(); + } - if (faturaParsed.GetProperty("external").GetString() is not null) - { - pagina = JsonDocument.Parse(faturaParsed.GetProperty("external").GetString() !).RootElement.GetProperty("origin")[0].GetProperty("page").GetInt32(); - } - - if (Agrupada) - { - Agrupada_children = faturaParsed.GetProperty("children").EnumerateArray(); - } + if (Agrupada) + { + Agrupada_children = faturaParsed.GetProperty("children").EnumerateArray(); } } diff --git a/Webhook 4docs/Program.cs b/Webhook 4docs/Program.cs index 52d21d1..0c1a48c 100644 --- a/Webhook 4docs/Program.cs +++ b/Webhook 4docs/Program.cs @@ -13,33 +13,14 @@ namespace Webhook_4docs { PermitLimit = 1 }); + public static void Main(string[] args) { - static string IndexedFilename(string stub, string extension) - { - int ix = 0; - string? filename = null; - - if (File.Exists(String.Format("{0}.{1}", stub, extension))) - { - do - { - ix++; - filename = String.Format("{0} ({1}).{2}", stub, ix, extension); - } while (File.Exists(filename)); - } - else - { - filename = String.Format("{0}.{1}", stub, extension); - } - return filename; - } - var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddJsonFile("appsettings.json"); var connectionString = builder.Configuration.GetConnectionString("WebhookDbContext"); - string? appFolder = builder.Configuration["PathBase"]; + var appFolder = builder.Configuration["PathBase"]; builder.Services.AddDbContext(options => options.UseNpgsql(connectionString) @@ -51,213 +32,382 @@ namespace Webhook_4docs loggingBuilder.AddConsole(); }); - // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); - var logger = app.Services.GetRequiredService>(); using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.Database.EnsureCreated(); dbContext.Database.Migrate(); } - string fatura_arquivo; - // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { 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(); } - - app.UsePathBase(appFolder); + + if (!string.IsNullOrWhiteSpace(appFolder)) + { + app.UsePathBase(appFolder); + } app.UseRouting(); - app.UseStaticFiles(); - app.UseAuthorization(); app.MapRazorPages(); - app.Use((context, next) => + // PATCH /api -> health ou invoice (json) + app.MapPatch("/api", async context => { - context.Request.PathBase = new PathString(appFolder); - return next(context); + var logger = context.RequestServices.GetRequiredService>(); + var bodyText = await SafeReadBodyAsync(context.Request, logger); + if (bodyText is null) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Invalid JSON"); + return; + } + + if (!TryParseJson(bodyText, out var root, logger)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Invalid JSON"); + return; + } + + // Health: healthy + blame + locationID + bool isHealth = + root.TryGetProperty("healthy", out _) && + root.TryGetProperty("blame", out _) && + root.TryGetProperty("locationID", out _); + + if (isHealth) + { + await ProcessHealthAsync(root, GetCaminhoDb(), logger); + context.Response.StatusCode = StatusCodes.Status200OK; + return; + } + + // Invoice: requestID/requestId + json + bool hasRequestId = root.TryGetProperty("requestID", out var reqIdEl); + bool hasJson = root.TryGetProperty("json", out var dataEl); + + if (hasRequestId && hasJson) + { + await ProcessInvoiceAsync(root, dataEl, GetCaminhoDb(), logger, app, reqIdEl.ToString()); + + context.Response.StatusCode = StatusCodes.Status200OK; + return; + } + + logger.LogDebug("PATCH /api payload não reconhecido."); + context.Response.StatusCode = StatusCodes.Status202Accepted; }); - // Endpoint para \api - app.UseEndpoints(endpoints => + // PUT /api -> somente invoice (result) + app.MapPut("/api", async context => { - endpoints.MapControllers(); - endpoints.MapPatch("/api", async context => + var logger = context.RequestServices.GetRequiredService>(); + var bodyText = await SafeReadBodyAsync(context.Request, logger); + if (bodyText is null) { - - string requestBody; - using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, true, 1024, true)) - { - requestBody = await reader.ReadToEndAsync(); - } + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Invalid JSON"); + return; + } - var JsonBody = JsonDocument.Parse(requestBody).RootElement; - string CaminhoDB = "X:/Middle/Informativo Setorial/Modelo Word/BD1_dados cadastrais e faturas.accdb"; - - if (JsonBody.TryGetProperty("requestID", out JsonElement fatura_ID_json)) - { - string fatura_ID = fatura_ID_json.ToString(); - - if (!JsonBody.TryGetProperty("json", out JsonElement root)) { return; } - - JsonElement DadosJson = JsonDocument.Parse(root.ToString()).RootElement; - - string tipoDocumento = root.Get("documentType").ToString() ?? string.Empty.ToLower(); - - if (tipoDocumento != string.Empty && tipoDocumento != "nota_fiscal") - { - return; - } - - Fatura fatura = new(fatura_ID, JsonBody); - bool completed = false; - - while (!completed) - { - var connLease = await connRateLimiter.AcquireAsync(); - - if (connLease.IsAcquired) - { - using (OleDbConnection conn = new(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21")) - { - if (conn != null) - { - if (conn.State == System.Data.ConnectionState.Closed) - { - await conn.OpenAsync(); - } - - try - { - fatura.Processar(conn); - } - catch (Exception ex) - { - completed = true; - connLease.Dispose(); - throw new Exception(ex.Message); - } - } - } - completed = true; - connLease.Dispose(); - } - } - - string? status = fatura.Status; - - int status_id = 0; - - switch (status) - { - case ("FATURA DUPLICADA NO BD"): - status_id = 4; - break; - case ("UNIDADE CONSUMIDORA NÃO LOCALIZADA NO BD"): - status_id = 5; - break; - case ("FATURA INCLUIDA NO BD"): - status_id = 6; - break; - case (_): - status = "ERRO"; - status_id = 7; - break; - } - - try - { - string path = $@"X:\Middle\Carteira {fatura.Gestao![0]}\Carteira {fatura.Gestao}\Faturas fourdocs\{status_id} - {status}"; - if (status_id == 6 && fatura.PastaTUSD!.Exists) - { - path = fatura.PastaTUSD!.FullName; - } - - if (status_id != 4) - { - fatura_arquivo = IndexedFilename($@"{path}\ID {fatura_ID!} - Mês {fatura.Mes} - Empresa {fatura.Empresa} - Unidade {fatura.Unidade}", "pdf"); - CriarArquivo(fatura_arquivo, JsonBody.GetProperty("pdfFile").ToString()); - - var DatabaseModel = new ProcessedInvoices - { - InvoiceId = Int32.Parse(fatura_ID), - DateTimeProcessed = DateTime.UtcNow, - Status = status, - InvoicePath = fatura_arquivo - }; - - logger.LogInformation("Fatura incluída no BD"); - - logger.LogInformation("Fatura salva na pasta " + fatura_arquivo); - - using (var scope = app.Services.CreateScope()) - { - var dbContext = scope.ServiceProvider.GetRequiredService(); - dbContext.ProcessedInvoices.Add(DatabaseModel); - await dbContext.SaveChangesAsync(); - } - } - else - { - logger.LogInformation("Fatura duplicada no BD"); - - logger.LogInformation("Fatura descartada"); - } - } - catch - { - logger.LogError("Erro no processamento da fatura"); - } - } - else if (JsonBody.TryGetProperty("healthy", out _) && JsonBody.TryGetProperty("blame", out _) && JsonBody.TryGetProperty("locationID", out _)) - { - double errorID = 0; - - switch (JsonBody.GetProperty("healthy").GetBoolean(), JsonBody.GetProperty("blame").ToString().ToLower()) - { - case (false, "user"): - logger.LogError("Loc ID: " + JsonBody.GetProperty("locationID").ToString() + " Sem acesso"); - errorID = 1; - break; - case (false, "system"): - logger.LogError("Loc ID: " + JsonBody.GetProperty("locationID").ToString() + " Com erro de sistema"); - errorID = 2; - break; - case (false, _): - logger.LogError("Loc ID: " + JsonBody.GetProperty("locationID").ToString() + " Com erro " + JsonBody.GetProperty("blame").ToString()); - errorID = 3; - break; - case (true, _): - //logger.LogInformation("Loc ID: " + JsonBody.GetProperty("locationID").ToString() + " está saudável"); - break; - } - - var errorText = GetErrorText(JsonBody); - await UpdateErrorIdStatusAsync(CaminhoDB, JsonBody.GetProperty("locationID").GetInt64(), errorID, errorText, logger); - - } - }); - //implementação futura para receber as faturas enviadas via api de forma assíncrona - Upload4Docs (substituir parte do agendador de tarefas) - endpoints.MapPut("/api", async context => + if (!TryParseJson(bodyText, out var root, logger)) { - var a = 10; - a++; - await Task.CompletedTask; - }); + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync("Invalid JSON"); + return; + } + + // Invoice: requestID/requestId + result + bool hasRequestId = root.TryGetProperty("requestId", out var reqIdEl); + bool hasResult = root.TryGetProperty("result", out var dataEl); + + if (!(hasRequestId && hasResult)) + { + logger.LogDebug("PUT /api payload inválido para este endpoint."); + context.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + + await ProcessInvoiceAsync(root, dataEl, GetCaminhoDb(), logger, app, reqIdEl.ToString()); + + context.Response.StatusCode = StatusCodes.Status200OK; }); + app.Run(); + + string GetCaminhoDb() => + @"X:/Middle/Informativo Setorial/Modelo Word/BD1_dados cadastrais e faturas.accdb"; + } + + // ===== Utilitários de request/JSON ===== + static async Task SafeReadBodyAsync(HttpRequest request, ILogger logger, int maxBytes = 10 * 1024 * 1024) + { + request.EnableBuffering(); + using var ms = new MemoryStream(capacity: Math.Min(maxBytes, 1024 * 64)); + await request.Body.CopyToAsync(ms); + if (ms.Length > maxBytes) + { + logger.LogWarning("Payload excede o limite de {MaxBytes} bytes.", maxBytes); + return null; + } + ms.Position = 0; + using var reader = new StreamReader(ms, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true); + return await reader.ReadToEndAsync(); + } + + static bool TryParseJson(string text, out JsonElement root, ILogger logger) + { + try + { + root = JsonDocument.Parse(text).RootElement.Clone(); + return true; + } + catch (JsonException ex) + { + logger.LogWarning(ex, "Falha ao parsear JSON."); + root = default; + return false; + } + } + + // ===== Processamento de Invoice ===== + static async Task ProcessInvoiceAsync(JsonElement root, JsonElement dataEl, string caminhoDB, ILogger logger, WebApplication app, string fatura_ID) + { + var tipoDocumento = dataEl.Get("documentType")?.GetString()?.ToLowerInvariant() + ?? string.Empty; + + var deveProcessar = string.IsNullOrEmpty(tipoDocumento) || tipoDocumento == "nota_fiscal"; + if (!deveProcessar) + { + logger.LogInformation("Invoice ignorada por tipoDocumento: {TipoDocumento}", tipoDocumento); + return; + } + + var hasPath = !string.IsNullOrEmpty(root.GetProperty("clientData").ToString()); + + + if (hasPath) + { + root.GetProperty("clientData").TryGetProperty("fatura_PATH", out var pathEl); + await ProcessInvoiceFromPath(root, fatura_ID, pathEl!.GetString()!, caminhoDB, logger, app); + } + else + { + await ProcessInvoiceFromJson(root, fatura_ID, caminhoDB, logger, app); + } + } + + static async Task ProcessInvoiceFromPath(JsonElement root, string fatura_ID, string pdfPath, string caminhoDB, ILogger logger, WebApplication app) + { + var fatura = new Fatura(fatura_ID, root, pdfPath); + var completed = false; + + while (!completed) + { + var connLease = await connRateLimiter.AcquireAsync(); + if (!connLease.IsAcquired) continue; + + try + { + using var conn = new OleDbConnection(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + caminhoDB + ";Jet OLEDB:Database Password=gds21"); + if (conn.State == System.Data.ConnectionState.Closed) + { + await conn.OpenAsync(); + } + + fatura.Processar(conn); + completed = true; + } + catch (Exception ex) + { + completed = true; + throw new Exception(ex.Message); + } + finally + { + connLease.Dispose(); + } + } + + await FinalizarPersistenciaAsync(fatura, fatura_ID, async targetPath => + { + try + { + if (!File.Exists(targetPath)) + { + fatura.Mover(false); + } + } + catch (IOException ioEx) + { + logger.LogWarning(ioEx, "Falha ao copiar arquivo para {Target}", targetPath); + } + await Task.CompletedTask; + }, logger, app); + } + + static async Task ProcessInvoiceFromJson(JsonElement root, string fatura_ID, string caminhoDB, ILogger logger, WebApplication app) + { + var fatura = new Fatura(fatura_ID, root); + var completed = false; + + while (!completed) + { + var connLease = await connRateLimiter.AcquireAsync(); + if (!connLease.IsAcquired) continue; + + try + { + using var conn = new OleDbConnection(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + caminhoDB + ";Jet OLEDB:Database Password=gds21"); + if (conn.State == System.Data.ConnectionState.Closed) + { + await conn.OpenAsync(); + } + + fatura.Processar(conn); + completed = true; + } + catch (Exception ex) + { + completed = true; + throw new Exception(ex.Message); + } + finally + { + connLease.Dispose(); + } + } + + await FinalizarPersistenciaAsync(fatura, fatura_ID, async targetPath => + { + var base64 = root.Get("pdfFile")?.GetString(); + + if (string.IsNullOrWhiteSpace(base64)) + { + logger.LogWarning("Campo pdfFile/pdfFileBase64 ausente no payload."); + return; + } + + CriarArquivo(targetPath, base64!); + await Task.CompletedTask; + }, logger, app); + } + + static async Task FinalizarPersistenciaAsync(Fatura fatura, string fatura_ID, Func persistFile, ILogger logger, WebApplication app) + { + var status = fatura.Status; + var status_id = status switch + { + "FATURA DUPLICADA NO BD" => 4, + "UNIDADE CONSUMIDORA NÃO LOCALIZADA NO BD" => 5, + "FATURA INCLUIDA NO BD" => 6, + _ => 7 + }; + status = status_id == 7 ? "ERRO" : status; + + try + { + var pathBase = $@"X:\Middle\Carteira {fatura.Gestao![0]}\Carteira {fatura.Gestao}\Faturas fourdocs\{status_id} - {status}"; + if (status_id == 6 && fatura.PastaTUSD!.Exists) + { + pathBase = fatura.PastaTUSD!.FullName; + } + + if (status_id != 4) + { + Directory.CreateDirectory(pathBase); + var targetFile = IndexedFilename($@"{pathBase}\ID {fatura_ID!} - Mês {fatura.Mes} - Empresa {fatura.Empresa} - Unidade {fatura.Unidade}", "pdf"); + + await persistFile(targetFile); + + var dbModel = new ProcessedInvoices + { + InvoiceId = int.Parse(fatura_ID), + DateTimeProcessed = DateTime.UtcNow, + Status = status, + InvoicePath = targetFile + }; + + logger.LogInformation("Fatura incluída no BD"); + logger.LogInformation("Fatura salva na pasta {Arquivo}", targetFile); + + using var scope = app.Services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.ProcessedInvoices.Add(dbModel); + await dbContext.SaveChangesAsync(); + } + else + { + logger.LogInformation("Fatura duplicada no BD"); + logger.LogInformation("Fatura descartada"); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Erro no processamento da fatura"); + } + } + + public static void CriarArquivo(string fatura_arquivo, string pdfFile64) + { + if (!File.Exists(fatura_arquivo)) + { + byte[] bytes = Convert.FromBase64String(pdfFile64); + using var stream = new FileStream(fatura_arquivo, FileMode.CreateNew, FileAccess.Write, FileShare.None); + using var writer = new BinaryWriter(stream); + writer.Write(bytes, 0, bytes.Length); + } + } + + static string IndexedFilename(string stub, string extension) + { + int ix = 0; + string? filename; + + if (File.Exists($"{stub}.{extension}")) + { + do + { + ix++; + filename = $"{stub} ({ix}).{extension}"; + } while (File.Exists(filename)); + } + else + { + filename = $"{stub}.{extension}"; + } + return filename; + } + + // ===== Processamento de Health ===== + static async Task ProcessHealthAsync(JsonElement jsonBody, string caminhoDB, ILogger logger) + { + var healthy = jsonBody.GetProperty("healthy").GetBoolean(); + var blame = jsonBody.GetProperty("blame").GetString()?.ToLowerInvariant() ?? string.Empty; + var locationId = jsonBody.GetProperty("locationID").ToString(); + + var (errorID, logAction) = (healthy, blame) switch + { + (false, "user") => (1d, (Action)(() => logger.LogError("Loc ID: {LocationId} Sem acesso", locationId))), + (false, "system") => (2d, (Action)(() => logger.LogError("Loc ID: {LocationId} Com erro de sistema", locationId))), + (false, var other) => (3d, (Action)(() => logger.LogError("Loc ID: {LocationId} Com erro {Blame}", locationId, other))), + (true, _) => (0d, (Action)(() => { })) + }; + + logAction(); + + var errorText = GetErrorText(jsonBody); + await UpdateErrorIdStatusAsync(caminhoDB, jsonBody.GetProperty("locationID").GetInt64(), errorID, errorText, logger); } //tenta pegar o deactivationReport, se não conseguir ou for null, tenta pegar o websiteText, se não conseguir ou for null, tenta pegar o systemText. As propriedades podem não exisir public static string GetErrorText(JsonElement root) @@ -283,6 +433,7 @@ namespace Webhook_4docs return errorText; } + public static async Task UpdateErrorIdStatusAsync(string CaminhoDB, double location_id, double errorID, string systemText, ILogger logger) { int test = 0; @@ -297,40 +448,35 @@ namespace Webhook_4docs { try { - using (OleDbConnection conn = new (@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21")) + using (OleDbConnection conn = new(@"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + CaminhoDB + ";Jet OLEDB:Database Password=gds21")) { await conn.OpenAsync(); - using (OleDbCommand cmd = new ( + using (OleDbCommand cmd = new( @"UPDATE AgVirtual4Docs SET errorID = @errorID, status = @status WHERE location_id = @location_id", conn)) { - // Adiciona parâmetros de forma segura cmd.Parameters.AddWithValue("@errorID", errorID); cmd.Parameters.AddWithValue("@status", systemText); cmd.Parameters.AddWithValue("@location_id", location_id); - // Executa o comando e captura o resultado test = await cmd.ExecuteNonQueryAsync(); } } - break; // Sai do loop em caso de sucesso + break; } catch (OleDbException ex) { - // Registra o erro logger.LogInformation($"Erro no OleDb update: {ex.Message} (Tentativa {attempt + 1} de {maxRetries})"); if (attempt < maxRetries - 1) { - // Aguarda antes de tentar novamente - await Task.Delay(1000 * (int)Math.Pow(2, attempt)); // Retry com Backoff Exponencial + await Task.Delay(1000 * (int)Math.Pow(2, attempt)); } else { - // Propaga a exceção na última tentativa throw; } } @@ -339,32 +485,17 @@ namespace Webhook_4docs connLease.Dispose(); } - attempt++; // Incrementa a tentativa após adquirir o lease, mesmo que falhe + attempt++; } else { - // Aguarda um curto período antes de tentar novamente se não conseguiu adquirir o lease await Task.Delay(200); } } - return test; } - public static void CriarArquivo(string fatura_arquivo, string pdfFile64) - { - //string fatura_arquivo = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test.pdf"; - if (!File.Exists(fatura_arquivo)) - { - byte[] bytes = Convert.FromBase64String(pdfFile64); - - System.IO.FileStream stream = new(fatura_arquivo, FileMode.CreateNew); - System.IO.BinaryWriter writer = - new(stream); - writer.Write(bytes, 0, bytes.Length); - writer.Close(); - } - } } + public static partial class JsonExtensions { public static JsonElement? Get(this JsonElement element, string name) => @@ -375,7 +506,7 @@ namespace Webhook_4docs { if (element.ValueKind == JsonValueKind.Null || element.ValueKind == JsonValueKind.Undefined) return null; - // Throw if index < 0 + return index < element.GetArrayLength() ? element[index] : null; } } diff --git a/Webhook 4docs/appsettings.json b/Webhook 4docs/appsettings.json index a710ae7..8c2e770 100644 --- a/Webhook 4docs/appsettings.json +++ b/Webhook 4docs/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "Logging": { "LogLevel": { "Default": "Information", @@ -17,7 +17,7 @@ "Kestrel": { "Endpoints": { "HttpLocalhost": { - "Url": "http://localhost:8664/" + "Url": "http://*:8664/" } } },