feat: proxy para arquivos dos modulos com ratelimit
This commit is contained in:
parent
6b79a44e39
commit
f3de10cc4f
113
OnlyOneAccessTemplate/Controllers/DynamicProxyController.cs
Normal file
113
OnlyOneAccessTemplate/Controllers/DynamicProxyController.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
[Route("api/modules")]
|
||||
public class DynamicProxyController : ControllerBase
|
||||
{
|
||||
private readonly IModuleService _moduleService;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IRateLimitService _rateLimitService;
|
||||
private readonly ILogger<DynamicProxyController> _logger;
|
||||
|
||||
public DynamicProxyController(
|
||||
IModuleService moduleService,
|
||||
HttpClient httpClient,
|
||||
IRateLimitService rateLimitService,
|
||||
ILogger<DynamicProxyController> logger)
|
||||
{
|
||||
_moduleService = moduleService;
|
||||
_httpClient = httpClient;
|
||||
_rateLimitService = rateLimitService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("{moduleId}/{action}")]
|
||||
[HttpGet("{moduleId}/{action}")]
|
||||
[HttpPut("{moduleId}/{action}")]
|
||||
[HttpDelete("{moduleId}/{action}")]
|
||||
public async Task<IActionResult> ProxyRequest(string moduleId, string action)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Buscar configuração do módulo
|
||||
var module = await _moduleService.GetModuleConfigAsync(moduleId);
|
||||
if (module == null || !module.IsActive)
|
||||
{
|
||||
return NotFound(new { success = false, message = "Módulo não encontrado ou inativo" });
|
||||
}
|
||||
|
||||
// Rate limiting
|
||||
var clientIP = GetClientIP();
|
||||
await _rateLimitService.RecordRequestAsync(clientIP);
|
||||
|
||||
if (await _rateLimitService.ShouldShowCaptchaAsync(clientIP))
|
||||
{
|
||||
return StatusCode(429, new
|
||||
{
|
||||
success = false,
|
||||
message = "Rate limit exceeded",
|
||||
requiresCaptcha = true
|
||||
});
|
||||
}
|
||||
|
||||
// Verificar se o endpoint existe no mapeamento
|
||||
if (!module.ProxyMappings.ContainsKey(action))
|
||||
{
|
||||
return NotFound(new { success = false, message = $"Ação '{action}' não disponível para este módulo" });
|
||||
}
|
||||
|
||||
var targetUrl = module.ProxyMappings[action];
|
||||
|
||||
// Preparar requisição
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
new HttpMethod(Request.Method),
|
||||
targetUrl + Request.QueryString);
|
||||
|
||||
// Copiar headers necessários
|
||||
foreach (var header in module.Headers)
|
||||
{
|
||||
requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
// Copiar corpo da requisição se necessário
|
||||
if (Request.ContentLength > 0)
|
||||
{
|
||||
requestMessage.Content = new StreamContent(Request.Body);
|
||||
if (Request.ContentType != null)
|
||||
{
|
||||
requestMessage.Content.Headers.TryAddWithoutValidation("Content-Type", Request.ContentType);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Proxying {Method} request to {ModuleId}.{Action} -> {TargetUrl}",
|
||||
Request.Method, moduleId, action, targetUrl);
|
||||
|
||||
// Fazer requisição
|
||||
var response = await _httpClient.SendAsync(requestMessage);
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
// Retornar resposta
|
||||
Response.StatusCode = (int)response.StatusCode;
|
||||
|
||||
if (response.Content.Headers.ContentType?.MediaType != null)
|
||||
{
|
||||
Response.ContentType = response.Content.Headers.ContentType.MediaType;
|
||||
}
|
||||
|
||||
return Content(responseContent);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro no proxy para {ModuleId}.{Action}", moduleId, action);
|
||||
return StatusCode(500, new { success = false, message = "Erro interno do servidor" });
|
||||
}
|
||||
}
|
||||
|
||||
private string GetClientIP()
|
||||
{
|
||||
return HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
51
OnlyOneAccessTemplate/Controllers/MenuController.cs
Normal file
51
OnlyOneAccessTemplate/Controllers/MenuController.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
[Route("api/menu")]
|
||||
public class MenuController : ControllerBase
|
||||
{
|
||||
private readonly IModuleService _moduleService;
|
||||
|
||||
public MenuController(IModuleService moduleService)
|
||||
{
|
||||
_moduleService = moduleService;
|
||||
}
|
||||
|
||||
[HttpGet("converters")]
|
||||
public async Task<IActionResult> GetConvertersMenu(string language = "pt")
|
||||
{
|
||||
try
|
||||
{
|
||||
var modules = await _moduleService.GetAllActiveModulesAsync();
|
||||
|
||||
var menuItems = modules
|
||||
.Where(m => m.ShowInMenu && m.IsActive && m.IsHealthy)
|
||||
.OrderBy(m => m.MenuOrder)
|
||||
.ThenBy(m => m.MenuTitle)
|
||||
.GroupBy(m => m.MenuCategory)
|
||||
.Select(g => new
|
||||
{
|
||||
category = g.Key,
|
||||
items = g.Select(m => new
|
||||
{
|
||||
moduleId = m.ModuleId,
|
||||
title = m.SeoTitles.ContainsKey(language) ? m.SeoTitles[language] : m.MenuTitle,
|
||||
description = m.SeoDescriptions.ContainsKey(language) ? m.SeoDescriptions[language] : m.MenuDescription,
|
||||
icon = m.MenuIcon,
|
||||
url = $"/{language}/{m.RequestBy}",
|
||||
order = m.MenuOrder,
|
||||
isNew = m.CreatedAt > DateTime.UtcNow.AddDays(-7) // Novos nos últimos 7 dias
|
||||
}).ToList()
|
||||
}).ToList();
|
||||
|
||||
return Ok(new { success = true, menu = menuItems });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
326
OnlyOneAccessTemplate/Controllers/ModuleManagementController.cs
Normal file
326
OnlyOneAccessTemplate/Controllers/ModuleManagementController.cs
Normal file
@ -0,0 +1,326 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OnlyOneAccessTemplate.Services;
|
||||
using OnlyOneAccessTemplate.Models;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
namespace OnlyOneAccessTemplate.Controllers
|
||||
{
|
||||
[Route("api/module-management")]
|
||||
[ApiController]
|
||||
public class ModuleManagementController : ControllerBase
|
||||
{
|
||||
private readonly IModuleService _moduleService;
|
||||
private readonly ILogger<ModuleManagementController> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public ModuleManagementController(
|
||||
IModuleService moduleService,
|
||||
ILogger<ModuleManagementController> logger,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
_moduleService = moduleService;
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> RegisterModule([FromBody] ModuleRegistrationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Registrando novo módulo: {ModuleId}", request.ModuleId);
|
||||
|
||||
// Verificar se módulo já existe
|
||||
var existing = await _moduleService.GetModuleConfigAsync(request.ModuleId);
|
||||
if (existing != null)
|
||||
{
|
||||
return BadRequest(new { success = false, message = "Módulo já existe" });
|
||||
}
|
||||
|
||||
// Testar conectividade com o módulo
|
||||
var healthUrl = $"{request.BaseUrl.TrimEnd('/')}/{request.HealthEndpoint?.TrimStart('/') ?? "api/converter/health"}";
|
||||
var isHealthy = await TestModuleHealth(healthUrl);
|
||||
|
||||
// Gerar API Key
|
||||
var apiKey = GenerateApiKey();
|
||||
|
||||
// Criar configuração
|
||||
var moduleConfig = new ModuleConfig
|
||||
{
|
||||
ModuleId = request.ModuleId,
|
||||
Name = request.Name,
|
||||
Url = $"{request.BaseUrl.TrimEnd('/')}/modules/{request.ModuleId}",
|
||||
RequestBy = request.ModuleId,
|
||||
IsActive = request.AutoActivate.HasValue && request.AutoActivate.Value && isHealthy,
|
||||
CacheMinutes = request.CacheMinutes ?? 5,
|
||||
|
||||
// Proxy Configuration
|
||||
UseProxy = true,
|
||||
ProxyEndpoint = $"/api/modules/{request.ModuleId}",
|
||||
ProxyMappings = new Dictionary<string, string>
|
||||
{
|
||||
["convert"] = $"{request.BaseUrl}/api/converter/convert",
|
||||
["config"] = $"{request.BaseUrl}/api/converter/config",
|
||||
["health"] = $"{request.BaseUrl}/api/converter/health"
|
||||
},
|
||||
|
||||
// Security
|
||||
ApiKey = apiKey,
|
||||
AllowedOrigins = request.AllowedOrigins ?? new List<string> { Request.GetDisplayUrl() },
|
||||
RateLimitPerMinute = request.RateLimitPerMinute ?? 60,
|
||||
|
||||
// Menu
|
||||
MenuTitle = request.MenuTitle ?? request.Name,
|
||||
MenuDescription = request.MenuDescription,
|
||||
MenuIcon = request.MenuIcon ?? "fas fa-exchange-alt",
|
||||
MenuCategory = request.MenuCategory ?? "Conversores",
|
||||
MenuOrder = request.MenuOrder ?? 0,
|
||||
ShowInMenu = request.ShowInMenu ?? true,
|
||||
|
||||
// SEO
|
||||
SeoTitles = request.SeoTitles ?? new Dictionary<string, string>(),
|
||||
SeoDescriptions = request.SeoDescriptions ?? new Dictionary<string, string>(),
|
||||
SeoKeywords = request.SeoKeywords ?? new Dictionary<string, string>(),
|
||||
|
||||
// Technical
|
||||
HealthEndpoint = request.HealthEndpoint ?? "/api/converter/health",
|
||||
HealthCheckIntervalMinutes = request.HealthCheckIntervalMinutes ?? 5,
|
||||
AutoStart = request.AutoActivate ?? true,
|
||||
Version = request.Version ?? "1.0.0",
|
||||
IsHealthy = isHealthy,
|
||||
LastHealthCheck = DateTime.UtcNow,
|
||||
|
||||
// Developer
|
||||
DeveloperName = request.DeveloperName,
|
||||
DeveloperEmail = request.DeveloperEmail,
|
||||
Repository = request.Repository,
|
||||
Documentation = request.Documentation,
|
||||
|
||||
// Headers
|
||||
Headers = new Dictionary<string, string>
|
||||
{
|
||||
["X-API-Key"] = apiKey,
|
||||
["User-Agent"] = "ConvertIt-MainApp/1.0"
|
||||
}
|
||||
};
|
||||
|
||||
await _moduleService.SaveModuleConfigAsync(moduleConfig);
|
||||
|
||||
_logger.LogInformation("Módulo {ModuleId} registrado com sucesso", request.ModuleId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "Módulo registrado com sucesso",
|
||||
moduleId = request.ModuleId,
|
||||
apiKey = apiKey,
|
||||
proxyEndpoint = moduleConfig.ProxyEndpoint,
|
||||
isHealthy = isHealthy,
|
||||
isActive = moduleConfig.IsActive
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao registrar módulo {ModuleId}", request.ModuleId);
|
||||
return StatusCode(500, new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("modules")]
|
||||
public async Task<IActionResult> ListModules()
|
||||
{
|
||||
try
|
||||
{
|
||||
var modules = await _moduleService.GetAllActiveModulesAsync();
|
||||
|
||||
var result = modules.Select(m => new
|
||||
{
|
||||
moduleId = m.ModuleId,
|
||||
name = m.Name,
|
||||
isActive = m.IsActive,
|
||||
isHealthy = m.IsHealthy,
|
||||
lastHealthCheck = m.LastHealthCheck,
|
||||
version = m.Version,
|
||||
menuTitle = m.MenuTitle,
|
||||
menuCategory = m.MenuCategory,
|
||||
showInMenu = m.ShowInMenu,
|
||||
developerName = m.DeveloperName,
|
||||
proxyEndpoint = m.ProxyEndpoint
|
||||
});
|
||||
|
||||
return Ok(new { success = true, modules = result });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao listar módulos");
|
||||
return StatusCode(500, new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("modules/{moduleId}/toggle")]
|
||||
public async Task<IActionResult> ToggleModule(string moduleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await _moduleService.GetModuleConfigAsync(moduleId);
|
||||
if (module == null)
|
||||
{
|
||||
return NotFound(new { success = false, message = "Módulo não encontrado" });
|
||||
}
|
||||
|
||||
module.IsActive = !module.IsActive;
|
||||
module.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _moduleService.SaveModuleConfigAsync(module);
|
||||
|
||||
_logger.LogInformation("Módulo {ModuleId} {Status}", moduleId,
|
||||
module.IsActive ? "ativado" : "desativado");
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
moduleId = moduleId,
|
||||
isActive = module.IsActive,
|
||||
message = $"Módulo {(module.IsActive ? "ativado" : "desativado")} com sucesso"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao alternar módulo {ModuleId}", moduleId);
|
||||
return StatusCode(500, new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("modules/{moduleId}/health-check")]
|
||||
public async Task<IActionResult> CheckModuleHealth(string moduleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await _moduleService.GetModuleConfigAsync(moduleId);
|
||||
if (module == null)
|
||||
{
|
||||
return NotFound(new { success = false, message = "Módulo não encontrado" });
|
||||
}
|
||||
|
||||
var healthUrl = $"{GetBaseUrl(module.Url)}{module.HealthEndpoint}";
|
||||
var isHealthy = await TestModuleHealth(healthUrl);
|
||||
|
||||
module.IsHealthy = isHealthy;
|
||||
module.LastHealthCheck = DateTime.UtcNow;
|
||||
await _moduleService.SaveModuleConfigAsync(module);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
moduleId = moduleId,
|
||||
isHealthy = isHealthy,
|
||||
lastCheck = module.LastHealthCheck,
|
||||
healthUrl = healthUrl
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao verificar saúde do módulo {ModuleId}", moduleId);
|
||||
return StatusCode(500, new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("modules/{moduleId}")]
|
||||
public async Task<IActionResult> UnregisterModule(string moduleId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var module = await _moduleService.GetModuleConfigAsync(moduleId);
|
||||
if (module == null)
|
||||
{
|
||||
return NotFound(new { success = false, message = "Módulo não encontrado" });
|
||||
}
|
||||
|
||||
// Aqui você implementaria a remoção (dependendo de como está armazenado)
|
||||
// Por enquanto, vamos apenas desativar
|
||||
module.IsActive = false;
|
||||
module.UpdatedAt = DateTime.UtcNow;
|
||||
await _moduleService.SaveModuleConfigAsync(module);
|
||||
|
||||
_logger.LogInformation("Módulo {ModuleId} desregistrado", moduleId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
message = "Módulo desregistrado com sucesso"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Erro ao desregistrar módulo {ModuleId}", moduleId);
|
||||
return StatusCode(500, new { success = false, message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> TestModuleHealth(string healthUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
client.Timeout = TimeSpan.FromSeconds(10);
|
||||
var response = await client.GetAsync(healthUrl);
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateApiKey()
|
||||
{
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
var bytes = new byte[32];
|
||||
rng.GetBytes(bytes);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string GetBaseUrl(string fullUrl)
|
||||
{
|
||||
var uri = new Uri(fullUrl);
|
||||
return $"{uri.Scheme}://{uri.Host}:{uri.Port}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ModuleRegistrationRequest
|
||||
{
|
||||
public string ModuleId { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string BaseUrl { get; set; } = "";
|
||||
public bool? AutoActivate { get; set; } = true;
|
||||
public int? CacheMinutes { get; set; } = 5;
|
||||
public List<string>? AllowedOrigins { get; set; }
|
||||
public int? RateLimitPerMinute { get; set; } = 60;
|
||||
|
||||
// Menu
|
||||
public string? MenuTitle { get; set; }
|
||||
public string? MenuDescription { get; set; }
|
||||
public string? MenuIcon { get; set; }
|
||||
public string? MenuCategory { get; set; }
|
||||
public int? MenuOrder { get; set; }
|
||||
public bool? ShowInMenu { get; set; } = true;
|
||||
|
||||
// SEO
|
||||
public Dictionary<string, string>? SeoTitles { get; set; }
|
||||
public Dictionary<string, string>? SeoDescriptions { get; set; }
|
||||
public Dictionary<string, string>? SeoKeywords { get; set; }
|
||||
|
||||
// Technical
|
||||
public string? HealthEndpoint { get; set; }
|
||||
public int? HealthCheckIntervalMinutes { get; set; }
|
||||
public string? Version { get; set; }
|
||||
|
||||
// Developer
|
||||
public string? DeveloperName { get; set; }
|
||||
public string? DeveloperEmail { get; set; }
|
||||
public string? Repository { get; set; }
|
||||
public string? Documentation { get; set; }
|
||||
}
|
||||
}
|
||||
@ -25,5 +25,44 @@ namespace OnlyOneAccessTemplate.Models
|
||||
public string? JavaScriptFunction { get; set; } // Função de inicialização
|
||||
public string? CssUrl { get; set; } // CSS opcional
|
||||
public Dictionary<string, string> Assets { get; set; } = new(); // Assets adicionais
|
||||
|
||||
// ADICIONAR na classe ModuleConfig existente:
|
||||
|
||||
// Configurações de Proxy Dinâmico
|
||||
public string? ProxyEndpoint { get; set; } // "/api/modules/{moduleId}"
|
||||
public Dictionary<string, string> ProxyMappings { get; set; } = new(); // endpoint -> moduleUrl
|
||||
public bool UseProxy { get; set; } = true;
|
||||
|
||||
// Configurações de Segurança
|
||||
public string? ApiKey { get; set; } // Gerado automaticamente
|
||||
public List<string> AllowedOrigins { get; set; } = new();
|
||||
public int RateLimitPerMinute { get; set; } = 60;
|
||||
|
||||
// Configurações de Menu
|
||||
public string? MenuTitle { get; set; }
|
||||
public string? MenuDescription { get; set; }
|
||||
public string? MenuIcon { get; set; }
|
||||
public string? MenuCategory { get; set; } = "Conversores";
|
||||
public int MenuOrder { get; set; } = 0;
|
||||
public bool ShowInMenu { get; set; } = true;
|
||||
|
||||
// Configurações de SEO
|
||||
public Dictionary<string, string> SeoTitles { get; set; } = new();
|
||||
public Dictionary<string, string> SeoDescriptions { get; set; } = new();
|
||||
public Dictionary<string, string> SeoKeywords { get; set; } = new();
|
||||
|
||||
// Configurações Técnicas
|
||||
public string? HealthEndpoint { get; set; } = "/api/converter/health";
|
||||
public int HealthCheckIntervalMinutes { get; set; } = 5;
|
||||
public bool AutoStart { get; set; } = true;
|
||||
public string? Version { get; set; }
|
||||
public DateTime? LastHealthCheck { get; set; }
|
||||
public bool IsHealthy { get; set; } = false;
|
||||
|
||||
// Metadados do Desenvolvedor
|
||||
public string? DeveloperName { get; set; }
|
||||
public string? DeveloperEmail { get; set; }
|
||||
public string? Repository { get; set; }
|
||||
public string? Documentation { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
47
OnlyOneAccessTemplate/register-sentence-converter.http
Normal file
47
OnlyOneAccessTemplate/register-sentence-converter.http
Normal file
@ -0,0 +1,47 @@
|
||||
### Registrar o módulo sentence-converter
|
||||
POST https://localhost:7001/api/module-management/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"moduleId": "sentence-converter",
|
||||
"name": "Conversor de Primeira Maiúscula",
|
||||
"baseUrl": "https://localhost:7002",
|
||||
"autoActivate": true,
|
||||
"cacheMinutes": 5,
|
||||
"rateLimitPerMinute": 60,
|
||||
|
||||
"menuTitle": "Primeira Maiúscula",
|
||||
"menuDescription": "Converte texto para formato de primeira letra maiúscula",
|
||||
"menuIcon": "fas fa-text-height",
|
||||
"menuCategory": "Texto",
|
||||
"menuOrder": 1,
|
||||
"showInMenu": true,
|
||||
|
||||
"seoTitles": {
|
||||
"pt": "Conversor para Primeira Maiúscula Online - Gratuito",
|
||||
"en": "Sentence Case Converter Online - Free",
|
||||
"es": "Convertidor a Mayúscula Inicial en Línea - Gratis"
|
||||
},
|
||||
"seoDescriptions": {
|
||||
"pt": "Converta seu texto para o formato de primeira letra maiúscula rapidamente. Ferramenta gratuita e fácil de usar.",
|
||||
"en": "Convert your text to sentence case format quickly. Free and easy to use tool.",
|
||||
"es": "Convierte tu texto al formato de primera letra mayúscula rápidamente. Herramienta gratuita y fácil de usar."
|
||||
},
|
||||
|
||||
"version": "1.0.0",
|
||||
"developerName": "Seu Nome",
|
||||
"developerEmail": "seu@email.com",
|
||||
"repository": "https://github.com/user/sentence-converter"
|
||||
}
|
||||
|
||||
### Listar todos os módulos
|
||||
GET https://localhost:7001/api/module-management/modules
|
||||
|
||||
### Verificar saúde de um módulo
|
||||
POST https://localhost:7001/api/module-management/modules/sentence-converter/health-check
|
||||
|
||||
### Ativar/desativar módulo
|
||||
POST https://localhost:7001/api/module-management/modules/sentence-converter/toggle
|
||||
|
||||
### Buscar menu de conversores
|
||||
GET https://localhost:7001/api/menu/converters?language=pt
|
||||
Loading…
Reference in New Issue
Block a user