diff --git a/OnlyOneAccessTemplate/Controllers/DynamicProxyController.cs b/OnlyOneAccessTemplate/Controllers/DynamicProxyController.cs new file mode 100644 index 0000000..6d22293 --- /dev/null +++ b/OnlyOneAccessTemplate/Controllers/DynamicProxyController.cs @@ -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 _logger; + + public DynamicProxyController( + IModuleService moduleService, + HttpClient httpClient, + IRateLimitService rateLimitService, + ILogger logger) + { + _moduleService = moduleService; + _httpClient = httpClient; + _rateLimitService = rateLimitService; + _logger = logger; + } + + [HttpPost("{moduleId}/{action}")] + [HttpGet("{moduleId}/{action}")] + [HttpPut("{moduleId}/{action}")] + [HttpDelete("{moduleId}/{action}")] + public async Task 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"; + } + } +} \ No newline at end of file diff --git a/OnlyOneAccessTemplate/Controllers/MenuController.cs b/OnlyOneAccessTemplate/Controllers/MenuController.cs new file mode 100644 index 0000000..08f4894 --- /dev/null +++ b/OnlyOneAccessTemplate/Controllers/MenuController.cs @@ -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 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 }); + } + } + } +} \ No newline at end of file diff --git a/OnlyOneAccessTemplate/Controllers/ModuleManagementController.cs b/OnlyOneAccessTemplate/Controllers/ModuleManagementController.cs new file mode 100644 index 0000000..003f22b --- /dev/null +++ b/OnlyOneAccessTemplate/Controllers/ModuleManagementController.cs @@ -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 _logger; + private readonly IConfiguration _configuration; + + public ModuleManagementController( + IModuleService moduleService, + ILogger logger, + IConfiguration configuration) + { + _moduleService = moduleService; + _logger = logger; + _configuration = configuration; + } + + [HttpPost("register")] + public async Task 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 + { + ["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 { 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(), + SeoDescriptions = request.SeoDescriptions ?? new Dictionary(), + SeoKeywords = request.SeoKeywords ?? new Dictionary(), + + // 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 + { + ["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 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 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 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 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 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? 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? SeoTitles { get; set; } + public Dictionary? SeoDescriptions { get; set; } + public Dictionary? 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; } + } +} \ No newline at end of file diff --git a/OnlyOneAccessTemplate/Models/ModuleConfig.cs b/OnlyOneAccessTemplate/Models/ModuleConfig.cs index 0965bc1..22a6db3 100644 --- a/OnlyOneAccessTemplate/Models/ModuleConfig.cs +++ b/OnlyOneAccessTemplate/Models/ModuleConfig.cs @@ -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 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 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 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 SeoTitles { get; set; } = new(); + public Dictionary SeoDescriptions { get; set; } = new(); + public Dictionary 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; } } } diff --git a/OnlyOneAccessTemplate/register-sentence-converter.http b/OnlyOneAccessTemplate/register-sentence-converter.http new file mode 100644 index 0000000..0237d56 --- /dev/null +++ b/OnlyOneAccessTemplate/register-sentence-converter.http @@ -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